소스 검색

Finish initial frontend

In this commit:
* Add Game component showing game state as text, the player hand and card chosen
* Rework url based routing
* Add button to join a game on landing page (currently links to 3936 game)
* Add simple deal button on deal state
niels 5 년 전
부모
커밋
ef3a7e71e4

+ 15 - 26
frontend/keezen-frontend/src/Card.js

@@ -1,13 +1,14 @@
 import React from 'react';
 
-export function CardComponent({value: {suit, denom}}, play) {
-    console.log({suit, denom})
-    return <div className={`playing-card card-${suit}`}>
+export function Card({value: {suit, denom}, play, animate, selected, select}) {
+    console.log({suit, denom, play})
+    return <div className={`playing-card card-${suit || Joker} ${animate ? "animate" : ""} ${selected ? "selected" : ""}`}
+                onClick={select || (() => {})}>
         <div className="card-top">
             <span className="card-denom">{denomToChar(denom)}</span>
             <span className="card-suit">{suitToIcon(suit)}</span>
         </div>
-        <button className="btn btn-success" onClick={play}><i className="fa fa-arrow-up"/></button>
+        {play && <button className="btn btn-success"  onClick={play}><i className="fa fa-arrow-up"/></button>}
     </div>;
 }
 
@@ -40,38 +41,26 @@ function denomToChar(denom) {
             return "V"
         case Denoms.JACK:
             return "J"
+        case Joker:
+            return "Joker"
         default:
             return denom
     }
 }
