Prechádzať zdrojové kódy

Add user indicator to topbar in Game
and minor frontend tweaks

Changes:
* Add a uid to the card object. This is to allow react to keep track of the cards in the DOM
* Add pawn and username to topbar in Game
* Move deal screen to Game (excluding first deal)
* Extract scheduleSetName to a debounce hook
* Extract Pawn to a component
* Replace single shuffle with builtin random.shuffle in backend
* Remove somes unused code

niels 5 rokov pred
rodič
commit
fdbd1abf47

+ 10 - 9
cards.py

@@ -3,8 +3,10 @@ import random
 suits = ["hearts", "spades", "clubs", "diamonds"]
 denoms = ["ace", "king", "queen", "jack", "10", "9", "8", "7", "6", "5", "4", "3", "2"]
 
+
 class Card(object):
-    def __init__(self, suit, denom):
+    def __init__(self, uid, suit, denom):
+        self.uid = uid
         self.suit = suit
         self.denom = denom
 
@@ -19,15 +21,14 @@ class Card(object):
     def __ne__(self, other):
         return not self.__eq__(other)
 
-def shuffle(numberOfDecks):
-    deck = [Card(x, y) for x in suits for y in denoms] + [Card(None, "joker") for _ in range(3)]
-    stock = deck * numberOfDecks
 
-    for n in range(len(stock)):
-        x = random.randint(0, len(stock) - n)
-        value = stock[x]
-        stock[x] = stock[n]
-        stock[n] = value
+def shuffle(number_of_decks):
+    deck = [(x, y) for x in suits for y in denoms] + [(None, "joker") for _ in range(3)]
+    stock = deck * number_of_decks
+
+    stock = zip(stock, range(len(stock)))
+    stock = [Card(uid, x, y) for ((x, y), uid) in stock]
+    random.shuffle(stock)
 
     return stock
 

+ 1 - 9
frontend/keezen-frontend/src/App.js

@@ -5,20 +5,12 @@ import './css/bootstrap/bootstrap.css';
 import './css/fontawesome.css';
 import './css/regular.css';
 import './css/solid.css';
-import { Denoms, Suits } from "./Card";
-import Hand from "./Hand";
+import "./css/pawn.css"
 import StateRouter from "./StateRouter";
 
 function App() {
     return <div className="App">
         <StateRouter/>
-        {/*<Hand cards={[*/}
-        {/*    { suit: Suits.DIAMONDS, denom: Denoms.ACE },*/}
-        {/*    { suit: Suits.CLUBS, denom: Denoms.D8 },*/}
-        {/*    { suit: Suits.CLUBS, denom: Denoms.D8 },*/}
-        {/*    { suit: Suits.CLUBS, denom: Denoms.D8 },*/}
-        {/*    { suit: Suits.CLUBS, denom: Denoms.D8 },*/}
-        {/*]}/>*/}
     </div>
 }
 

+ 0 - 1
frontend/keezen-frontend/src/Card.js

@@ -1,7 +1,6 @@
 import React from 'react';
 
 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">

+ 61 - 20
frontend/keezen-frontend/src/Game.js

@@ -3,10 +3,18 @@ import Hand from "./Hand";
 import { SiteState } from "./StateRouter";
 import { Card } from "./Card";
 import { colorToText } from "./util/colors";
+import "./css/Game.css"
+import Pawn from "./Pawn";
+import { useDebounce } from "./util/useDebounce";
+import deck from "./img/deck.svg";
 
