From 39de15a7ab1238eb988d8d7cead88f8b5e131c29 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 23 Sep 2025 19:08:23 -0700 Subject: [PATCH] Trying to get it functional again --- Dockerfile.dev | 8 + Dockerfile.server | 1 + client/package.json | 2 +- client/src/Actions.js | 257 ---------- client/src/Activities.js | 229 --------- client/src/App.js | 567 ---------------------- client/src/App.test.js | 8 - client/src/App.tsx | 2 +- client/src/Bird.js | 88 ---- client/src/Board.js | 867 ---------------------------------- client/src/BoardPieces.js | 51 -- client/src/Chat.js | 202 -------- client/src/ChooseCard.js | 142 ------ client/src/Dice.js | 23 - client/src/GameOrder.js | 108 ----- client/src/Hand.js | 178 ------- client/src/HouseRules.js | 426 ----------------- client/src/MediaControl.js | 711 ---------------------------- client/src/PingPong.js | 41 -- client/src/Placard.js | 96 ---- client/src/PlayerColor.js | 17 - client/src/PlayerColor.tsx | 5 +- client/src/PlayerList.js | 177 ------- client/src/PlayerName.js | 37 -- client/src/PlayersStatus.js | 211 --------- client/src/Resource.js | 45 -- client/src/SelectPlayer.js | 84 ---- client/src/Sheep.js | 108 ----- client/src/Styles.ts | 24 +- client/src/Trade.js | 527 --------------------- client/src/ViewCard.js | 193 -------- client/src/Winner.js | 199 -------- client/src/index.js | 21 - client/src/index.tsx | 5 +- docker-compose.client.dev.yml | 1 + docker-compose.dev.yml | 4 +- launch.sh | 12 + 37 files changed, 41 insertions(+), 5636 deletions(-) create mode 100644 Dockerfile.dev delete mode 100644 client/src/Actions.js delete mode 100644 client/src/Activities.js delete mode 100755 client/src/App.js delete mode 100644 client/src/App.test.js delete mode 100644 client/src/Bird.js delete mode 100644 client/src/Board.js delete mode 100644 client/src/BoardPieces.js delete mode 100644 client/src/Chat.js delete mode 100644 client/src/ChooseCard.js delete mode 100644 client/src/Dice.js delete mode 100644 client/src/GameOrder.js delete mode 100644 client/src/Hand.js delete mode 100644 client/src/HouseRules.js delete mode 100644 client/src/MediaControl.js delete mode 100644 client/src/PingPong.js delete mode 100644 client/src/Placard.js delete mode 100644 client/src/PlayerColor.js delete mode 100644 client/src/PlayerList.js delete mode 100644 client/src/PlayerName.js delete mode 100644 client/src/PlayersStatus.js delete mode 100644 client/src/Resource.js delete mode 100644 client/src/SelectPlayer.js delete mode 100644 client/src/Sheep.js delete mode 100644 client/src/Trade.js delete mode 100644 client/src/ViewCard.js delete mode 100644 client/src/Winner.js delete mode 100644 client/src/index.js create mode 100755 launch.sh diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..eca3c36 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM node:20-alpine + +WORKDIR /server + +# For dev, we install in container, but to speed up, perhaps copy package and install +# But since volumes mount, just run npm install in command + +CMD ["sh", "-c", "cd /server && npm install --no-audit --no-fund --silent && npm rebuild sqlite3 && npm run start:dev"] \ No newline at end of file diff --git a/Dockerfile.server b/Dockerfile.server index d701ffa..7d3341a 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -18,6 +18,7 @@ WORKDIR / # Copy built server COPY --from=builder /server/dist ./server/dist +COPY --from=builder /server/node_modules ./server/node_modules COPY server/package*.json /server/ WORKDIR /server diff --git a/client/package.json b/client/package.json index 6d25933..554a6ba 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,7 @@ "name": "peddlers-client", "version": "1.0.0", "private": true, - "proxy": "http://peddlers-of-ketran:8930", + "proxy": "http://peddlers-server:8930", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/client/src/Actions.js b/client/src/Actions.js deleted file mode 100644 index 19b96ec..0000000 --- a/client/src/Actions.js +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useState, useEffect, useContext, useRef, useMemo, useCallback } from "react"; -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; -import equal from "fast-deep-equal"; - -import "./Actions.css"; -import { PlayerName } from './PlayerName.js'; -import { GlobalContext } from "./GlobalContext.js"; - -const Actions = ({ - tradeActive, setTradeActive, - buildActive, setBuildActive, - houseRulesActive, setHouseRulesActive -}) => { - const { ws, gameId, name } = useContext(GlobalContext); - const [state, setState] = useState('lobby'); - const [color, setColor] = useState(undefined); - const [priv, setPriv] = useState(undefined); - const [turn, setTurn] = useState({}); - const [edit, setEdit] = useState(name); - const [active, setActive] = useState(0); - const [players, setPlayers] = useState({}); - const [alive, setAlive] = useState(0); - - const fields = useMemo(() => [ - 'state', 'turn', 'private', 'active', 'color', 'players' - ], []); - - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`actions - game update`, data.update); - if ('private' in data.update && !equal(data.update.private, priv)) { - setPriv(data.update.private); - } - if ('state' in data.update && data.update.state !== state) { - setState(data.update.state); - } - if ('color' in data.update && data.update.color !== color) { - setColor(data.update.color); - } - if ('name' in data.update && data.update.name !== edit) { - setEdit(data.update.name); - } - if ('turn' in data.update && !equal(data.update.turn, turn)) { - setTurn(data.update.turn); - } - if ('active' in data.update && data.update.active !== active) { - setActive(data.update.active); - } - - if ('players' in data.update && !equal(data.update.players, players)) { - setPlayers(data.update.players); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { - ws.removeEventListener('message', cbMessage); - } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - const sendMessage = useCallback((data) => { - if (!ws) { - console.warn(`No socket`); - } else { - ws.send(JSON.stringify(data)); - } - }, [ws]); - - const buildClicked = () => { - setBuildActive(!buildActive); - }; - - useEffect(() => { - let count = 0; - for (let key in players) { - if (players[key].live) { - count++; - } - } - setAlive(count); - }, [players, setAlive]); - - const setName = (update) => { - if (update !== name) { - sendMessage({ type: 'player-name', name: update }); - } - setEdit(name); - if (buildActive) setBuildActive(false); - } - - const changeNameClick = () => { - setEdit(""); - if (buildActive) setBuildActive(false); - } - - const discardClick = () => { - const nodes = document.querySelectorAll('.Hand .Resource.Selected'), - discards = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 }; - - for (let i = 0; i < nodes.length; i++) { - discards[nodes[i].getAttribute("data-type")]++; - nodes[i].classList.remove('Selected'); - } - - sendMessage({ type: 'discard', discards }); - if (buildActive) setBuildActive(false); - } - - const newTableClick = () => { - sendMessage({ type: 'shuffle' }); - if (buildActive) setBuildActive(false); - }; - - const tradeClick = () => { - if (!tradeActive) { - setTradeActive(true); - sendMessage({ type: 'trade' }); - } else { - setTradeActive(false); - sendMessage({ type: 'trade', action: 'cancel', offer: undefined }); - } - if (buildActive) setBuildActive(false); - } - - const rollClick = () => { - sendMessage({ type: 'roll' }); - if (buildActive) setBuildActive(false); - } - - const passClick = () => { - sendMessage({ type: 'pass' }); - if (buildActive) setBuildActive(false); - } - - const houseRulesClick = () => { - /* sendMessage({ type: 'house-rules' }); */ - setHouseRulesActive(!houseRulesActive); - } - - const startClick = () => { - sendMessage({ - type: 'set', - field: 'state', - value: 'game-order' - }); - if (buildActive) setBuildActive(false); - }; - - const resetGame = () => { - sendMessage({ - type: 'clear-game' - }); - if (buildActive) setBuildActive(false); - }; - - if (!gameId) { - return (); - } - - const inLobby = state === 'lobby', - inGame = state === 'normal', - inGameOrder = state === 'game-order', - hasGameOrderRolled = (priv && priv.orderRoll) ? true : false, - hasRolled = (turn && turn.roll) ? true : false, - isTurn = (turn && turn.color === color) ? true : false, - robberActions = (turn && turn.robberInAction), - haveResources = priv ? priv.resources !== 0 : false, - volcanoActive = state === 'volcano', - placement = (state === 'initial-placement' || (turn && turn.active === 'road-building')), - placeRoad = placement && turn && turn.actions && - (turn.actions.indexOf('place-road') !== -1 - || turn.actions.indexOf('place-city') !== -1 - || turn.actions.indexOf('place-settlement') !== -1); - - if (tradeActive - && (!turn || !turn.actions || turn.actions.indexOf('trade'))) { - setTradeActive(false); - } else if (!tradeActive - && turn && turn.actions && turn.actions.indexOf('trade') !== -1) { - setTradeActive(true); - } - - let disableRoll = false; - if (robberActions) { disableRoll = true; } - if (turn && turn.select) { disableRoll = true; } - if (inGame && !isTurn) { disableRoll = true; } - if (inGame && hasRolled) { disableRoll = true; } - if (volcanoActive && (!isTurn || hasRolled)) { disableRoll = true; } - if (volcanoActive && isTurn && turn && !turn.select) { disableRoll = false; } - if (inGameOrder && hasGameOrderRolled) { disableRoll = true; } - if (placement) { disableRoll = true; } - - console.log(`actions - `, { - disableRoll, robberActions, turn, inGame, isTurn, hasRolled, volcanoActive, inGameOrder, - hasGameOrderRolled}); - - const disableDone = - volcanoActive || - placeRoad || - robberActions || - !isTurn || - !hasRolled; - - return ( - - { edit === "" && } -
- { name && alive === 1 && } - { name && inLobby && <> - - - } - { name && !color && } - {name && color && inLobby && } - { name && !inLobby && <> - - - - - - - - {name && color && } - - } - { /* inLobby && - - */ } -
-
- ); -} - -export { Actions }; - diff --git a/client/src/Activities.js b/client/src/Activities.js deleted file mode 100644 index 77aaf26..0000000 --- a/client/src/Activities.js +++ /dev/null @@ -1,229 +0,0 @@ -import React, { useState, useContext, useMemo, useEffect, useRef } from "react"; -import equal from "fast-deep-equal"; -import "./Activities.css"; -import { PlayerColor } from './PlayerColor.js'; -import { Dice } from './Dice.js'; -import { GlobalContext } from "./GlobalContext.js"; - -const Activity = ({ keep, activity }) => { - const [animation, setAnimation] = useState('open'); - const [display, setDisplay] = useState(true) - - const hide = async (ms) => { - await new Promise(r => setTimeout(r, ms)); - setAnimation('close') - await new Promise(r => setTimeout(r, 1000)); - setDisplay(false) - }; - - if (display && !keep) { - setTimeout(() => { hide(10000) }, 0); - } - - let message; - /* If the date is in the future, set it to now */ - const dice = activity.message.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/); - if (dice) { - if (dice[4]) { - const sum = parseInt(dice[2]) + parseInt(dice[4]); - message = <>{dice[1]}{sum}: , {dice[5]}; - } else { - message = <>{dice[1]}{dice[5]}; - } - } else { - message = activity.message; - } - - return <>{ display && -
- {message} -
- }; -} - -const Activities = () => { - const { ws } = useContext(GlobalContext); - const [activities, setActivities] = useState([]); - const [turn, setTurn] = useState(); - const [color, setColor] = useState(); - const [players, setPlayers] = useState({}); - const [timestamp, setTimestamp] = useState(0); - const [state, setState] = useState(''); - - const fields = useMemo(() => [ - 'activities', 'turn', 'players', 'timestamp', 'color', - 'state' - ], []); - const requestUpdate = (fields) => { - let request; - if (!Array.isArray(fields)) { - request = [ fields ]; - } else { - request = fields; - } - ws.send(JSON.stringify({ - type: 'get', - fields: request - })); - }; - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - const ignoring = [], processing = []; - for (let field in data.update) { - if (fields.indexOf(field) === -1) { - ignoring.push(field); - } else { - processing.push(field); - } - } - console.log(`activities - game update`, data.update); - console.log(`activities - ignoring ${ignoring.join(',')}`); - console.log(`activities - processing ${processing.join(',')}`); - - if ('state' in data.update - && data.update.state !== state) { - requestUpdate('turn'); - setState(data.update.state); - } - if ('activities' in data.update - && !equal(data.update.activities, activities)) { - setActivities(data.update.activities); - } - if ('turn' in data.update - && !equal(data.update.turn, turn)) { - setTurn(data.update.turn); - } - if ('players' in data.update - && !equal(data.update.players, players)) { - setPlayers(data.update.players); - } - if ('timestamp' in data.update - && data.update.timestamp !== timestamp) { - setTimestamp(data.update.timestamp); - } - if ('color' in data.update && data.update.color !== color) { - setColor(data.update.color); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { - ws.removeEventListener('message', cbMessage); - } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - if (!timestamp) { - return <>; - } - - const - isTurn = (turn && turn.color === color) ? true : false, - normalPlay = ([ 'initial-placement', 'normal', 'volcano' ].indexOf(state) !== -1), - mustPlaceRobber = (turn && !turn.placedRobber && turn.robberInAction), - placement = (state === 'initial-placement' || (turn && turn.active === 'road-building')), - placeRoad = placement && turn && turn.actions && turn.actions.indexOf('place-road') !== -1, - mustStealResource = turn && turn.actions && turn.actions.indexOf('steal-resource') !== -1, - rollForVolcano = (state === 'volcano' && turn && !turn.select), - rollForOrder = (state === 'game-order'), - selectResources = turn && turn.actions && turn.actions.indexOf('select-resources') !== -1; - - console.log(`activities - `, state, turn, activities); - - let discarders = [], mustDiscard = false; - for (let key in players) { - const player = players[key]; - if (!player.mustDiscard) { - continue; - } - mustDiscard = true; - const name = (color === key) ? 'You' : player.name; - discarders.push(
{name} must discard {player.mustDiscard} cards.
); - } - - let list = activities - .filter((activity, index) => - activities.length - 1 === index || timestamp - activity.date < 11000); - list = list.map((activity, index) => { - return ; - }); - - let who; - if (turn && turn.select) { - const selecting = []; - for (let key in turn.select) { - selecting.push({ - color: key, - name: color === key ? 'You' : players[key].name}); - } - who = selecting.map((player, index) => -
{ player.name }{ index !== selecting.length - 1 ? ', ' : '' }
); - } else { - if (isTurn) { - who = 'You'; - } else { - if (!turn || !turn.name) { - who = 'Everyone'; - } else { - who = <> {turn.name} - } - } - } - - return ( -
- { list } - { normalPlay && !mustDiscard && mustPlaceRobber && -
{who} must move the Robber.
- } - - { placement && -
{who} must place a {placeRoad ? 'road' : 'settlement'}.
- } - - { mustStealResource && -
{who} must select a player to steal from.
- } - - { rollForOrder && -
{who} must roll for game order.
- } - - { rollForVolcano && -
{who} must roll for Volcano devastation!
- } - - { selectResources && -
{who} must select resources!
- } - - { normalPlay && mustDiscard && <> { discarders } } - - { !isTurn && normalPlay && turn && -
It is {turn.name}'s turn.
- } - - { isTurn && normalPlay && turn && -
It is your turn.
- } -
- ); -}; - -export {Activities}; \ No newline at end of file diff --git a/client/src/App.js b/client/src/App.js deleted file mode 100755 index 9c7c97d..0000000 --- a/client/src/App.js +++ /dev/null @@ -1,567 +0,0 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; -import { - BrowserRouter as Router, - Route, - Routes, - useParams -} from "react-router-dom"; - -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; - -import { GlobalContext } from "./GlobalContext.js"; -//import { PingPong } from "./PingPong.js"; -import { PlayerList } from "./PlayerList.js"; -import { Chat } from "./Chat.js"; -import { Board } from "./Board.js"; -import { Actions } from "./Actions.js"; -import { base, gamesPath } from './Common.js'; -import { GameOrder } from "./GameOrder.js"; -import { Activities } from "./Activities.js"; -import { SelectPlayer } from "./SelectPlayer.js"; -import { PlayersStatus } from "./PlayersStatus.js"; -import { ViewCard } from "./ViewCard.js"; -import { ChooseCard } from "./ChooseCard.js"; -import { Hand } from "./Hand.js"; -import { Trade } from "./Trade.js"; -import { Winner } from "./Winner.js"; -import { HouseRules } from "./HouseRules.js"; -import { Dice } from "./Dice.js"; -import { assetsPath } from "./Common.js"; -//import { Sheep } from "./Sheep.js"; - -import history from "./history.js"; -import "./App.css"; -import equal from "fast-deep-equal"; - -/* -const Pip = () => { -
-} -*/ - -let audioEffects = { -}; - -const loadAudio = (src) => { - const audio = document.createElement("audio"); - audio.src = `${assetsPath}/${src}`; - audio.setAttribute("preload", "auto"); - audio.setAttribute("controls", "none"); - audio.style.display = "none"; - document.body.appendChild(audio); - audio.play(); - audio.hasPlayed = true; - return audio; -} - -const Table = () => { - const params = useParams(); - const [ gameId, setGameId ] = useState(params.gameId ? params.gameId : undefined); - const [ ws, setWs ] = useState(); /* tracks full websocket lifetime */ - const [connection, setConnection] = useState(undefined); /* set after ws is in OPEN */ - const [retryConnection, setRetryConnection] = useState(true); /* set when connection should be re-established */ - const [ name, setName ] = useState(""); - const [ error, setError ] = useState(undefined); - const [ warning, setWarning ] = useState(undefined); - const [loaded, setLoaded] = useState(false); - - const [dice, setDice] = useState(undefined); - const [state, setState] = useState(undefined); - const [color, setColor] = useState(undefined); - const [priv, setPriv] = useState(undefined); - const [turn, setTurn] = useState(undefined); - const [buildActive, setBuildActive] = useState(false); - const [tradeActive, setTradeActive] = useState(false); - const [cardActive, setCardActive] = useState(undefined); - const [houseRulesActive, setHouseRulesActive] = useState(undefined); - const [winnerDismissed, setWinnerDismissed] = useState(undefined); - const [global, setGlobal] = useState({}); - const [count, setCount] = useState(0); - const [audio, setAudio] = useState( - localStorage.getItem('audio') ? - localStorage.getItem('audio') : false); - const [animations, setAnimations] = useState( - localStorage.getItem('animations') || false); - const [volume, setVolume] = useState( - localStorage.getItem('volume') ? - parseFloat(localStorage.getItem('volume')) : 0.5); - const fields = [ 'id', 'state', 'color', 'name', 'private', 'dice', 'turn' ]; - - const onWsOpen = (event) => { - console.log(`ws: open`); - setError(""); - - /* We do not set the socket as connected until the 'open' message - * comes through */ - setConnection(ws); - - /* Request a full game-update - * We only need gameId and name for App.js, however in the event - * of a network disconnect, we need to refresh the entire game - * state on reload so all bound components reflect the latest - * state */ - event.target.send(JSON.stringify({ - type: 'game-update' - })); - event.target.send(JSON.stringify({ - type: 'get', - fields - })); - }; - - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'error': - console.error(`App - error`, data.error); - setError(data.error); - break; - case 'warning': - console.warn(`App - warning`, data.warning); - setWarning(data.warning); - setTimeout(() => { - console.log(`todo: stack warnings in a window and have them disappear one at a time.`); - console.log(`app - clearing warning`); - setWarning(""); - }, 3000); - break; - case 'game-update': - if (!loaded) { - setLoaded(true); - } - console.log(`app - message - ${data.type}`, data.update); - - if ('private' in data.update && !equal(priv, data.update.private)) { - const priv = data.update.private; - if (priv.name !== name) { - console.log(`App - setting name (via private): ${priv.name}`); - setName(priv.name); - } - if (priv.color !== color) { - console.log(`App - setting color (via private): ${priv.color}`); - setColor(priv.color); - } - setPriv(priv); - } - - if ('name' in data.update) { - if (data.update.name) { - console.log(`App - setting name: ${data.update.name}`); - setName(data.update.name); - } else { - setWarning(""); - setError(""); - setPriv(undefined); - } - } - if ('id' in data.update && data.update.id !== gameId) { - console.log(`App - setting gameId ${data.update.id}`); - setGameId(data.update.id); - } - if ('state' in data.update && data.update.state !== state) { - console.log(`App - setting game state: ${data.update.state}`); - if (data.update.state !== 'winner' && winnerDismissed) { - setWinnerDismissed(false); - } - setState(data.update.state); - } - if ('dice' in data.update && !equal(data.update.dice, dice)) { - setDice(data.update.dice); - } - if ('turn' in data.update && !equal(data.update.turn, turn)) { - setTurn(data.update.turn); - } - if ('color' in data.update && data.update.color !== color) { - console.log(`App - setting color: ${color}`); - setColor(data.update.color); - } - break; - default: - break; - } - }; - - const sendUpdate = (update) => { - ws.send(JSON.stringify(update)); - }; - - const cbResetConnection = useCallback(() => { - let timer = 0; - function reset() { - timer = 0; - setRetryConnection(true); - }; - return _ => { - if (timer) { - clearTimeout(timer); - } - timer = setTimeout(reset, 5000); - }; - }, [setRetryConnection]); - - const resetConnection = cbResetConnection(); - - if (global.ws !== connection - || global.name !== name - || global.gameId !== gameId) { - console.log(`board - (app) - setting global`, global, { - ws: connection, - name, - gameId - }); - setGlobal({ - ws: connection, - name, - gameId - }); - } - - const onWsError = (event) => { - console.error(`ws: error`, event); - const error = `Connection to Ketr Ketran game server failed! ` + - `Connection attempt will be retried every 5 seconds.`; - setError(error); - setGlobal(Object.assign({}, global, { ws: undefined })); - setWs(undefined); /* clear the socket */ - setConnection(undefined); /* clear the connection */ - resetConnection(); - }; - - const onWsClose = (event) => { - const error = `Connection to Ketr Ketran game was lost. ` + - `Attempting to reconnect...`; - console.warn(`ws: close`); - setError(error); - setGlobal(Object.assign({}, global, { ws: undefined })); - setWs(undefined); /* clear the socket */ - setConnection(undefined); /* clear the connection */ - resetConnection(); - }; - - /* callback refs are used to provide correct state reference - * in the callback handlers, while also preventing rebinding - * of event handlers on every render */ - const refWsOpen = useRef(onWsOpen); - useEffect(() => { refWsOpen.current = onWsOpen; }); - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - const refWsClose = useRef(onWsClose); - useEffect(() => { refWsClose.current = onWsClose; }); - const refWsError = useRef(onWsError); - useEffect(() => { refWsError.current = onWsError; }); - - /* This effect is responsible for triggering a new game load if a - * game id is not provided in the URL. If the game is provided - * in the URL, the backend will create a new game if necessary - * during the WebSocket connection sequence. - * - * This should be the only HTTP request made from the game. - */ - useEffect(() => { - if (gameId) { - console.log(`Game in use ${gameId}`) - return; - } - - console.log(`Requesting new game.`); - - window.fetch(`${base}/api/v1/games/`, { - method: 'POST', - cache: 'no-cache', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json' - }, - }).then((res) => { - if (res.status >= 400) { - const error = `Unable to connect to Ketr Ketran game server! ` + - `Try refreshing your browser in a few seconds.`; - console.error(error); - setError(error); - throw new Error(error); - } - return res.json(); - }).then((update) => { - if (update.id !== gameId) { - console.log(`Game available: ${update.id}`); - history.push(`${gamesPath}/${update.id}`); - setGameId(update.id); - } - }).catch((error) => { - console.error(error); - }); - }, [ gameId, setGameId ]); - - /* Once a game id is known, create the sole WebSocket connection - * to the backend. This WebSocket is then shared with any component - * that performs game state updates. Those components should - * bind to the 'message:game-update' WebSocket event and parse - * their update information from those messages - */ - useEffect(() => { - if (!gameId) { - return; - } - - const unbind = () => { - console.log(`table - unbind`); - } - - console.log(`table - bind`); - - if (!ws && !connection && retryConnection) { - let loc = window.location, new_uri; - if (loc.protocol === "https:") { - new_uri = "wss"; - } else { - new_uri = "ws"; - } - new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}?${count}`; - console.log(`Attempting WebSocket connection to ${new_uri}`); - setWs(new WebSocket(new_uri)); - setConnection(undefined); - setRetryConnection(false); - setCount(count + 1); - return unbind; - } - - if (!ws) { - return unbind; - } - - const cbOpen = e => refWsOpen.current(e); - const cbMessage = e => refWsMessage.current(e); - const cbClose = e => refWsClose.current(e); - const cbError = e => refWsError.current(e); - - ws.addEventListener('open', cbOpen); - ws.addEventListener('close', cbClose); - ws.addEventListener('error', cbError); - ws.addEventListener('message', cbMessage); - - return () => { - unbind(); - ws.removeEventListener('open', cbOpen); - ws.removeEventListener('close', cbClose); - ws.removeEventListener('error', cbError); - ws.removeEventListener('message', cbMessage); - } - }, [ ws, setWs, connection, setConnection, - retryConnection, setRetryConnection, gameId, - refWsOpen, refWsMessage, refWsClose, refWsError, count, setCount - ]); - - console.log(`board - (app) - Render with ws: ${ws ? '!' : ''}NULL, connection: ${connection ? '!' : ''}NULL`); - - useEffect(() => { - if (state === 'volcano') { - if (!audioEffects.volcano) { - audioEffects.volcano = loadAudio('volcano-eruption.mp3'); - audioEffects.volcano.volume = volume * volume; - } else { - if (!audioEffects.volcano.hasPlayed) { - audioEffects.volcano.hasPlayed = true; - audioEffects.volcano.play(); - } - } - } else { - if (audioEffects.volcano) { - audioEffects.volcano.hasPlayed = false; - } - } - }, [state, volume]); - - useEffect(() => { - if (turn && turn.color === color && state !== 'lobby') { - if (!audioEffects.yourTurn) { - audioEffects.yourTurn = loadAudio('its-your-turn.mp3'); - audioEffects.yourTurn.volume = volume * volume; - } else { - if (!audioEffects.yourTurn.hasPlayed) { - audioEffects.yourTurn.hasPlayed = true; - audioEffects.yourTurn.play(); - } - } - } else if (turn) { - if (audioEffects.yourTurn) { - audioEffects.yourTurn.hasPlayed = false; - } - } - - if (turn && turn.roll === 7) { - if (!audioEffects.robber) { - audioEffects.robber = loadAudio('robber.mp3'); - audioEffects.robber.volume = volume * volume; - } else { - if (!audioEffects.robber.hasPlayed) { - audioEffects.robber.hasPlayed = true; - audioEffects.robber.play(); - } - } - } else if (turn) { - if (audioEffects.robber) { - audioEffects.robber.hasPlayed = false; - } - } - - if (turn && turn.actions && turn.actions.indexOf('playing-knight') !== -1) { - if (!audioEffects.knights) { - audioEffects.knights = loadAudio('the-knights-who-say-ni.mp3'); - audioEffects.knights.volume = volume * volume; - } else { - if (!audioEffects.knights.hasPlayed) { - audioEffects.knights.hasPlayed = true; - audioEffects.knights.play(); - } - } - } else if (turn && turn.actions && turn.actions.indexOf('playing-knight') === -1) { - if (audioEffects.knights) { - audioEffects.knights.hasPlayed = false; - } - } - }, [state, turn, color, volume]); - - useEffect(() => { - for (let key in audioEffects) { - audioEffects[key].volume = volume * volume; - } - }, [volume]); - - return - { /* */ } -
-
- - { dice && dice.length &&
- {dice.length === 1 &&
Volcano roll!
} - {dice.length === 2 &&
Current roll
} -
- - { dice.length === 2 && } -
-
} -
-
-
- { error &&
- -
{ error }
- -
-
} - - { priv && priv.turnNotice &&
- -
{ priv.turnNotice }
- -
-
} - { warning &&
- -
{ warning }
- -
-
} - { state === 'normal' && } - { color && state === 'game-order' && } - - { !winnerDismissed && } - { houseRulesActive && } - - -
- - - - - -
-
- { name !== "" && volume !== undefined && -
Audio effects
{ - const value = !audio; - localStorage.setItem('audio', value); - setAudio(value) - }}/> -
Sound effects volume
{ - const alpha = e.currentTarget.value / 100; - - localStorage.setItem('volume', alpha); - setVolume(alpha); - }}/> -
Animations
{ - const value = !animations; - localStorage.setItem('animations', value); - setAnimations(value) - }} /> -
} - { name !== "" && } - - { name !== "" && } - { /* name !== "" && */ } - { loaded && } -
-
-
; -}; - -const App = () => { - const [playerId, setPlayerId] = useState(undefined); - const [error, setError] = useState(undefined); - - useEffect(() => { - if (playerId) { - return; - } - window.fetch(`${base}/api/v1/games/`, { - method: 'GET', - cache: 'no-cache', - credentials: 'same-origin', /* include cookies */ - headers: { - 'Content-Type': 'application/json' - }, - }).then((res) => { - if (res.status >= 400) { - const error = `Unable to connect to Ketr Ketran game server! ` + - `Try refreshing your browser in a few seconds.`; - console.error(error); - setError(error); - } - console.log(res.headers); - return res.json(); - }).then((data) => { - setPlayerId(data.player); - }).catch((error) => { - }); - }, [playerId, setPlayerId]); - - if (!playerId) { - return <>{ error }; - } - - return ( - - - } path={`${base}/:gameId`}/> - } path={`${base}`}/> - - - ); -} - -export default App; diff --git a/client/src/App.test.js b/client/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/client/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/client/src/App.tsx b/client/src/App.tsx index 4f110c9..0050f9b 100755 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,7 @@ import { PlayerList } from "./PlayerList"; import { Chat } from "./Chat"; import { Board } from "./Board"; import { Actions } from "./Actions"; -import { base, gamesPath } from "./Common"; +import { base } from "./Common"; import { GameOrder } from "./GameOrder"; import { Activities } from "./Activities"; import { SelectPlayer } from "./SelectPlayer"; diff --git a/client/src/Bird.js b/client/src/Bird.js deleted file mode 100644 index 3cf0997..0000000 --- a/client/src/Bird.js +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useEffect, useState, useCallback } from "react"; -import { assetsPath } from "./Common.js"; -import "./Bird.css"; - -const - birdAngles = 12; -const frames = [0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - -const useAnimationFrame = callback => { - // Use useRef for mutable variables that we want to persist - // without triggering a re-render on their change - const requestRef = React.useRef(); - - const animate = time => { - callback(time) - requestRef.current = requestAnimationFrame(animate); - } - - React.useEffect(() => { - requestRef.current = requestAnimationFrame(animate); - return () => cancelAnimationFrame(requestRef.current); - }, []); // Make sure the effect runs only once -} - -const Bird = ({ origin, radius, speed, size, style }) => { - const [time, setTime] = useState(0); - const [angle, setAngle] = useState(Math.random() * 360.); - const [rotation] = useState(Math.PI * 2 * radius / 5); - const [direction, setDirection] = useState(Math.floor(birdAngles * (angle ? angle : 0) / 360.)); - - const [cell, setCell] = useState(0); - - const previousTimeRef = React.useRef(); - - useAnimationFrame(time => { - if (previousTimeRef.current !== undefined) { - const deltaTime = time - previousTimeRef.current; - setTime(deltaTime); - } else { - previousTimeRef.current = time; - } - }); - - useEffect(() => { - const alpha = (time % speed) / speed; - const frame = Math.floor(frames.length * alpha); - const newAngle = (angle + rotation) % 360.; - setAngle(newAngle); - setCell(frames[Math.floor(frame)]); - setDirection(Math.floor(birdAngles * newAngle / 360.)); - }, [time, setCell, speed, rotation, setDirection]); - - return
; -}; - -const Flock = ({count, style}) => { - const [birds, setBirds] = useState([]); - useEffect(() => { - const tmp = []; - for (let i = 0; i < count; i++) { - const scalar = Math.random(); - tmp.push() - } - setBirds(tmp); - }, [count, setBirds]); - - return
{ birds }
; -}; - -export { Bird, Flock }; diff --git a/client/src/Board.js b/client/src/Board.js deleted file mode 100644 index 5f2bb9e..0000000 --- a/client/src/Board.js +++ /dev/null @@ -1,867 +0,0 @@ -import React, { useEffect, useState, useContext, useRef, useMemo } from "react"; -import equal from "fast-deep-equal"; -import { assetsPath } from "./Common.js"; -import "./Board.css"; -import { GlobalContext } from "./GlobalContext.js"; -import { Flock } from "./Bird.js"; -import { Herd } from "./Sheep.js"; - -const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */ - -const - hexRatio = 1.1547, - tileWidth = 67, - tileHalfWidth = tileWidth * 0.5, - tileHeight = tileWidth * hexRatio, - tileHalfHeight = tileHeight * 0.5, - radius = tileHeight * 2, - borderOffsetX = 86, /* ~1/10th border image width... hand tuned */ - borderOffsetY = 3; - -/* Actual sizing */ -const - tileImageWidth = 90, /* Based on hand tuned and image width */ - tileImageHeight = tileImageWidth/hexRatio, - borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */ - borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */ - - -const showTooltip = () => { - document.querySelector('.Board .Tooltip').style.display = 'flex'; -}; - -const clearTooltip = () => { - document.querySelector('.Board .Tooltip').style.display = 'none'; -}; - -const Board = ({ animations }) => { - const { ws } = useContext(GlobalContext); - const board = useRef(); - const [transform, setTransform] = useState(1.); - const [pipElements, setPipElements] = useState(<>); - const [borderElements, setBorderElements] = useState(<>); - const [tileElements, setTileElements] = useState(<>); - const [cornerElements, setCornerElements] = useState(<>); - const [roadElements, setRoadElements] = useState(<>); - const [ signature, setSignature ] = useState(""); - const [ generated, setGenerated ] = useState(""); - const [ robber, setRobber ] = useState(-1); - const [ robberName, setRobberName ] = useState([]); - const [ pips, setPips ] = useState(); - const [ pipOrder, setPipOrder ] = useState(); - const [ borders, setBorders ] = useState(); - const [ borderOrder, setBorderOrder ] = useState(); - const [animationSeeds, setAnimationSeeds] = useState(); - const [ tiles, setTiles ] = useState(); - const [ tileOrder, setTileOrder ] = useState([]); - const [ placements, setPlacements ] = useState(undefined); - const [ turn, setTurn ] = useState({}); - const [ state, setState ] = useState(""); - const [ color, setColor ] = useState(""); - const [ rules, setRules ] = useState({}); - const [ longestRoadLength, setLongestRoadLength ] = useState(0); - const fields = useMemo(() => [ - 'signature', 'robber', 'robberName', - 'pips', 'pipOrder', 'borders', 'borderOrder', 'tiles', 'tileOrder', - 'placements', 'turn', 'state', 'color', 'longestRoadLength', - 'rules', 'animationSeeds' - ], []); - - console.log(`board - ws`, ws); - - const onWsMessage = (event) => { - if (ws && ws !== event.target) { - console.error(`Disconnect occur?`); - } - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`board - game update`, data.update); - if ('robber' in data.update && data.update.robber !== robber) { - setRobber(data.update.robber); - } - - if ('robberName' in data.update - && data.update.robberName !== robberName) { - setRobberName(data.update.robberName); - } - - if ('state' in data.update && data.update.state !== state) { - setState(data.update.state); - } - - if ('rules' in data.update - && !equal(data.update.rules, rules)) { - setRules(data.update.rules); - } - - if ('color' in data.update && data.update.color !== color) { - setColor(data.update.color); - } - - if ('longestRoadLength' in data.update - && data.update.longestRoadLength !== longestRoadLength) { - setLongestRoadLength(data.update.longestRoadLength); - } - - if ('turn' in data.update) { - if (!equal(data.update.turn, turn)) { - console.log(`board - turn`, data.update.turn); - setTurn(data.update.turn); - } - } - - if ('placements' in data.update && !equal(data.update.placements, placements)) { - console.log(`board - placements`, data.update.placements); - setPlacements(data.update.placements); - } - - /* The following are only updated if there is a new game - * signature */ - - if ('pipOrder' in data.update && !equal(data.update.pipOrder, pipOrder)) { - console.log(`board - setting new pipOrder`); - setPipOrder(data.update.pipOrder); - } - - if ('borderOrder' in data.update && !equal(data.update.borderOrder, borderOrder)) { - console.log(`board - setting new borderOrder`); - setBorderOrder(data.update.borderOrder); - } - - if ('animationSeeds' in data.update && !equal(data.update.animationSeeds, animationSeeds)) { - console.log(`board - setting new animationSeeds`); - setAnimationSeeds(data.update.animationSeeds); - } - - if ('tileOrder' in data.update && !equal(data.update.tileOrder, tileOrder)) { - console.log(`board - setting new tileOrder`); - setTileOrder(data.update.tileOrder); - } - - if (data.update.signature !== signature) { - console.log(`board - setting new signature`); - setSignature(data.update.signature); - } - - /* This is permanent static data from the server -- do not update - * once set */ - if ('pips' in data.update && !pips) { - console.log(`board - setting new static pips`); - setPips(data.update.pips); - } - if ('tiles' in data.update && !tiles) { - console.log(`board - setting new static tiles`); - setTiles(data.update.tiles); - } - if ('borders' in data.update && !borders) { - console.log(`board - setting new static borders`); - setBorders(data.update.borders); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - console.log('board - bind'); - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { - console.log('board - unbind'); - ws.removeEventListener('message', cbMessage); - } - }, [ws]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - useEffect(() => { - const boardBox = board.current.querySelector('.BoardBox'); - if (boardBox) { - console.log(`board - setting transform scale to ${transform}`); - boardBox.style.transform = `scale(${transform})`; - } - }, [transform]); - - const onResize = () => { - if (!board.current) { - return; - } - - /* Adjust the 'transform: scale' for the BoardBox - * so the board fills the Board - * - * The board is H tall, and H * hexRatio wide */ - const width = board.current.offsetWidth, - height = board.current.offsetHeight; - let _transform; - if (height * hexRatio > width) { - _transform = width / (450. * hexRatio); - } else { - _transform = height / (450.); - } - - if (transform !== _transform) { - setTransform(_transform); - } - }; - - const refOnResize = useRef(onResize); - useEffect(() => { refOnResize.current = onResize; }); - useEffect(() => { - const cbOnResize = e => refOnResize.current(e); - window.addEventListener('resize', cbOnResize); - return () => { - window.removeEventListener('resize', cbOnResize); - } - }, [refOnResize]); - - onResize(); - - useEffect(() => { - if (!ws) { - return; - } - - console.log(`Generating static corner data... should only occur once per reload or socket reconnect.`); - const onCornerClicked = (event, corner) => { - let type; - if (event.currentTarget.getAttribute('data-type') === 'settlement') { - type = 'place-city'; - } else { - type = 'place-settlement'; - } - ws.send(JSON.stringify({ - type, index: corner.index - })); - }; - - const Corner = ({corner}) => { - return
{ - if (e.shiftPressed) { - const tooltip = document.querySelector('.Board .Tooltip'); - tooltip.innerHTML = `
${corner}
`; - showTooltip(); - } - }} - onClick={(event) => { onCornerClicked(event, corner) }} - data-index={corner.index} - style={{ - top: `${corner.top}px`, - left: `${corner.left}px` - }} - >
; - }; - - const generateCorners = () => { - let row = 0, rowCount = 0; - let y = -8 + 0.5 * tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, - x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight; - let index = 0; - const corners = []; - let corner; - - for (let i = 0; i < 21; i++) { - if (row > 2 && rowCount === 0) { - corner = { - index: index++, - top: y-0.5*tileHalfHeight, - left: x-tileHalfHeight - }; - corners.push(); - } - - corner = { - index: index++, - top: y, - left: x - }; - corners.push(); - - corner = { - index: index++, - top: y-0.5*tileHalfHeight, - left: x+tileHalfHeight - }; - corners.push(); - - if (++rowCount === rows[row]) { - corner = { - index: index++, - top: y, - left: x+2.*tileHalfHeight - }; - corners.push(); - - if (row > 2) { - corner = { - index: index++, - top: y-0.5*tileHalfHeight, - left: x+3.*tileHalfHeight - }; - corners.push(); - } - - row++; - rowCount = 0; - y += tileHeight - 10.5; - x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight; - } else { - x += tileHeight; - } - } - return corners; - }; - - setCornerElements(generateCorners()); - }, [ws, setCornerElements]); - - useEffect(() => { - if (!ws) { - return; - } - - console.log(`Generating static road data... should only occur once per reload or socket reconnect.`); - const Road = ({road}) => { - const onRoadClicked = (road) => { - console.log(`Road clicked: ${road.index}`); - if (!ws) { console.error(`board - onRoadClicked - ws is NULL`); return; } - ws.send(JSON.stringify({ - type: 'place-road', index: road.index - })); - }; - - return
{ onRoadClicked(road) }} - data-index={road.index} - style={{ - transform: `translate(-50%, -50%) rotate(${road.angle}deg)`, - top: `${road.top}px`, - left: `${road.left}px` - }} - >
; - }; - - const generateRoads = () => { - let row = 0, rowCount = 0; - let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, - x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight; - - let index = 0; - let road; - - const corners = []; - - for (let i = 0; i < 21; i++) { - const lastRow = row === rows.length - 1; - if (row > 2 && rowCount === 0) { - road = { - index: index++, - angle: -60, - top: y-0.5*tileHalfHeight, - left: x-tileHalfHeight - }; - corners.push(); - } - - road = { - index: index++, - angle: 240, - top: y, - left: x - }; - corners.push(); - - road = { - index: index++, - angle: -60, - top: y-0.5*tileHalfHeight, - left: x+tileHalfHeight - }; - corners.push(); - - if (!lastRow) { - road = { - index: index++, - angle: 0, - top: y, - left: x - }; - corners.push(); - } - - if (++rowCount === rows[row]) { - if (!lastRow) { - road = { - index: index++, - angle: 0, - top: y, - left: x+2.*tileHalfHeight - }; - corners.push(); - } - - if (row > 2) { - road = { - index: index++, - angle: 60, - top: y-0.5*tileHalfHeight, - left: x+3.*tileHalfHeight - }; - corners.push(); - } - - row++; - rowCount = 0; - y += tileHeight - 10.5; - x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight; - } else { - x += tileHeight; - } - } - return corners; - } - setRoadElements(generateRoads()); - }, [ws, setRoadElements]); - - /* Generate Pip, Tile, and Border elements */ - useEffect(() => { - if (!ws) { - return; - } - console.log(`board - Generate pip, border, and tile elements`); - const Pip = ({pip, className}) => { - const onPipClicked = (pip) => { - if (!ws) { console.error(`board - sendPlacement - ws is NULL`); return; } - ws.send(JSON.stringify({ - type: 'place-robber', index: pip.index - })); - }; - - return
{ onPipClicked(pip) }} - data-roll={pip.roll} - data-index={pip.index} - style={{ - top: `${pip.top}px`, - left: `${pip.left}px`, - backgroundImage: `url(${assetsPath}/gfx/pip-numbers.png)`, - backgroundPositionX: `${ 100. * (pip.order % 6) / 5.}%`, - backgroundPositionY: `${ 100 * Math.floor(pip.order / 6) / 5. }%` - }} - >
; - } - - const generatePips = function (pipOrder) { - let row = 0, rowCount = 0; - let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, - x = -(rows[row] - 1) * 0.5 * tileHeight; - let index = 0; - let pip; - return pipOrder.map(order => { - let volcano = false; - pip = { - roll: pips[order].roll, - index: index++, - top: y, - left: x, - order: order - }; - if ('volcano' in rules - && rules[`volcano`].enabled - && pip.roll === 7) { - pip.order = pips.findIndex( - pip => pip.roll === rules[`volcano`].number); - pip.roll = rules[`volcano`].number; - volcano = true; - } - const div = ; - - if (++rowCount === rows[row]) { - row++; - rowCount = 0; - y += tileWidth; - x = - (rows[row] - 1) * 0.5 * tileHeight; - } else { - x += tileHeight; - } - - return div; - }); - }; - - const Tile = ({tile}) => { - const style = { - top: `${tile.top}px`, - left: `${tile.left}px`, - width: `${tileImageWidth}px`, - height: `${tileImageHeight}px`, - backgroundImage: `url(${assetsPath}/gfx/tiles-${tile.type}.png)`, - backgroundPositionY: `-${tile.card * tileHeight}px` - }; - - if (tile.type === 'volcano') { - style.transform = `rotate(-90deg)`; - style.top = `${tile.top + 6}px`; - style.transformOrigin = '0% 50%'; - } - - return
; - }; - - const generateTiles = function (tileOrder, animationSeeds) { - let row = 0, rowCount = 0; - let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, - x = -(rows[row] - 1) * 0.5 * tileHeight; - let index = 0; - return tileOrder.map(order => { - const tile = Object.assign({}, - tiles[order], - { index: index++, left: x, top: y}); - const volcanoActive = 'volcano' in rules - && rules[`volcano`].enabled; - - if ('tiles-start-facing-down' in rules - && rules[`tiles-start-facing-down`].enabled - && state !== 'normal' - && state !== 'volcano' - && state !== 'winner' - && (!volcanoActive || tile.type !== 'desert')) { - tile.type = 'jungle'; - tile.card = 0; - } - if (volcanoActive - && tile.type === 'desert') { - tile.type = 'volcano'; - tile.card = 0; - } - let div; - - if (tile.type === 'wheat') { - div =
- { animations && - }
; - } else if (tile.type === 'sheep') { - div =
- { animations && - - }
; - } else { - div = ; - }; - - if (++rowCount === rows[row]) { - row++; - rowCount = 0; - y += tileWidth; - x = - (rows[row] - 1) * 0.5 * tileHeight; - } else { - x += tileHeight; - } - return div; - }); - }; - - const calculateBorderSlot = (side, e) => { - const borderBox = document.querySelector('.Borders').getBoundingClientRect(); - let angle = (360 + Math.floor(90 + Math.atan2(e.pageY - borderBox.top, e.pageX - borderBox.left) * 180 / Math.PI)) % 360 - (side * 60); - if (angle > 180) { - angle = angle - 360; - } - let slot = 0; - if (angle > -20 && angle < 5) { - slot = 1; - } else if (angle > 5) { - slot = 2; - } - return slot; - } - - const mouseEnter = (border, side, e) => { - const slot = calculateBorderSlot(side, e); - if (!border[slot]) { - clearTooltip(); - return; - } - const tooltip = document.querySelector('.Board .Tooltip'); - tooltip.textContent = border[slot] === 'bank' - ? '3 of one kind for 1 resource' : - `2 ${border[slot]} for 1 resource`; - tooltip.style.top = `${e.pageY}px`; - tooltip.style.left = `${e.pageX + 16}px`; - showTooltip(); - }; - - const mouseMove = (border, side, e) => { - const slot = calculateBorderSlot(side, e); - if (!border[slot]) { - clearTooltip(); - return; - } - const tooltip = document.querySelector('.Board .Tooltip'); - tooltip.textContent = border[slot] === 'bank' - ? '3 of one kind for 1 resource' : - `2 ${border[slot]} for 1 resource`; - tooltip.style.top = `${e.pageY}px`; - tooltip.style.left = `${e.pageX + 16}px`; - showTooltip(); - } - - const mouseLeave = (border, e) => { - clearTooltip(); - }; - - const generateBorders = function(borderOrder) { - const sides = 6; - let side = -1; - return borderOrder.map(order => { - const border = borders[order]; - side++; - let x = + Math.sin(Math.PI - side / sides * 2. * Math.PI) * radius, - y = Math.cos(Math.PI - side / sides * 2. * Math.PI) * radius; - let prev = (order === 0) ? 6 : order; - const file = `borders-${order+1}.${prev}.png`; - const value = side; - return
{ mouseEnter(border, value, e) }} - onMouseMove={(e) => { mouseMove(border, value, e) }} - onMouseLeave={mouseLeave} - style={{ - width: `${borderImageWidth}px`, - height: `${borderImageHeight}px`, - top: `${y}px`, - left: `${x}px`, - transform: `rotate(${side*(360/sides)}deg) translate(${borderOffsetX}px, ${borderOffsetY}px) scale(-1, -1)`, - backgroundImage: `url(${assetsPath}/gfx/${file} )` - }} - />; - }); - }; - - if (borders && borderOrder) { - console.log(`board - Generate board - borders`); - setBorderElements(generateBorders(borderOrder)); - } - - if (tiles && tileOrder && animationSeeds) { - console.log(`board - Generate board - tiles`); - setTileElements(generateTiles(tileOrder, animationSeeds)); - } - - /* Regenerate pips every time; it uses ws */ - if (pips && pipOrder) { - console.log(`board - Generate board - pips`); - setPipElements(generatePips(pipOrder)); - } - - if (signature && signature !== generated) { - console.log(`board - Regnerating for ${signature}`); - setGenerated(signature); - } - }, [ - signature, generated, - pips, pipOrder, borders, borderOrder, tiles, tileOrder, - animationSeeds, - ws, state, rules, animations - ]); - - /* Re-render turn info after every render */ - useEffect(() => { - if (!turn) { return; } - let nodes = document.querySelectorAll('.Active'); - for (let i = 0; i < nodes.length; i++) { - nodes[i].classList.remove('Active'); - } - if (turn.roll) { - nodes = document.querySelectorAll(`.Pip[data-roll="${turn.roll}"]`); - for (let i = 0; i < nodes.length; i++) { - const index = nodes[i].getAttribute('data-index'); - if (index !== null) { - const tile = document.querySelector(`.Tile[data-index="${index}"]`); - if (tile) { - tile.classList.add('Active'); - } - } - nodes[i].classList.add('Active'); - } - } - }); - - /* Re-render placements after every render */ - useEffect(() => { - if (!placements) { return; } - /* Set color and type based on placement data from the server */ - placements.corners.forEach((corner, index) => { - const el = document.querySelector(`.Corner[data-index="${index}"]`); - if (!el) { return; } - if (turn.volcano === index) { - el.classList.add('Lava'); - } else { - el.classList.remove('Lava'); - } - if (!corner.color) { - el.removeAttribute('data-color'); - el.removeAttribute('data-type'); - } else { - el.setAttribute('data-color', corner.color); - el.setAttribute('data-type', corner.type); - } - }); - placements.roads.forEach((road, index) => { - const el = document.querySelector(`.Road[data-index="${index}"]`); - if (!el) { return; } - if (!road.color) { - el.removeAttribute('data-color'); - } else { - if (road.longestRoad) { - if (road.longestRoad === longestRoadLength) { - el.classList.add('LongestRoad'); - } else { - el.classList.remove('LongestRoad'); - } - el.setAttribute('data-longest', road.longestRoad); - } else { - el.removeAttribute('data-longest'); - } - el.setAttribute('data-color', road.color); - } - }); - - /* Clear all 'Option' targets */ - let nodes = document.querySelectorAll(`.Option`); - for (let i = 0; i < nodes.length; i++) { - nodes[i].classList.remove('Option'); - } - - /* Add 'Option' based on turn.limits */ - if (turn && turn.limits) { - if (turn.limits['roads']) { - turn.limits['roads'].forEach(index => { - const el = document.querySelector(`.Road[data-index="${index}"]`); - if (!el) { return; } - el.classList.add('Option'); - el.setAttribute('data-color', turn.color); - }); - } - if (turn.limits['corners']) { - turn.limits['corners'].forEach(index => { - const el = document.querySelector(`.Corner[data-index="${index}"]`); - if (!el) { return; } - el.classList.add('Option'); - el.setAttribute('data-color', turn.color); - }); - } - if (turn.limits['tiles']) { - turn.limits['tiles'].forEach(index => { - const el = document.querySelector(`.Tile[data-index="${index}"]`); - if (!el) { return; } - el.classList.add('Option'); - }); - } - if (turn.limits['pips']) { - turn.limits['pips'].forEach(index => { - const el = document.querySelector(`.Pip[data-index="${index}"]`); - if (!el) { return; } - el.classList.add('Option'); - }); - } - } - - /* Clear the robber */ - nodes = document.querySelectorAll(`.Pip.Robber`); - for (let i = 0; i < nodes.length; i++) { - nodes[i].classList.remove('Robber'); - [ 'Robert', 'Roberta', 'Velocirobber' ].forEach(robberName => - nodes[i].classList.remove(robberName) - ); - } - - /* Place the robber */ - if (robber !== undefined) { - const el = document.querySelector(`.Pip[data-index="${robber}"]`); - if (el) { - el.classList.add('Robber'); - el.classList.add(robberName); - } - } - }); - - const canAction = (action) => { - return (turn && Array.isArray(turn.actions) && turn.actions.indexOf(action) !== -1); - }; - - const canRoad = (canAction('place-road') - && turn.color === color - && (state === 'initial-placement' || state === 'normal')); - - const canCorner = ((canAction('place-settlement') || canAction('place-city')) - && turn.color === color - && (state === 'initial-placement' || state === 'normal')); - - const canPip = (canAction('place-robber') - && turn.color === color - && (state === 'initial-placement' || state === 'normal')); - - console.log(`board - tile elements`, tileElements); - return ( -
-
tooltip
-
-
- { borderElements } -
-
- { tileElements } -
-
- { pipElements } -
-
- { cornerElements } -
-
- { roadElements } -
-
-
- ); -}; - -export { Board }; diff --git a/client/src/BoardPieces.js b/client/src/BoardPieces.js deleted file mode 100644 index 1f9e6d4..0000000 --- a/client/src/BoardPieces.js +++ /dev/null @@ -1,51 +0,0 @@ - -import React from "react"; - -import "./BoardPieces.css"; -import { useStyles } from './Styles.js'; - -const Road = ({ color, onClick }) => { - const classes = useStyles(); - return
onClick('road')}>
; -} - -const Settlement = ({ color, onClick }) => { - const classes = useStyles(); - return
onClick('settlement')}>
; -} - -const City = ({ color, onClick }) => { - const classes = useStyles(); - return
onClick('city')}>
; -} - -const BoardPieces = ({ player, onClick }) => { - if (!player) { - return <>; - } - - const color = player.color; - - const roads = []; - for (let i = 0; i < player.roads; i++) { - roads.push(); - } - const settlements = []; - for (let i = 0; i < player.settlements; i++) { - settlements.push(); - } - const cities = []; - for (let i = 0; i < player.cities; i++) { - cities.push(); - } - - return ( -
-
{cities}
-
{settlements}
-
{roads}
-
- ); -}; - -export { BoardPieces }; \ No newline at end of file diff --git a/client/src/Chat.js b/client/src/Chat.js deleted file mode 100644 index 861f453..0000000 --- a/client/src/Chat.js +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState, useEffect, useContext, useRef, useCallback, useMemo } from "react"; -import Paper from '@material-ui/core/Paper'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import Moment from 'react-moment'; -import TextField from '@material-ui/core/TextField'; -import 'moment-timezone'; -import equal from "fast-deep-equal"; - -import "./Chat.css"; -import { PlayerColor } from './PlayerColor.js'; -import { Resource } from './Resource.js'; -import { Dice } from './Dice.js'; -import { GlobalContext } from "./GlobalContext.js"; - -const Chat = () => { - const [lastTop, setLastTop] = useState(0); - const [autoScroll, setAutoScroll] = useState(true); - const [latest, setLatest] = useState(''); - const [scrollTime, setScrollTime] = useState(0); - const [chat, setChat] = useState([]); - const [startTime, setStartTime] = useState(0); - - const { ws, name } = useContext(GlobalContext); - const fields = useMemo(() => [ - 'chat', 'startTime' - ], []); - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`chat - game update`); - if (data.update.chat && !equal(data.update.chat, chat)) { - console.log(`chat - game update - ${data.update.chat.length} lines`); - setChat(data.update.chat); - } - if (data.update.startTime && data.update.startTime !== startTime) { - setStartTime(data.update.startTime); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { - ws.removeEventListener('message', cbMessage); - } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - const chatKeyPress = useCallback((event) => { - if (event.key === "Enter") { - if (!autoScroll) { - setAutoScroll(true); - } - - ws.send(JSON.stringify({ type: 'chat', message: event.target.value })); - event.target.value = ""; - } - }, [ws, setAutoScroll, autoScroll]); - - const chatScroll = (event) => { - const chatList = event.target, - fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop)); - - /* If scroll is within 20 pixels of the bottom, turn on auto-scroll */ - const shouldAutoscroll = (fromBottom < 20); - - if (shouldAutoscroll !== autoScroll) { - setAutoScroll(shouldAutoscroll); - } - - /* If the list should not auto scroll, then cache the current - * top of the list and record when we did this so we honor - * the auto-scroll for at least 500ms */ - if (!shouldAutoscroll) { - const target = Math.round(chatList.scrollTop); - if (target !== lastTop) { - setLastTop(target); - setScrollTime(Date.now()); - } - } - }; - - useEffect(() => { - const chatList = document.getElementById("ChatList"), - currentTop = Math.round(chatList.scrollTop); - - if (autoScroll) { - /* Auto-scroll to the bottom of the chat window */ - const target = Math.round(chatList.scrollHeight - chatList.offsetHeight); - if (currentTop !== target) { - chatList.scrollTop = target; - } - return; - } - - /* Maintain current position in scrolled view if the user hasn't - * been scrolling in the past 0.5s */ - if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) { - chatList.scrollTop = lastTop; - } - }); - - const messages = chat.map((item, index) => { - let message; - /* Do not perform extra parsing on player-generated - * messages */ - if (item.normalChat) { - message =
{item.message}
; - } else { - const punctuation = item.message.match(/(\.+$)/); - let period; - if (punctuation) { - period = punctuation[1]; - } else { - period = ''; - } - let lines = item.message.split('.'); - message = lines - .filter(line => line.trim() !== '') - .map((line, index) => { - /* If the date is in the future, set it to now */ - const dice = line.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/); - if (dice) { - if (dice[4]) { - return
{dice[1]} - , - {dice[5]}{ period }
; - } else { - return
{dice[1]} - {dice[5]}{ period }
; - } - } - - let start = line, message; - while (start) { - const resource = start.match(/^(.*)(([0-9]+) (wood|sheep|wheat|stone|brick),?)(.*)$/); - if (resource) { - const count = resource[3] ? parseInt(resource[3]) : 1; - message = <>{resource[5]}{message}; - start = resource[1]; - } else { - message = <>{start}{message}; - start = ''; - } - } - return
{ message }{ period }
; - }); - } - - return ( - - { item.color && - - } - Date.now() ? - Date.now() : item.date} interval={1000}/>} /> - - ); - }); - - if (chat.length && chat[chat.length - 1].date !== latest) { - setLatest(chat[chat.length - 1].date); - setAutoScroll(true); - } - - return ( - - - { messages } - - Game duration: } - variant="outlined"/> - - ); -} - -export { Chat }; \ No newline at end of file diff --git a/client/src/ChooseCard.js b/client/src/ChooseCard.js deleted file mode 100644 index ddbe513..0000000 --- a/client/src/ChooseCard.js +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useState, useCallback, useEffect, useMemo, useRef, - useContext } from "react"; -import equal from "fast-deep-equal"; - -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; -import "./ChooseCard.css"; -import {Resource} from './Resource.js'; -import {GlobalContext} from './GlobalContext.js'; - -const ChooseCard = () => { - const { ws } = useContext(GlobalContext); - const [turn, setTurn] = useState(undefined); - const [color, setColor] = useState(undefined); - const [state, setState] = useState(undefined); - const [cards, setCards] = useState([]); - const fields = useMemo(() => [ - 'turn', 'color', 'state' - ], []); - - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`choose-card - game-update: `, data.update); - if ('turn' in data.update && !equal(turn, data.update.turn)) { - setTurn(data.update.turn); - } - if ('color' in data.update && data.update.color !== color) { - setColor(data.update.color); - } - if ('state' in data.update && data.update.state !== state) { - setState(data.update.state); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { ws.removeEventListener('message', cbMessage); } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - const selectResources = useCallback((event) => { - ws.send(JSON.stringify({ - type: 'select-resources', - cards - })); - }, [ws, cards]); - - let count = 0; - if (turn && turn.actions && turn.actions.indexOf('select-resources') !== -1) { - if (turn.active) { - if (turn.color === color) { - count = turn.active === 'monopoly' ? 1 : 2; - } - } - - if (state === 'volcano') { - if (!turn.select) { - count = 0; - } else if (color in turn.select) { - count = turn.select[color]; - } else { - count = 0; - } - } - } - - const selectCard = useCallback((event) => { - const selected = document.querySelectorAll('.ChooseCard .Selected'); - if (selected.length > count) { - for (let i = 0; i < selected.length; i++) { - selected[i].classList.remove('Selected'); - } - setCards([]); - return; - } - - let tmp = []; - for (let i = 0; i < selected.length; i++) { - tmp.push(selected[i].getAttribute('data-type')); - } - setCards(tmp); - }, [ setCards, count ]); - - if (count === 0) { - return <>; - } - - const resources = [ - 'wheat', 'brick', 'stone', 'sheep', 'wood' - ].map(type => { - return ; - }); - - let title; - switch (turn.active) { - case 'monopoly': - title = <>Monopoly! Tap the resource type you want everyone to give you!; - break; - case 'year-of-plenty': - title = <>Year of Plenty! Tap the two resources you want to receive from the bank!; - break; - case 'volcano': - title = <>Volcano has minerals! Tap the {count} resources you want to receive from the bank!; - break; - default: - title = <>Unknown card type {turn.active}.; - break; - } - - return ( -
- -
{ title }
-
- { resources } -
-
-
-
- ); -}; - -export {ChooseCard}; \ No newline at end of file diff --git a/client/src/Dice.js b/client/src/Dice.js deleted file mode 100644 index 7b4304d..0000000 --- a/client/src/Dice.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import "./Dice.css"; -import { assetsPath } from './Common.js'; - -const Dice = ({ pips }) => { - let name; - switch (pips.toString()) { - case '1': name = 'one'; break; - case '2': name = 'two'; break; - case '3': name = 'three'; break; - case '4': name = 'four'; break; - case '5': name = 'five'; break; - default: - case '6': name = 'six'; break; - } - return ( - {name} - ); -}; - -export { Dice }; - - diff --git a/client/src/GameOrder.js b/client/src/GameOrder.js deleted file mode 100644 index dd0200f..0000000 --- a/client/src/GameOrder.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState, useEffect, useContext, useRef, useMemo } from "react"; -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; -import equal from "fast-deep-equal"; - -import { Dice } from "./Dice.js"; -import { PlayerColor } from "./PlayerColor.js"; - -import "./GameOrder.css"; - -import { GlobalContext } from "./GlobalContext.js"; - -const GameOrder = () => { - const { ws } = useContext(GlobalContext); - const [players, setPlayers] = useState({}); - const [color, setColor] = useState(undefined); - const fields = useMemo(() => [ - 'players', 'color' - ], []); - - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`GameOrder game-update: `, data.update); - if ('players' in data.update && !equal(players, data.update.players)) { - setPlayers(data.update.players); - } - if ('color' in data.update && data.update.color !== color) { - setColor(data.update.color); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { ws.removeEventListener('message', cbMessage); } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - const sendMessage = (data) => { - ws.send(JSON.stringify(data)); - } - - const rollClick = (event) => { - sendMessage({ type: 'roll' }); - } - - let playerElements = [], hasRolled = true; - for (let key in players) { - const item = players[key], name = item.name; - if (!name) { - continue; - } - if (!item.orderRoll) { - item.orderRoll = 0; - } - if (key === color) { - hasRolled = item.orderRoll !== 0; - } - playerElements.push({ name, color: key, ...item }); - } - - playerElements.sort((A, B) => { - if (A.order === B.order) { - if (A.orderRoll === B.orderRoll) { - return A.name.localeCompare(B.name); - } - return B.orderRoll - A.orderRoll; - } - return B.order - A.order; - }); - - playerElements = playerElements.map(item => -
- -
{item.name}
- { item.orderRoll !== 0 && <>rolled . {item.orderStatus} } - { item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}} -
- ); - - return ( -
- -
Game Order
-
- { playerElements } -
- -
-
- ); -}; - -export { GameOrder }; diff --git a/client/src/Hand.js b/client/src/Hand.js deleted file mode 100644 index 79aedc4..0000000 --- a/client/src/Hand.js +++ /dev/null @@ -1,178 +0,0 @@ -import React, { useContext, useState, useMemo, useRef, useEffect } from "react"; -import equal from "fast-deep-equal"; - -import { Resource } from './Resource.js'; -import { Placard } from './Placard.js'; -import { GlobalContext } from './GlobalContext.js'; -import { assetsPath } from "./Common.js"; -import "./Hand.css"; - -const Development = ({type, card, onClick}) => { - return ( -
- ); -}; - -const Hand = ({buildActive, setBuildActive, setCardActive}) => { - const { ws } = useContext(GlobalContext); - const [priv, setPriv] = useState(undefined); - const [color, setColor] = useState(undefined); - const [turn, setTurn] = useState(undefined); - const [longestRoad, setLongestRoad] = useState(undefined); - const [largestArmy, setLargestArmy] = useState(undefined); - const [development, setDevelopment] = useState([]); - const [mostPorts, setMostPorts] = useState(undefined); - const [mostDeveloped, setMostDeveloped] = useState(undefined); - const [selected, setSelected] = useState(0); - - const fields = useMemo(() => [ - 'private', 'turn', 'color', 'longestRoad', 'largestArmy', 'mostPorts', 'mostDeveloped' - ], []); - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`hand - game-update: `, data.update); - if ('private' in data.update && !equal(priv, data.update.private)) { - setPriv(data.update.private); - } - if ('turn' in data.update && !equal(turn, data.update.turn)) { - setTurn(data.update.turn); - } - if ('color' in data.update && color !== data.update.color) { - setColor(data.update.color); - } - if ('longestRoad' in data.update && longestRoad !== data.update.longestRoad) { - setLongestRoad(data.update.longestRoad); - } - if ('largestArmy' in data.update && largestArmy !== data.update.largestArmy) { - setLargestArmy(data.update.largestArmy); - } - if ('mostDeveloped' in data.update - && data.update.mostDeveloped !== mostDeveloped) { - setMostDeveloped(data.update.mostDeveloped); - } - if ('mostPorts' in data.update - && data.update.mostPorts !== mostPorts) { - setMostPorts(data.update.mostPorts); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { ws.removeEventListener('message', cbMessage); } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - useEffect(() => { - if (!priv) { - return; - } - - const cardClicked = (card) => { - setCardActive(card); - } - - const stacks = {}; - priv.development.forEach(card => - (card.type in stacks) - ? stacks[card.type].push(card) - : stacks[card.type] = [card]); - - const development = []; - for (let type in stacks) { - const cards = stacks[type] - .sort((A, B) => { - if (A.played) { - return -1; - } - if (B.played) { - return +1; - } - return B.turn - A.turn; /* Put playable cards on top */ - }).map(card => cardClicked(card)} - card={card} - key={`${type}-${card.card}`} - type={`${type}-${card.card}`}/>); - development.push(
{ cards }
); - } - setDevelopment(development); - }, [priv, setDevelopment, setCardActive]); - - useEffect(() => { - const count = document.querySelectorAll('.Hand .CardGroup .Resource.Selected'); - if (count.length !== selected) { - setSelected(count.length); - } - }, [setSelected, selected, turn]); - - if (!priv) { - return <>; - } - - const cardSelected = (event) => { - const count = document.querySelectorAll('.Hand .CardGroup .Resource.Selected'); - setSelected(count.length); - } - - return
- {
- {selected} cards selected -
} -
- - - - - -
-
- { development } -
- {mostDeveloped && mostDeveloped === color && - - } - {mostPorts && mostPorts === color && - - } - { longestRoad && longestRoad === color && - - } - { largestArmy && largestArmy === color && - - } - -
; -} - -export { Hand }; \ No newline at end of file diff --git a/client/src/HouseRules.js b/client/src/HouseRules.js deleted file mode 100644 index e54808f..0000000 --- a/client/src/HouseRules.js +++ /dev/null @@ -1,426 +0,0 @@ -import React, { useState, useEffect, useContext, useRef, useMemo, useCallback } from "react"; -import equal from "fast-deep-equal"; - -import Paper from '@material-ui/core/Paper'; -import Button from '@material-ui/core/Button'; -import Switch from '@material-ui/core/Switch'; - -import "./HouseRules.css"; - -import { GlobalContext } from "./GlobalContext.js"; -import { Placard } from "./Placard.js"; - -/* Volcano based on https://www.ultraboardgames.com/catan/the-volcano.php */ -const Volcano = ({ ws, rules, field, disabled }) => { - const init = (Math.random() > 0.5) - ? Math.floor(8 + Math.random() * 5) /* Do not include 7 */ - : Math.floor(2 + Math.random() * 5); /* Do not include 7 */ - const [number, setNumber] = - useState((field in rules && 'number' in rules[field]) ? rules[field].number : init); - const [gold, setGold] = - useState((field in rules && 'gold' in rules[field]) ? rules[field].gold : false); - - console.log(`house-rules - ${field} - `, rules[field]); - - useEffect(() => { - if (field in rules) { - setGold('gold' in rules[field] ? rules[field].gold : true); - setNumber('number' in rules[field] ? rules[field].number : init); - let update = false; - if (!('gold' in rules[field])) { - rules[field].gold = true; - update = true; - } - if (!('number' in rules[field])) { - rules[field].number = init; - update = true; - } - - if (update) { - ws.send(JSON.stringify({ - type: 'rules', - rules: rules - })); - } - } - }, [rules, field, init, ws]); - - const toggleGold = () => { - rules[field].gold = !gold; - rules[field].number = number; - setGold(rules[field].gold); - - ws.send(JSON.stringify({ - type: 'rules', - rules: rules - })); - }; - - const update = (delta) => { - let value = number + delta; - if (value < 2 || value > 12) { - return; - } - /* Number to trigger Volcano cannot be 7 */ - if (value === 7) { - value = delta > 0 ? 8 : 6; - } - setNumber(value); - rules[field].gold = gold; - rules[field].number = value; - ws.send(JSON.stringify({ - type: 'rules', - rules: rules - })); - }; - - return
-
- The Volcano replaces the Desert. When the Volcano erupts, roll a die to determine the direction the lava will flow. One of the six intersections on the Volcano tile will be affected. If there is a settlement on the selected intersection, it is destroyed! -
-
- Remove it from the board (its owner may rebuild it later). If a city is located there, it is reduced to a settlement! Replace the city with a settlement of its owner's color. If he has no settlements remaining, the - city is destroyed instead. -
-
- The presence of the Robber on the Volcano does not prevent the Volcano from erupting. -
-
- Roll {number} and the Volcano erupts! -  /  - -
-
-
Volcanoes have gold!: Volcano can produce resources when its number is rolled.
-
- toggleGold()} - {...{ disabled }} /> -
-
-
- Volcanoes tend to be rich in valuable minerals such as gold or gems. - Each settlement that is adjacent to the Volcano when it erupts may produce any one of the five resources it's owner desires. -
-
- Each city adjacent to the Volcano may produce any two resources. This resource production is taken before the results of the volcano eruption are resolved. Note that while the Robber can not prevent the Volcano from erupting, he does prevent any player from producing resources from the Volcano hex if he has been placed there. -
-
; -} - -const VictoryPoints = ({ ws, rules, field }) => { - const minVP = 10; - const [points, setPoints] = useState(rules[field].points || minVP); - console.log(`house-rules - ${field} - `, rules[field]); - - if (!(field in rules)) { - rules[field] = { - points: minVP - } - }; - - if (rules[field].points && rules[field].points !== points) { - setPoints(rules[field].points); - } - - const update = (value) => { - let points = (rules[field].points || minVP) + value; - if (points < minVP) { - return; - } - if (points !== rules[field].points) { - setPoints(points); - rules[field].points = points; - ws.send(JSON.stringify({ - type: 'rules', - rules: rules - })); - } - }; - - return
- {points} points. -  /  - < button onClick = {() => update(-1)}> down -
; -} - -const NotImplemented = () => { - return
Not yet implemented.
; -} - -/* -The Jungle -Setup -On any normally assembled map, replace all deserts with jungles. Select an extra number token with the value of 3, 4, 5, 9, 10, or 11. (All players must agree on which number to use.) Place this number token on the jungle hex. The Robber begins the game on the jungle hex. -Special rules -The Robber may be played on the jungle hex when a player has robber control. -However, when the number on an unblocked jungle tile is rolled, adjacent players may explore the jungle and make discoveries that aid them in developing their principalities. Each adjacent settlement will receive one discovery, while each adjacent city will receive two discoveries. -Discoveries are represented by Discovery Counters, instead of resource cards. Discovery Counters do not count against a player's hand limit of resources when a 7 is rolled. Discovery Counters can not be stolen by the Robber, can not be claimed by a Monopoly, can not be earned through a Year of Plenty, and may not be used in any trades. -Discovery Counters may be used to aid the purchase of development cards only. Each Discovery Counter can be used to replace any one of the three resources needed to purchase a card. Up to three Discovery Counters may be used on each card purchase. Any combination of Discovery Counters and the three usual resources may be used. For example, a player may purchase a development card with one ore and two Discovery Counters. Similarly, a development card could be purchased with one wool, one grain, and one Discovery Counter. -*/ -const HouseRules = ({ houseRulesActive, setHouseRulesActive }) => { - const { ws } = useContext(GlobalContext); - const [rules, setRules] = useState(undefined); - const [state, setState] = useState(undefined); - const [ruleElements, setRuleElements] = useState([]); - const fields = useMemo(() => [ - 'state', 'rules' - ], []); - const onWsMessage = (event) => { - const data = JSON.parse(event.data); - switch (data.type) { - case 'game-update': - console.log(`house-rules - game update`, data.update); - if ('state' in data.update && data.update.state !== state) { - setState(data.update.state); - } - if ('rules' in data.update - && !equal(data.update.rules, rules)) { - console.log(`house-rules - setting house rules to `, - data.update.rules); - setRules(data.update.rules); - } - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { refWsMessage.current = onWsMessage; }); - useEffect(() => { - if (!ws) { return; } - const cbMessage = e => refWsMessage.current(e); - ws.addEventListener('message', cbMessage); - return () => { - ws.removeEventListener('message', cbMessage); - } - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { return; } - ws.send(JSON.stringify({ - type: 'get', - fields - })); - }, [ws, fields]); - - const dismissClicked = useCallback((event) => { - setHouseRulesActive(false); - }, [setHouseRulesActive]); - - console.log(`house-rules - render - `, { rules }); - - const setRule = useCallback((event, key) => { - if (!(key in rules)) { - rules[key] = { enabled: false }; - } - rules[key].enabled = !rules[key].enabled; - console.log(`house-rules - set ${key} - ${rules[key].enabled}`); - setRules(Object.assign({}, rules)); - ws.send(JSON.stringify({ - type: 'rules', - rules - })); - }, [ws, rules]); - - useEffect(() => { - /* https://icebreaker.com/games/catan-1/feature/catan-house-rules */ - setRuleElements([{ - title: `Why you play so slowf`, - key: `slowest-turn`, - description: `The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.`, - element: , - implemented: false - }, { - title: `You are so developed`, - key: `most-developed`, - description: - `The player with the most development cards (more than 4) receives 2VP.`, - element: , - implemented: true - }, { - title: `Another round of port`, - key: `port-of-call`, - description: - `The player with the most harbor ports (more than 2) receives 2VP.`, - element: , - implemented: true - }, { - title: `More victory points`, - key: `victory-points`, - description: `Customize how many Victory Points are required to win.`, - element: , - implemented: true - }, { - title: `Tiles start facing down`, - key: `tiles-start-facing-down`, - description: `Resource tiles start upside-down while placing starting settlements.`, - element:
Once all players have placed their initial settlements - and roads, the tiles are flipped and you discover what the - resources are.
, - implemented: true - }, { - title: `Bribery`, - key: `bribery`, - description: `Dissuade enemies from robbing you by offering resources voluntarily.`, - element: , - }, { - title: `King of the Hill`, - key: `king-of-the-hill`, - description: `Keep your lead for one full turn after you reach max victory points.`, - element: , - }, { - title: `Everyone gets one re-roll`, - key: `everyone-gets-one-reroll`, - description: `Each player gets one chance re-roll at any point.`, - element: , - }, { - title: `The Bridge`, - key: `the-bridge`, - description: `Build a super-bridge across one resource tile.`, - element: , - }, { - title: `Discard desert`, - key: `discard-desert`, - description: `Scrap the desert in favour of an additional resource tile.`, - element: , - }, { - title: `Roll double, roll again`, - key: `roll-double-roll-again`, - description: `Roll again if you roll two of the same number.`, - element: < div>If you roll doubles, players get those resources and - then you must roll again.
, - implemented: true, - }, { - title: `Twelve and Two are synonyms`, - key: `twelve-and-two-are-synonyms`, - description: `If twelve is rolled, two scores as well. And vice-versa.`, - element: < div>If you roll a twelve or two, resources are triggered - for both.
, - implemented: true, - }, { - title: `Robin Hood robber`, - key: `robin-hood-robber`, - description: `Robbers can't steal from players with two or less victory points.`, - element: <>, - implemented: true - }, { - title: `Crime and Punishment`, - key: `crime-and-punishment`, - description: `Change how the robber works to make Catan more or less competitive.`, - element: , - }, { - title: `Credit? Debt? You bebt!`, - key: `credit`, - description: `Trade with resources you don't have.`, - element: , - }, { - title: `Volcanoes are a lava fun!`, - key: `volcano`, - description: `A volcano is on the island! Let the lava flow!`, - element: , - implemented: true, - }, { - title: `Don't keep paying those soldiers!`, - key: `mercenaries`, - description: `Disband a soldier and pick two resources to receive as tribute. If you no longer have the Longest Army, you lose it.`, - element: , - } ] - .filter(item => item.implemented) - .sort((A, B) => { - if (A.implemented && B.implemented) { - return A.title.localeCompare(); - } - if (A.implemented) { - return -1; - } - if (B.implemented) { - return +1; - } - return A.title.localeCompare(); - }) - .map(item => { - const disabled = (state !== 'lobby' || !item.implemented), - defaultChecked = rules - && (item.key in rules) - ? rules[item.key].enabled - : false; - console.log(`house-rules - ${item.key} - `, - { rules, defaultChecked, disabled }); - return
-
-
{item.title}: {item.description}
- setRule(e, item.key)} - {...{ disabled }} /> -
- { defaultChecked && item.element } -
- })); - }, [rules, setRules, setRuleElements, state, ws, setRule ]); - - if (!houseRulesActive) { - return <>; - } - - return ( -
- -
House Rules
-
- { ruleElements } -
- -
-
- ); -}; - -export { HouseRules }; \ No newline at end of file diff --git a/client/src/MediaControl.js b/client/src/MediaControl.js deleted file mode 100644 index b2fd10a..0000000 --- a/client/src/MediaControl.js +++ /dev/null @@ -1,711 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback, - useContext } from "react"; -import Moveable from "react-moveable"; - -import "./MediaControl.css"; - -import VolumeOff from '@mui/icons-material/VolumeOff'; -import VolumeUp from '@mui/icons-material/VolumeUp'; -import MicOff from '@mui/icons-material/MicOff'; -import Mic from '@mui/icons-material/Mic'; -import VideocamOff from '@mui/icons-material/VideocamOff'; -import Videocam from '@mui/icons-material/Videocam'; - -import { GlobalContext } from "./GlobalContext.js"; -const debug = true; - -/* Proxy object so we can pass in srcObject to