-
-export function fromString(s) {
-    s = s.toLowerCase()
-    if (s.toLowerCase() === Joker) {
-        return {denom: "Joker", suit: null}
-    }
-    const split = s.split(" ")
-    if (split.length !== 2) {
-        console.error("Unknown card", s)
-        return null
-    }
-    return {suit: split[0], denom: split[1]}
-}
-
 export const Joker = "joker"
 
 export const Suits = {
-    HEARTS: "harten",
-    DIAMONDS: "ruiten",
-    CLUBS: "klaver",
-    SPADES: "schoppen"
+    HEARTS: "hearts",
+    DIAMONDS: "diamonds",
+    CLUBS: "clubs",
+    SPADES: "spades"
 }
 
 export const Denoms = {
-    ACE: "aas",
-    KING: "koning",
-    QUEEN: "vrouw",
-    JACK: "boer",
+    ACE: "ace",
+    KING: "king",
+    QUEEN: "queen",
+    JACK: "jack",
     D10: "10",
     D9: "9",
     D8: "8",

+ 53 - 0
frontend/keezen-frontend/src/Game.js

@@ -0,0 +1,53 @@
+import React, { Fragment } from 'react';
+import Hand from "./Hand";
+import { SiteState } from "./StateRouter";
+import { Card } from "./Card";
+
+export default function Game({ message, swapCard, playCard, confirmPlay, undoPlay }) {
+    console.log(message);
+    const { hand, state: { code: state, args: {other_player_name, card} }, selected_card } = message;
+
+    let play = null;
+    let text;
+    switch (state) {
+        case SiteState.SWAP_CARD:
+            play = swapCard;
+            text = "Kies een kaart voor je partner";
+            break;
+        case SiteState.SWAP_CARD_PARTNER:
+            text = "Wacht op een kaart van je partner";
+            break;
+        case SiteState.SWAP_CARD_OTHERS:
+            text = "Wacht op het andere team";
+            break;
+        case SiteState.PLAY_CARD:
+            play = playCard;
+            text = "Kies een kaart om te spelen";
+            break;
+        case SiteState.PLAYING_CARD:
+            text = "Bevestig kaart keuze";
+            break;
+        case SiteState.PLAY_CARD_OTHER:
+            text = `${other_player_name} is een kaart aan het kiezen`;
+            break;
+        case SiteState.PLAYING_CARD_OTHER:
+            text = `${other_player_name} speelt kaart`;
+            break;
+
+    }
+
+
+    return <Fragment>
+        <h1>{text}</h1>
+        {card || selected_card ?
+            <Card value={card || selected_card}/> : null
+        }
+        {state === SiteState.PLAYING_CARD ?
+                <button className="btn btn-primary" onClick={confirmPlay}>Bevestig</button> : null
+        }
+        {state === SiteState.SWAP_CARD_PARTNER || state === SiteState.PLAYING_CARD ?
+            <button className="btn btn-danger" onClick={undoPlay}>Neem terug</button> : null
+        }
+        <Hand cards={hand} play={play}/>
+    </Fragment>;
+}

+ 9 - 5
frontend/keezen-frontend/src/Hand.js

@@ -1,10 +1,14 @@
-import React, {useState} from 'react';
-import { CardComponent } from "./Card";
+import React, { useState } from 'react';
+import { Card } from "./Card";
 
-export default function Hand({cards, play}) {
+export default function Hand({ cards, play }) {
     const [selected, setSelected] = useState(null);
 
     return <div className="hand">
-        {cards.map(c => <CardComponent value={c} play={() => play(c)}/>)}
+        {cards.map((c, i) => <Card value={c}
+                                   animate={play !== null}
+                                   play={play === null ? null : (() => play(c))}
+                                   select={selected === i ? () => setSelected(null) : () => setSelected(i)}
+                                   selected={selected === i}/>)}
     </div>
-}
+}

+ 3 - 2
frontend/keezen-frontend/src/LandingPage.js

@@ -1,8 +1,9 @@
 import React from 'react';
 import "./css/landingpage.css";
 
-export default function LandingPage({ state, newGame }) {
-    return <div className="landing-page row h-100 align-content-center justify-content-center">
+export default function LandingPage({ message, newGame, joinGame }) {
+    return <div className="landing-page row h-100 align-content-center justify-content-center flex-md-column">
         <button className="btn btn-primary mb-5" onClick={newGame}>Begin met spelen</button>
+        <button className="btn btn-secondary" onClick={joinGame}>Doe mee met ander spel</button>
     </div>;
 }

+ 21 - 18
frontend/keezen-frontend/src/Lobby.js

@@ -16,23 +16,23 @@ export default function Lobby({ message, pickColor, deal }) {
                 </h1>
             </div>
         }{/*<hr/>*/}{
-        state !== SiteState.JOIN_OTHERS &&
-        <div className="row justify-content-center my-2">
-            <Circle color={Colors.RED} {...circle_args}/>
-            <Circle color={Colors.YELLOW} {...circle_args}/>
-            <Circle color={Colors.BLUE} {...circle_args}/>
-            <Circle color={Colors.GREEN} {...circle_args}/>
-        </div>
-    }{
-        state === SiteState.FIRST_DEAL &&
-        <div className="row justify-content-center my-2">
-            <div className="btn btn-link" onClick={deal}>
-                <img src={deck} alt="kaartenstapel"/>
-                <br/>
-                <span>Delen</span>
+            state !== SiteState.JOIN_OTHERS &&
+            <div className="row justify-content-center my-2">
+                <Circle color={Colors.RED} {...circle_args}/>
+                <Circle color={Colors.YELLOW} {...circle_args}/>
+                <Circle color={Colors.BLUE} {...circle_args}/>
+                <Circle color={Colors.GREEN} {...circle_args}/>
             </div>
-        </div>
-    }
+        }{
+            state === SiteState.FIRST_DEAL &&
+            <div className="row justify-content-center my-2">
+                <div className="btn btn-link" onClick={deal}>
+                    <img src={deck} alt="kaartenstapel"/>
+                    <br/>
+                    <span>Delen</span>
+                </div>
+            </div>
+        }
     </div>
 }
 
@@ -41,8 +41,11 @@ function Circle({ state, color, myColor, freeColors, pickColor }) {
     const isSelectable = freeColors.includes(color)
     const isPicked = state === SiteState.PICK_COLOR && !isSelectable;
     return <div className="col-2">
-        <div className={`color-pick ${color} ${isPicked ? "selected" : ""} ${isSelectable ? "selectable" : ""} ${isMine ? "mine" : ""}`}
-             onClick={() => isSelectable && pickColor(color)}
+        <div className={`color-pick 
+                ${color} ${isPicked ? "selected" : ""} 
+                ${isSelectable ? "selectable" : ""} 
+                ${isMine ? "mine" : ""}`}
+            onClick={() => isSelectable && pickColor(color)}
         />
     </div>
 }

+ 87 - 22
frontend/keezen-frontend/src/StateRouter.js