-export default function Game({ message, swapCard, playCard, confirmPlay, undoPlay, skipTurn }) {
-    console.log(message);
-    const { hand, state, current_player, play_card, swap_card } = message;
+export default function Game({ message, swapCard, playCard, confirmPlay, undoPlay, skipTurn, deal, setName }) {
+    const { color, name, hand, state, current_player, play_card, swap_card } = message;
+
+    const scheduleSetName = useDebounce(setName, 500)
+
+    const currentPlayerName = current_player.name && current_player.name !== "" ?
+        current_player.name : colorToText(current_player.color);
 
     let play = null;
     let card = null;
@@ -31,28 +39,61 @@ export default function Game({ message, swapCard, playCard, confirmPlay, undoPla
             break;
         case SiteState.PLAY_CARD_OTHER:
             card = play_card;
-            text = `${colorToText(current_player.color)} is aan de beurt`;
+            text = `${currentPlayerName} is aan de beurt`;
+            break;
+        case SiteState.DEAL:
+            text = "Klik op de stapel om te delen";
+            break;
+        case SiteState.DEAL_OTHER:
+            text = `Wachten tot ${currentPlayerName} heeft gedeeld`;
             break;
     }
 
 
     return <Fragment>
-        <div className="top-bar">{text}</div>
-            <div className="container">
-                {card &&
-                <Card value={card}/>
-                }
-                {card && play_card && state === SiteState.PLAY_CARD &&
-                <button className="btn btn-success" onClick={confirmPlay}>Volgende speler</button>
-                }
-                {card && (state === SiteState.PLAY_CARD || state === SiteState.SWAP_CARD) &&
-                <button className="btn btn-secondary" onClick={undoPlay}>Neem terug</button>
-                }
-                <br/>
-                {state === SiteState.PLAY_CARD &&
-                <button className="btn btn-primary" onClick={skipTurn}>Pas</button>
-                }
+        <div className="top-bar">
+            <div className="row">
+                <div className="col-2"/>
+                <div className="col-8">{text}</div>
+                <div className="col-2">
+                    <Pawn color={color} isSelected={true} isMine={true} isSelectable={false} size="medium"/>
+                    <form className="status-name">
+                        <input className="form-control"
+                               type="text"
+                               defaultValue={name}
+                               placeholder="Vul een naam in"
+                               onChange={(e) => {
+                                   e.preventDefault();
+                                   scheduleSetName(e.target.value);
+                               }}/>
+                    </form>
+                </div>
+            </div>
+        </div>
+        <div className="container">
+            {card &&
+            <Card value={card}/>
+            }
+            {card && play_card && state === SiteState.PLAY_CARD &&
+            <button className="btn btn-success" onClick={confirmPlay}>Volgende speler</button>
+            }
+            {card && (state === SiteState.PLAY_CARD || state === SiteState.SWAP_CARD) &&
+            <button className="btn btn-secondary" onClick={undoPlay}>Neem terug</button>
+            }
+            <br/>
+            {state === SiteState.PLAY_CARD &&
+            <button className="btn btn-primary" onClick={skipTurn}>Pas</button>
+            }
+        </div>
+        {(state !== SiteState.DEAL && state !== SiteState.DEAL_OTHER) &&
+            <Hand cards={hand} play={play}/>
+        }
+        {state === SiteState.DEAL &&
+            <div className="row justify-content-center flex-md-column my-2">
+                <div className="btn btn-link" onClick={deal}>
+                    <img src={deck} alt="kaartenstapel"/><br/><span>Delen</span>
+                </div>
             </div>
-        <Hand cards={hand} play={play}/>
+        }
     </Fragment>;
 }

+ 13 - 4
frontend/keezen-frontend/src/Hand.js

@@ -4,15 +4,24 @@ import { Card } from "./Card";
 export default function Hand({ cards, play }) {
     const [selected, setSelected] = useState(null);
 
+    const deselectAndPlay = (c) => {
+        if (play !== null) {
+            if (selected === c.uid) {
+                setSelected(null)
+            }
+            play(c)
+        }
+    }
+
     return <div className="bottom-bar hand">
         <div className="container">
             <div className="row m-auto">
-                {cards.map((c, i) => <div className="col-2">
+                {cards.map((c) => <div key={c.uid} 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>)}
+                          play={() => deselectAndPlay(c)}
+                          select={selected === c.uid ? () => setSelected(null) : () => setSelected(c.uid)}
+                          selected={selected === c.uid}/></div>)}
             </div>
         </div>
     </div>

+ 12 - 25
frontend/keezen-frontend/src/Lobby.js

@@ -1,24 +1,15 @@
 import React, { Fragment, useRef } from "react";
-import "./css/lobby.css"
 import Colors from "./util/colors";
 import deck from './img/deck.svg';
 import { Commands } from "./StateRouter";
+import Pawn from "./Pawn";
+import { useDebounce } from "./util/useDebounce";
 
 export default function Lobby({ message, pickColor, deal, setName }) {
     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 timeoutCode = useRef(null);
-
-    const scheduleSetName = (name) => {
-        if (timeoutCode.current !== null) {
-            window.clearTimeout(timeoutCode.current);
-        }
-        timeoutCode.current = window.setTimeout(() => {
-            timeoutCode.current = null;
-            setName(name);
-        }, 500);
-    }
+    const scheduleSetName = useDebounce(setName, 500)
 
     const circle_args = { state, myColor: color, myName: name, setMyName: scheduleSetName, others, pickableColors, pickColor };
     const canDeal = options.find((o) => o.code === Commands.DEAL);
@@ -31,12 +22,12 @@ export default function Lobby({ message, pickColor, deal, setName }) {
         <div className="content">
             <div className="container">
                 <div className="row justify-content-center my-2">
-                    <Pawn color={Colors.RED} {...circle_args}/>
-                    <Pawn color={Colors.GREEN} {...circle_args}/>
+                    <PawnWrapper color={Colors.RED} {...circle_args}/>
+                    <PawnWrapper color={Colors.GREEN} {...circle_args}/>
                 </div>
                 <div className="row justify-content-center my2">
-                    <Pawn color={Colors.BLUE} {...circle_args}/>
-                    <Pawn color={Colors.YELLOW} {...circle_args}/>
+                    <PawnWrapper color={Colors.BLUE} {...circle_args}/>
+                    <PawnWrapper color={Colors.YELLOW} {...circle_args}/>
                 </div>
             </div>
         </div>
@@ -53,21 +44,16 @@ export default function Lobby({ message, pickColor, deal, setName }) {
     </Fragment>
 }
 
-function Pawn({ color, myColor, myName, setMyName, others, pickableColors, pickColor }) {
+function PawnWrapper({ color, myColor, myName, setMyName, others, pickableColors, pickColor }) {
     const isMine = color === myColor;
     const playerInfo = isMine ?
         { color, name: myName } :
         others.find((o) => o.color === color);
     const isSelectable = pickableColors.includes(color);
-    const isPicked = !isSelectable;
+    const isSelected = !isSelectable;
     const name = (playerInfo && playerInfo.name) || "\u00a0";
     return <div className="col col-lg-3 col-md-4">
-        <div className={`color-pick 
-                ${color} ${isPicked ? "selected" : ""} 
-                ${isSelectable ? "selectable" : ""} 
-                ${isMine ? "mine" : ""}`}
-             onClick={() => isSelectable && pickColor(color)}>
-        </div>
+        <Pawn onClick={pickColor} size="large" {...{color, isMine, isSelectable, isSelected}} />
         {isMine ?
             <form className="color-pick-name">
                 <input className="form-control"
@@ -79,6 +65,7 @@ function Pawn({ color, myColor, myName, setMyName, others, pickableColors, pickC
                            setMyName(e.target.value);
                        }}/>
             </form> :
-            <div className="color-pick-name">{name}</div>}
+            <div className="color-pick-name">{name}</div>
+        }
     </div>
 }

+ 12 - 0
frontend/keezen-frontend/src/Pawn.js

@@ -0,0 +1,12 @@
+import React from "react";
+
+export default function Pawn({color, isSelected, isSelectable, isMine, onClick, size}) {
+    return <div className={`pawn
+                ${size} 
+                ${color} 
+                ${isSelected ? "selected" : ""} 
+                ${isSelectable ? "selectable" : ""} 
+                ${isMine ? "mine" : ""}`}
+             onClick={() => isSelectable && onClick(color)}>
+        </div>
+}

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

@@ -47,7 +47,7 @@ export default function StateRouter() {
         null :
         parseInt(path.split("/")[1]);
 
-    console.log({path, path_code});
+    console.log({ path, path_code });
     const initial_state = path_code === null ?
         { state: SiteState.START } :
         { state: SiteState.JOIN_LINK, game_code: path_code };
@@ -133,17 +133,19 @@ export default function StateRouter() {
                           })}
                           deal={() => send({
                               code: Commands.DEAL,
-                              text: "Deel", 
+                              text: "Deel",
                           })}
-            setName={(name) => send({
-                code: Commands.CHANGE_NAME,
-                text: "Verander naam",
-                user_name: name
-            })}/>
+                          setName={(name) => send({
+                              code: Commands.CHANGE_NAME,
+                              text: "Verander naam",
+                              user_name: name
+                          })}/>
         case SiteState.SWAP_CARD:
         case SiteState.SWAP_CARD_OTHERS:
         case SiteState.PLAY_CARD:
         case SiteState.PLAY_CARD_OTHER:
+        case SiteState.DEAL:
+        case SiteState.DEAL_OTHER:
             return <Game message={message}
                          swapCard={(card) => send({
                              code: Commands.SWAP_CARD,
@@ -164,21 +166,18 @@ export default function StateRouter() {
                              text: "Terug/Neem kaart terug",
                          })}
                          skipTurn={() => send({
-                            code: Commands.SKIP_TURN,
-                            text: "Pas",
-                        })}/>
-        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 {colorToText(args.current_player.color)} heeft gedeeld</h1>
+                             code: Commands.SKIP_TURN,
+                             text: "Pas",
+                         })}
+                         deal={() => send({
+                             code: Commands.DEAL,
+                             text: "Delen",
+                         })}
+                         setName={(name) => send({
+                             code: Commands.CHANGE_NAME,
+                             text: "Verander naam",
+                             user_name: name
+                         })}/>
         default:
             return `state: ${state}, wsstatus: ${websocketStatus}`
     }

+ 4 - 10
frontend/keezen-frontend/src/css/App.css

@@ -44,10 +44,10 @@
 }
 
 .top-bar {
-  /*position: fixed;*/
-  top: 0;
   width: 100%;
-  /*height: 100px;*/
+  border-bottom: #1d2124 2px solid;
+  padding-top: 20px;
+  font-size: min(min(4.5vh, 2.5vw), 30pt);
 }
 
 .content {
@@ -60,10 +60,4 @@
   bottom: 0;
   width: 100%;
   height: 120px;
-}
-
-.top-bar {
-  border-bottom: #1d2124 2px solid;
-  padding-top: 20px;
-  font-size: min(min(4.5vh, 2.5vw), 30pt);
-}
+}

