Bladeren bron

Merge pull request #8 from NielsOverkamp/initial_react_frontend

Finish initial frontend
NielsOverkamp 5 jaren geleden
bovenliggende
commit
8def673c19

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

@@ -1,13 +1,18 @@
 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={(event) => {
+            event.preventDefault();
+            event.stopPropagation();
+            play(event);
+        }}><i className="fa fa-arrow-up"/></button>}
     </div>;
 }
 
@@ -40,38 +45,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",

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

@@ -0,0 +1,51 @@
+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, current_player, play_card, swap_card } = message;
+
+    let play = null;
+    let card = null;
+    let text;
+    switch (state) {
+        case SiteState.SWAP_CARD:
+            play = swapCard;
+            if (swap_card) {
+                card = swap_card;
+                text = "Wacht op een kaart van je partner";
+            } else {
+                text = "Kies een kaart voor je partner";
+            }
+            break;
+        case SiteState.SWAP_CARD_OTHERS:
+            text = "Wacht op het andere team";
+            break;
+        case SiteState.PLAY_CARD:
+            play = playCard;
+            card = play_card;
+            text = "Kies een kaart om te spelen";
+            break;
+        case SiteState.PLAY_CARD_OTHER:
+            card = play_card;
+            text = `${current_player.color}(${current_player.name}) is een kaart aan het spelen`;
+            break;
+    }
+
+
+    return <Fragment>
+        <h1>{text}</h1>
+        {card &&
+        <Card value={card}/>
+        }
+        {card && play_card && state === SiteState.PLAY_CARD &&
+        <button className="btn btn-primary" onClick={confirmPlay}>Bevestig</button>
+        }
+        {card && (state === SiteState.PLAY_CARD || state === SiteState.SWAP_CARD) &&
+        <button className="btn btn-danger" onClick={undoPlay}>Neem terug</button>
+        }
+        <Hand cards={hand} play={play}/>
+    </Fragment>;
+}

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

@@ -1,10 +1,17 @@
-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)}/>)}
+        <div className="row">
+        {cards.map((c, i) => <div className="col-2">
+            <Card value={c}
+                                   animate={play !== null}
+                                   play={play === null ? null : (() => play(c))}
+                                   select={selected === i ? () => setSelected(null) : () => setSelected(i)}
+                                                      selected={selected === i}/></div>)}
+        </div>
     </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>;
 }

+ 31 - 26
frontend/keezen-frontend/src/Lobby.js

@@ -2,47 +2,52 @@ import React from "react";
 import "./css/lobby.css"
 import Colors from "./util/colors";
 import deck from './img/deck.svg';
-import { SiteState } from "./StateRouter";
+import { Commands } from "./StateRouter";
 
 export default function Lobby({ message, pickColor, deal }) {
-    const { color, options, state: { code: state, args: { game_code, ...state_args } } } = message;
-    const freeColors = (options && options.map((o) => o.color)) || [];
-    const circle_args = { state, myColor: color, freeColors, pickColor };
+    const { color, options, state, game_code, others, name,...args } = message;
+    const pickableColors = options.filter((o) => o.code === Commands.PICK_COLOR).map((o) => o.color);
+    const circle_args = { state, myColor: color, myName: name, others, pickableColors, pickColor };
+    const canDeal = options.find((o) => o.code === Commands.DEAL);
     return <div className="mt-4">
-        {
-            state === SiteState.JOIN_OTHERS &&
-            <div className="row my-2">
-                <h1>Je kan meedoen door naar <a href={`/${game_code}`}>{window.location.host}/{game_code}</a> te gaan
-                </h1>
-            </div>
-        }{/*<hr/>*/}{
-        state !== SiteState.JOIN_OTHERS &&
+        <div className="row my-2">
+            <h1>Je kan meedoen door naar <a href={`/${game_code}`}>{window.location.host}/{game_code}</a> te gaan
+            </h1>
+        </div>
         <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>
+        {
+            canDeal &&
+            <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>
-    }
+        }
     </div>
 }
 