@@ -3,17 +3,30 @@ import useWebsocket, { WebsocketStatus } from "./util/useWebsocket";
 import LoadingJoker from "./LoadingJoker";
 import LandingPage from "./LandingPage";
 import Lobby from "./Lobby";
+import Game from "./Game";
+import deck from "./img/deck.svg";
 
 export const SiteState = {
     // Frontend only
     WAITING_FOR_WS: "waiting_for_ws",
     JOIN_LINK: "join_link",
-    // Shared
+    // Lobby
     START: "start",
     JOIN_OTHERS: "join_others",
     PICK_COLOR: "pick_color",
     PICK_COLOR_OTHERS: "pick_color_others",
     FIRST_DEAL: "first_deal",
+    // Hand
+    DEAL: "deal",
+    DEAL_OTHER: "deal_other",
+    SWAP_CARD: "swap_card",
+    SWAP_CARD_PARTNER: "swap_card_partner",
+    SWAP_CARD_OTHERS: "swap_card_others",
+    PLAY_CARD: "play_card",
+    PLAY_CARD_OTHER: "play_card_other",
+    PLAYING_CARD: "playing_card",
+    PLAYING_CARD_OTHER: "playing_card_other",
+
 }
 
 export const Commands = {
@@ -21,7 +34,7 @@ export const Commands = {
     JOIN_GAME: "join_game",
     PICK_COLOR: "pick_color",
     DEAL: "deal",
-    CHANGE_CARD: "change_card",
+    SWAP_CARD: "swap_card",
     PLAY_CARD: "play_card",
     READY: "ready",
     UNDO_CARD: "undo_card",
@@ -31,24 +44,32 @@ export const Commands = {
 export default function StateRouter() {
     const [websocket, websocketStatus] = useWebsocket();
 
-    const path = window.location.pathname.split("/");
-    console.log(path);
-    const initial_state = path[1] === "" ?
-        { state: {code: SiteState.WAITING_FOR_WS }} :
-        { state: {code: SiteState.JOIN_LINK, args: {game_code: parseInt(path[1]) }}};
+    const path = window.location.pathname;
+
+    const path_code = path === "/" ?
+        null :
+        parseInt(path.split("/")[1]);
+
+    console.log({path, path_code});
+    const initial_state = path_code === null ?
+        { state: { code: SiteState.WAITING_FOR_WS } } :
+        { state: { code: SiteState.JOIN_LINK, args: { game_code: path_code } } };
 
+    const send = websocketStatus === WebsocketStatus.CONNECTED ?
+        (data) => websocket.send(JSON.stringify(data)) :
+        (data) => console.error(`Cannot send: websocket is in state ${websocketStatus}, data:`, data);
 
     const [message, setMessage] = useState(initial_state);
 
-    const {state: {code: state, args: state_args}, options} = message;
+    const { state: { code: state, args: state_args }, options } = message;
 
     useEffect(() => {
         if (websocketStatus === WebsocketStatus.CONNECTED && state === SiteState.JOIN_LINK) {
-            websocket.send(JSON.stringify({
+            send({
                 code: Commands.JOIN_GAME,
                 game_code: state_args.game_code,
-                text: "Neem deel aan spel",   //TODO replace with option
-            }))
+                text: "Neem deel aan spel",
+            })
         }
     }, [websocketStatus, message])
 
@@ -56,7 +77,7 @@ export default function StateRouter() {
         if (state_args && state_args.game_code !== undefined && state_args.game_code !== null) {
             window.history.pushState(null, "", `/${state_args.game_code}`)
         }
-        if (state_args && isNaN(state_args.game_code)) {
+        if (isNaN(path_code)) {
             window.history.pushState(null, "", "/")
         }
     }, [message])
@@ -96,25 +117,69 @@ export default function StateRouter() {
         case SiteState.WAITING_FOR_WS:
             return <LoadingJoker size={44}/>
         case SiteState.START:
-            return <LandingPage state={message}
-                                newGame={() => websocket.send(JSON.stringify({
+            return <LandingPage message={message}
+                                newGame={() => send({
                                     code: Commands.NEW_GAME,
-                                    text: "Begin nieuw spel",   //TODO replace with option
-                                }))}/>
+                                    text: "Begin nieuw spel",
+                                })}
+                                joinGame={(code) => send({
+                                    code: Commands.JOIN_GAME,
+                                    game_code: 3936,
+                                    text: "Neem deel aan spel 3936",
+                                })}/>
         case SiteState.JOIN_OTHERS:
         case SiteState.PICK_COLOR:
         case SiteState.PICK_COLOR_OTHERS:
         case SiteState.FIRST_DEAL:
             return <Lobby message={message}
-                          pickColor={(color) => websocket.send(JSON.stringify({
+                          pickColor={(color) => send({
                               code: Commands.PICK_COLOR,
-                              text: "Kies kleur",   //TODO replace with option
+                              text: "Kies kleur",
                               color
-                          }))}
-                          deal={() => websocket.send(JSON.stringify({
+                          })}
+                          deal={() => send({
                               code: Commands.DEAL,
-                              text: "Deel",   //TODO replace with option
-                          }))}/>
+                              text: "Deel", 
+                          })}/>
+        case SiteState.SWAP_CARD:
+        case SiteState.SWAP_CARD_PARTNER:
+        case SiteState.SWAP_CARD_OTHERS:
+        case SiteState.PLAY_CARD:
+        case SiteState.PLAY_CARD_OTHER:
+        case SiteState.PLAYING_CARD:
+        case SiteState.PLAYING_CARD_OTHER:
+            return <Game message={message}
+                         swapCard={(card) => send({
+                             code: Commands.SWAP_CARD,
+                             text: "Wissel kaart",
+                             card,
+                         })}
+                         playCard={(card) => send({
+                             code: Commands.PLAY_CARD,
+                             text: "Speel kaart",
+                             card,
+                         })}
+                         confirmPlay={() => send({
+                             code: Commands.READY,
+                             text: "Klaar/Bevestig kaart",
+                         })}
+                         undoPlay={() => send({
+                             code: Commands.UNDO_CARD,
+                             text: "Terug/Neem kaart terug",
+                         })}/>
+        case SiteState.DEAL:
+            return <div className="row justify-content-center flex-md-column my-2">
+                <h1>Klik om te delen</h1><br/>
+                <div className="btn btn-link" onClick={() => send({
+                    code: Commands.DEAL,
+                    text: "Delen",
+                })}>
+                    <img src={deck} alt="kaartenstapel"/><br/><span>Delen</span>
+                </div>
+            </div>;
+        case SiteState.DEAL_OTHER:
+            return <h1>Wachten tot {state_args.other_player_name} heeft gedeeld</h1>
+            
     }
 
     return `state: ${state}, wsstatus: ${websocketStatus}`

+ 24 - 14
frontend/keezen-frontend/src/css/Card.css

@@ -2,48 +2,58 @@
     display: flex;
     position: fixed;
     bottom: 0;
+    height: 160px;
 }
 
 .playing-card {
-    width: 165px;
-    height: 160px;
-    border-top: solid 2px;
-    border-left: solid 2px;
-    border-right: solid 2px;
-    border-color: lightgray;
-    border-top-left-radius: 15px;
-    border-top-right-radius: 15px;
+    width: 170px;
+    height: 240px;
+    border: 2px solid lightgray;
+    border-radius: 15px;
+
+    background-color: white;
 
     display: flex;
     flex-flow: row wrap;
     justify-content: center;
+    align-content: flex-start;
 
     padding: 20px;
-    margin: 70px 20px -70px;
+    margin: 70px 20px -150px;
     transition: margin-top 500ms ease;
 }
 
-.playing-card:hover {
+.playing-card.animate:hover,
+.playing-card.selected {
     margin-top: 0;
     transition: margin-top 500ms ease;
 }
 
+.playing-card.selected {
+    background-color: lightgray;
+    border-color: gray;
+}
+
 .playing-card .card-denom {
     font-size: 36pt;
-    margin-right: 50px;
-    width: 35px;
+    margin-right: 35px;
+    width: 55px;
+}
+
+.playing-card.card-joker .card-denom {
+    margin-right: 0;
 }
 
 .playing-card .card-suit {
     font-size: 27pt;
 }
 
-.playing-card.card-ruiten,.playing-card.card-harten
+.playing-card.card-diamonds,.playing-card.card-hearts
 {
     color: red;
 }
 
-.playing-card.card-schoppen,.playing-card.card-klaver
+.playing-card.card-spades,.playing-card.card-clubs
 {
     color: black;
 }