+ 13 - 0
frontend/keezen-frontend/src/css/Game.css

@@ -0,0 +1,13 @@
+.status-name input {
+    font-size: min(2vh, 10pt);
+    text-align: center;
+    border: none;
+    border-bottom: transparent solid;
+    border-radius: 0;
+    margin-bottom: 5px;
+}
+
+.status-name input:focus {
+    box-shadow: none;
+    border-bottom: lightgray solid;
+}

+ 29 - 12
frontend/keezen-frontend/src/css/lobby.css → frontend/keezen-frontend/src/css/pawn.css

@@ -1,41 +1,58 @@
-.color-pick {
-    font-family: "Suits";
+.pawn.large {
     font-size: min(20vh, 70pt);
     padding: min(4vh, 20px);
+}
+
+.pawn.medium {
+    font-size: min(15vh, 40pt);
+    padding: min(2vh, 10px);
+}
+
+.pawn.small {
+    font-size: min(7vh, 25pt);
+    padding: min(1.5vh, 3px);
+}
+
+.pawn {
+    font-family: "Suits";
     margin:auto;
     line-height: 1;
     opacity: 0.5;
 }
 
-.color-pick:before {
+.pawn:before {
     content: "p";
 }
 
-.color-pick.mine,
-.color-pick.selectable:hover {
+.pawn.mine,
+.pawn.selectable:hover {
     opacity: 1;
 }
 
-.color-pick.mine:before,
-.color-pick.selected:before,
-.color-pick.selectable:hover:before {
+.pawn.selectable:hover {
+    cursor: pointer;
+}
+
+.pawn.mine:before,
+.pawn.selected:before,
+.pawn.selectable:hover:before {
     content: "P";
 }
 
-.color-pick.red {
+.pawn.red {
     color: #ff1d1d;
 }
 
-.color-pick.blue {
+.pawn.blue {
     color: #2b46e5;
 }
 
-.color-pick.yellow {
+.pawn.yellow {
     color: #ffff00;
 }
 
 
-.color-pick.green {
+.pawn.green {
     color: #0ce20a;
 }
 

+ 17 - 0
frontend/keezen-frontend/src/util/useDebounce.js

@@ -0,0 +1,17 @@
+import { useRef } from "react";
+
+export function useDebounce(callback, timeout) {
+    const timeoutCode = useRef(null);
+
+    function setTimeout(...args) {
+        if (timeoutCode.current !== null) {
+            window.clearTimeout(timeoutCode.current);
+        }
+
+        timeoutCode.current = window.setTimeout(() => {
+            timeoutCode.current = null;
+            callback(...args);
+        }, timeout);
+    }
+    return setTimeout
+}

+ 1 - 1
option.py

@@ -23,7 +23,7 @@ class Option(object):
         self.text = text
         self.game_code = int(game_code) if game_code != None else None
         self.color = Color(color) if color != None else None
-        self.card = card if isinstance(card, Card) else Card(card['suit'], card['denom']) if isinstance(card, dict) else None
+        self.card = card if isinstance(card, Card) else Card(card['uid'], card['suit'], card['denom']) if isinstance(card, dict) else None
         self.user_name = user_name
 
     def isOption(self, other):