-function Circle({ state, color, myColor, freeColors, pickColor }) {
+function Circle({ color, myColor, myName, others, pickableColors, pickColor }) {
     const isMine = color === myColor;
-    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" : ""}`}
+    const playerInfo = isMine ?
+        {color, name: myName} :
+        others.find((o) => o.color === color);
+    const isSelectable = pickableColors.includes(color);
+    const isPicked = !isSelectable;
+    const name = (playerInfo && playerInfo.name) || "";
+    return <div className="col-2 flex-column">
+        <div className={`color-pick 
+                ${color} ${isPicked ? "selected" : ""} 
+                ${isSelectable ? "selectable" : ""} 
+                ${isMine ? "mine" : ""}`}
              onClick={() => isSelectable && pickColor(color)}
         />
+        <h4>{name}</h4>
     </div>
 }

+ 86 - 33
frontend/keezen-frontend/src/StateRouter.js

@@ -3,17 +3,25 @@ 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",
+    // Dealing
+    DEAL: "deal",
+    DEAL_OTHER: "deal_other",
+    // Hand
+    SWAP_CARD: "swap_card",
+    SWAP_CARD_OTHERS: "swap_card_others",
+    PLAY_CARD: "play_card",
+    PLAY_CARD_OTHER: "play_card_other",
+
 }
 
 export const Commands = {
@@ -21,7 +29,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,32 +39,40 @@ 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: SiteState.WAITING_FOR_WS } :
+        { state: SiteState.JOIN_LINK, 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, options, game_code, ...args } = 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
-            }))
+                game_code,
+                text: "Neem deel aan spel",
+            })
         }
     }, [websocketStatus, message])
 
     useEffect(() => {
-        if (state_args && state_args.game_code !== undefined && state_args.game_code !== null) {
-            window.history.pushState(null, "", `/${state_args.game_code}`)
+        if (game_code !== undefined && game_code !== null) {
+            window.history.pushState(null, "", `/${game_code}`)
         }
-        if (state_args && isNaN(state_args.game_code)) {
+        if (isNaN(path_code)) {
             window.history.pushState(null, "", "/")
         }
     }, [message])
@@ -96,26 +112,63 @@ 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
-                                }))}/>
-        case SiteState.JOIN_OTHERS:
+                                    text: "Begin nieuw spel",
+                                })}
+                                joinGame={(code) => send({
+                                    code: Commands.JOIN_GAME,
+                                    game_code: 3936,
+                                    text: "Neem deel aan spel 3936",
+                                })}/>
         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_OTHERS:
+        case SiteState.PLAY_CARD:
+        case SiteState.PLAY_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 {args.current_player.color}({args.current_player.name}) heeft gedeeld</h1>
+        default:
+            return `state: ${state}, wsstatus: ${websocketStatus}`
     }
-
-    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 0 -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;
 }

+ 6 - 4
game.py

@@ -146,7 +146,7 @@ class Game(object):
         for other in self.players:
             if other.color != player.color:
                 other.message = f"{player.name} speelt een {card.denom}"
-                other.state = StateCode.PLAY_CARD_OTHER,
+                other.state = StateCode.PLAY_CARD_OTHER
                 other.play_card = card
 
 
@@ -228,9 +228,11 @@ class Game(object):
         
         for player in self.players:
             player.set_current(dealer)
+            player.play_card = None
             if player.color != dealer.color:
                 player.message = f"{dealer.name} is aan de beurt om te delen"
-                player.state= StateCode.DEAL_OTHER 
+                player.state = StateCode.DEAL_OTHER
+                
 
 
     def turn(self):
@@ -239,13 +241,13 @@ class Game(object):
         player.options.append(Option(OptionCode.PASS, "Pas"))
         player.message = "Kies een kaart om te spelen"
         player.state = StateCode.PLAY_CARD
-        player.play_card = None
 
         for player in self.players:
             player.set_current(self.current_player)
+            player.play_card = None
             if player.color != self.current_player.color:
                 player.message = f"{self.current_player.name} is aan de beurt"
-                player.state = StateCode.PLAY_CARD_OTHER 
+                player.state = StateCode.PLAY_CARD_OTHER
 
 
     def partner(self, player):

+ 2 - 1
player.py

@@ -53,6 +53,7 @@ class Player(object):
 
     color: Color
     name: str
+    game_code: int
     hand: List[Card]
     options: List[Option]
     message: str
@@ -68,6 +69,7 @@ class Player(object):
     def __init__(self, color = None, name = None):
         self.color = color
         self.name = name
+        self.game_code = None
         self.hand = []
         self.options = []
         self.message = "Even wachten"
@@ -100,7 +102,6 @@ class Player(object):
 
     def set_current(self, current_player):
         self.current_player = OtherPlayer(current_player.color, current_player.name)
-        self.play_card = current_player.play_card
 
     def merge_from(self, other):
         self.color = other.color

+ 3 - 3
webserver.py

@@ -97,8 +97,6 @@ async def handler(websocket, path):
                 await notify([(player, websocket)])
                 continue
 
-            # Thisis a change
-
             player.set_error(None)
 
             if option.code == OptionCode.NEW_GAME:
@@ -110,12 +108,13 @@ async def handler(websocket, path):
                     users.append(users[0])
                     users.remove(users[0])
                 player = game.join_player(option.user_name)
+                player.game_code = game_code
                 sockets[game_code] = [(player, websocket)]
                 await notify(sockets[game_code])
 
             elif option.code == OptionCode.JOIN_GAME:
                 game_code = option.game_code
-                game = games.get(game_code) if game_code > 0 else None
+                game = games.get(game_code) if game_code is not None and type(game_code) is int and game_code > 0 else None
 
                 if game is None:
                     player.message = f"onbekande code {game_code}"
@@ -128,6 +127,7 @@ async def handler(websocket, path):
                     users.append(users[0])
                     users.remove(users[0])
                 player = game.join_player(option.user_name)
+                player.game_code = game_code
                 sockets[game_code].append((player, websocket))
                 await notify(sockets[game_code])