1
0

Board rendering only occurs once per signature

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-03-12 00:19:54 -08:00
parent dfc5123d25
commit 222ca2d3c3
11 changed files with 1171 additions and 1176 deletions

View File

@ -28,7 +28,7 @@ body {
left: 0;
bottom: 0;
right: 0;
background-color: #00000060;
background-color: #80000060;
}
.Table .ErrorDialog .Error {
@ -36,6 +36,23 @@ body {
padding: 1rem;
}
.Table .WarningDialog {
z-index: 10000;
display: flex;
justify-content: space-around;
align-items: center;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.Table .WarningDialog .Warning {
display: flex;
padding: 1rem;
}
.Table .Game {
display: flex;
flex-direction: column;

View File

@ -17,7 +17,8 @@ import { Chat } from "./Chat.js";
import { MediaAgent } from "./MediaControl.js";
import { Board } from "./Board.js";
import { Actions } from "./Actions.js";
import { base, gamesPath, debounce } from './Common.js';
import { base, gamesPath } from './Common.js';
import { GameOrder } from "./GameOrder.js";
import history from "./history.js";
import "./App.css";
@ -28,66 +29,89 @@ const Table = () => {
const [ ws, setWs ] = useState(global.ws);
const [ name, setName ] = useState(global.name);
const [ error, setError ] = useState(undefined);
const [ warning, setWarning ] = useState(undefined);
const [ peers, setPeers ] = useState({});
const [loaded, setLoaded] = useState(false);
const [connecting, setConnecting] = useState(false);
const [connecting, setConnecting] = useState(undefined);
const [state, setState] = useState(undefined);
const [color, setColor] = useState(undefined);
const fields = [ 'name', 'id', 'state', 'color', 'name' ];
/*
useEffect(() => {
console.log(peers);
}, [peers]);
*/
const onWsOpen = (event) => {
console.log(`ws: open`);
setError("");
/* We do not set the socket as bound until the 'open' message
/* We do not set the socket as connected until the 'open' message
* comes through */
setWs(event.target);
setConnecting(false);
setConnecting(event.target);
/* 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 */
if (loaded) {
event.target.send(JSON.stringify({
type: 'game-update'
}));
if (name) {
event.target.send(JSON.stringify({
type: 'player-name',
name
}));
}
} else {
event.target.send(JSON.stringify({
type: 'get',
fields: [ 'name', 'id' ]
}))
}
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(data.error);
console.error(`App - error`, data.error);
window.alert(data.error);
break;
case 'warning':
setWarning(`App - warning`, data.warning);
setTimeout(() => {
if (data.warning === warning) {
setWarning("");
}
}, 3000);
break;
case 'game-update':
if (!loaded) {
setLoaded(true);
}
console.log(`ws: message - ${data.type}`, data.update);
if ('player' in data.update) {
const player = data.update.player;
if (player.name !== name) {
console.log(`App - setting name (via player): ${data.update.name}`);
setName(data.update.name);
}
if (player.color !== color) {
console.log(`App - setting color (via player): ${data.update.color}`);
setColor(data.update.color);
}
}
if ('name' in data.update && data.update.name !== name) {
console.log(`Updating name to ${data.update.name}`);
console.log(`App - setting name: ${data.update.name}`);
setName(data.update.name);
}
if ('id' in data.update && data.update.id !== gameId) {
console.log(`Updating id to ${data.update.id}`);
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}`);
setState(data.update.state);
}
if ('color' in data.update && data.update.color !== color) {
console.log(`App - setting color: ${color}`);
setColor(data.update.color);
}
break;
default:
break;
@ -98,7 +122,7 @@ const Table = () => {
let timer = 0;
function reset() {
timer = 0;
setConnecting(false);
setConnecting(undefined);
};
return _ => {
if (timer) {
@ -109,20 +133,19 @@ const Table = () => {
})();
const onWsError = (event) => {
console.log(`ws: error`, event);
console.error(`ws: error`, event);
const error = `Connection to Ketr Ketran game server failed! ` +
`Connection attempt will be retried every 5 seconds.`;
console.error(error);
setError(error);
setWs(undefined);
resetConnection();
setError(error);
};
const onWsClose = (event) => {
const error = `Connection to Ketr Ketran game was lost. ` +
`Attempting to reconnect...`;
console.error(error);
console.log(`ws: close`);
console.warn(`ws: close`);
setError(error);
setWs(undefined);
resetConnection();
};
@ -152,7 +175,7 @@ const Table = () => {
return;
}
console.log("Requesting new game.");
console.log(`Requesting new game.`);
window.fetch(`${base}/api/v1/games/`, {
method: 'POST',
@ -190,8 +213,13 @@ const Table = () => {
return;
}
let socket = ws;
if (!ws) {
const unbind = () => {
console.log(`table - unbind`);
}
console.log(`table - bind`);
if (!ws && !connecting) {
let loc = window.location, new_uri;
if (loc.protocol === "https:") {
new_uri = "wss";
@ -200,42 +228,68 @@ const Table = () => {
}
new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}`;
console.log(`Attempting WebSocket connection to ${new_uri}`);
socket = new WebSocket(new_uri);
setConnecting(true);
setWs(new WebSocket(new_uri));
setConnecting(undefined);
return unbind;
}
if (!ws) {
return unbind;
}
console.log('table - bind');
const cbOpen = e => refWsOpen.current(e);
const cbMessage = e => refWsMessage.current(e);
const cbClose = e => refWsClose.current(e);
const cbError = e => refWsError.current(e);
socket.addEventListener('open', cbOpen);
socket.addEventListener('close', cbClose);
socket.addEventListener('error', cbError);
socket.addEventListener('message', cbMessage);
ws.addEventListener('open', cbOpen);
ws.addEventListener('close', cbClose);
ws.addEventListener('error', cbError);
ws.addEventListener('message', cbMessage);
return () => {
if (socket) {
console.log('table - unbind');
socket.removeEventListener('open', cbOpen);
socket.removeEventListener('close', cbClose);
socket.removeEventListener('error', cbError);
socket.removeEventListener('message', cbMessage);
}
unbind();
ws.removeEventListener('open', cbOpen);
ws.removeEventListener('close', cbClose);
ws.removeEventListener('error', cbError);
ws.removeEventListener('message', cbMessage);
}
}, [ setWs, connecting, setConnecting, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]);
console.log(`Loaded: ${loaded}`);
return <GlobalContext.Provider value={{
ws, name, gameId, peers, setPeers }}>
return <GlobalContext.Provider value={{ ws: connecting, name, gameId, peers, setPeers }}>
<MediaAgent/>
<PingPong/>
<div className="Table">
{ error && <div className="ErrorDialog"><Paper className="Error">{ error }</Paper></div> }
<div className="Game">
{ error && <div className="ErrorDialog"><Paper className="Error">{ error }</Paper></div> }
{ warning && <div className="WarningDialog"><Paper className="Warning">{ warning }</Paper></div> }
<Board/>
{ color && state === 'game-order' &&
<GameOrder/>
}
{ /* state === 'winner' &&
<Winner color={winner}/>
}
{ state === 'normal' &&
turn && turn.actions && turn.actions.indexOf('trade') !== -1 &&
<Trade/>
}
{ cardActive &&
<ViewCard card={cardActive}/>
}
{ isTurn && turn && turn.actions && game.turn.actions.indexOf('select-resources') !== -1 &&
<ChooseCard type={turn.active}/>
}
{ game.state === 'normal' &&
turn && isTurn && turn.actions && turn.actions.indexOf('steal-resource') !== -1 &&
<SelectPlayer table={this} game={this.state} players={game.turn.limits.players}/>
*/ }
<div className="Hand">todo: player's hand</div>
</div>
<div className="Sidebar">
@ -248,7 +302,6 @@ const Table = () => {
};
const App = () => {
console.log(`Base: ${base}`);
return (
<Router>
<Routes>

View File

@ -2,6 +2,24 @@ import React, { useEffect, useState, useContext, useRef, useMemo } from "react";
import { assetsPath, debounce } from "./Common.js";
import "./Board.css";
import { GlobalContext } from "./GlobalContext.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 Board = () => {
const { ws } = useContext(GlobalContext);
@ -12,13 +30,14 @@ const Board = () => {
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 [ tiles, setTiles ] = useState([]);
const [ pips, setPips ] = useState();
const [ pipOrder, setPipOrder ] = useState();
const [ borders, setBorders ] = useState();
const [ borderOrder, setBorderOrder ] = useState();
const [ tiles, setTiles ] = useState();
const [ tileOrder, setTileOrder ] = useState([]);
const [ placements, setPlacements ] = useState(undefined);
const [ turn, setTurn ] = useState({});
@ -31,59 +50,120 @@ const Board = () => {
'placements', 'turn', 'state', 'color', 'longestRoadLength'
], []);
/* Placements is a structure of roads and corners arrays
* indicating what is stored at each of the locations
*
* Corners consist of a type and color
* Roads consist of a color, and longestRoad
*
* See games.js resetGame, placeRoad, placeSettlement, placeCity,
* and calculateRoadLengths
*
* Returns: true === differences, false === same
*/
const comparePlacements = (A, B) => {
if (!A && !B) {
return false; /* same */
}
if ((A && !B)
|| (!A && B)) {
return true;
}
if ((A.roads.length !== B.roads.length)
|| (A.corners.length !== B.corners.length)) {
return true;
}
/* Roads compare color and longestRoad */
for (let i = 0; i < A.roads.length; i++) {
if (A.roads[i].color !== B.roads[i].color) {
return true;
}
if (A.roads[i].longestRoad !== B.roads[i].longestRoad) {
return true;
}
}
/* Corners compare type and color */
for (let i = 0; i < A.corners.length; i++) {
if (A.corners[i].type !== B.corners[i].type) {
return true;
}
if (A.corners[i].color !== B.corners[i].color) {
return true;
}
}
return false; /* same */
};
const onWsMessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'game-update':
if (data.update.signature && data.update.signature !== signature) {
setSignature(data.update.signature);
}
if (data.update.robber && data.update.robber !== robber) {
console.log(`board - game update`, data.update);
if ('robber' in data.update && data.update.robber !== robber) {
setRobber(data.update.robber);
}
if (data.update.robberName && data.update.robberName !== robberName) {
if ('robberName' in data.update && data.update.robberName !== robberName) {
setRobberName(data.update.robberName);
}
if (data.update.pips) {
setPips(data.update.pips);
}
if (data.update.pipOrder) {
setPipOrder(data.update.pipOrder);
}
if (data.update.borders) {
setBorders(data.update.borders);
}
if (data.update.borderOrder) {
setBorderOrder(data.update.borderOrder);
}
if (data.update.tiles) {
setTiles(data.update.tiles);
}
if (data.update.tileOrder) {
setTileOrder(data.update.tileOrder);
}
if (data.update.state && data.update.state !== state) {
if ('state' in data.update && data.update.state !== state) {
setState(data.update.state);
}
if (data.update.color && data.update.color !== color) {
if ('color' in data.update && data.update.color !== color) {
setColor(data.update.color);
}
if (data.update.longestRoadLength
if ('longestRoadLength' in data.update
&& data.update.longestRoadLength !== longestRoadLength) {
setLongestRoadLength(data.update.longestRoadLength);
}
if (data.update.turn) {
if ('turn' in data.update) {
setTurn(data.update.turn);
}
if (data.update.placements) {
console.log(`placements`, data.update.placements);
setPlacements(data.update.placements);
if ('placement' in data.update) {
if (comparePlacements(data.update.placements, placements)) {
console.log(`placements`, data.update.placements);
setPlacements(data.update.placements);
}
}
if ('signature' in data.update && data.update.signature !== signature) {
setSignature(data.update.signature);
/* The following are only updated if there is a new game
* signature */
if ('pipOrder' in data.update) {
setPipOrder(data.update.pipOrder);
}
if ('borderOrder' in data.update) {
setBorderOrder(data.update.borderOrder);
}
if ('tileOrder' in data.update) {
setTileOrder(data.update.tileOrder);
}
}
/* This is permanent static data from the server -- do not update
* once set */
if ('pips' in data.update && !pips) {
setPips(data.update.pips);
}
if ('tiles' in data.update && !tiles) {
setTiles(data.update.tiles);
}
if ('borders' in data.update && !borders) {
setBorders(data.update.borders);
}
break;
default:
@ -135,9 +215,11 @@ const Board = () => {
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, refWsMessage]);
@ -152,23 +234,6 @@ const Board = () => {
}));
}, [ws, fields]);
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 Tile = ({tile}) => {
const onClick = (event) => {
console.log(`Tile clicked: ${tile.index}`);
@ -237,7 +302,7 @@ const Board = () => {
sendPlacement('place-robber', pip.index);
};
return <div className="Pip"
return <div className="Pip"
onClick={onClick}
data-roll={pip.roll}
data-index={pip.index}
@ -255,7 +320,17 @@ const Board = () => {
if (!signature) {
return;
}
const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */
if (signature === generated) {
return;
}
if (!pips || !pipOrder || !borders || !borderOrder
|| !tiles || !tileOrder) {
return;
}
console.log(`board - Generate board - ${signature}`);
const generateRoads = () => {
let row = 0, rowCount = 0;
@ -406,7 +481,6 @@ const Board = () => {
return pipOrder.map(order => {
pip = {
roll: pips[order].roll,
robber: index === robber,
index: index++,
top: y,
left: x,
@ -484,19 +558,18 @@ const Board = () => {
});
};
console.log(`Generate for ${signature}`);
setPipElements(generatePips());
setBorderElements(generateBorders());
setTileElements(generateTiles());
setCornerElements(generateCorners());
setRoadElements(generateRoads());
setGenerated(signature);
}, [
signature,
setPipElements, setBorderElements, setTileElements,
setCornerElements, setRoadElements,
borderImageWidth, radius, tileHalfHeight, tileHalfWidth, tileHeight,
borderImageHeight,
borderOrder, borders, pipOrder, pips, robber, tileOrder, tiles
borderOrder, borders, pipOrder, pips, tileOrder, tiles
]);
if (turn) {

View File

@ -1,6 +1,4 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import "./Chat.css";
import PlayerColor from './PlayerColor.js';
import Paper from '@material-ui/core/Paper';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
@ -9,8 +7,10 @@ import Moment from 'react-moment';
import TextField from '@material-ui/core/TextField';
import 'moment-timezone';
import Resource from './Resource.js';
import Dice from './Dice.js';
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 = () => {

View File

@ -18,6 +18,6 @@ const Dice = ({ pips }) => {
);
};
export default Dice;
export { Dice };

View File

@ -14,4 +14,4 @@ const PlayerColor = ({ color }) => {
);
};
export default PlayerColor;
export { PlayerColor };

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import "./PlayerList.css";
import PlayerColor from './PlayerColor.js';
import Paper from '@material-ui/core/Paper';
import List from '@material-ui/core/List';
import "./PlayerList.css";
import { PlayerColor } from './PlayerColor.js';
import { MediaControl } from "./MediaControl.js";
import { GlobalContext } from "./GlobalContext.js";
@ -18,9 +18,12 @@ const PlayerList = () => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'game-update':
console.log(`player-list - game update`, data.update);
if ('unselected' in data.update) {
setUneslected(data.update.unselected);
}
if ('players' in data.update) {
let found = false;
for (let key in data.update.players) {
@ -35,6 +38,7 @@ const PlayerList = () => {
}
setPlayers(data.update.players);
}
if ('state' in data.update && data.update.state !== state) {
setState(data.update.state);
}

View File

@ -39,4 +39,4 @@ const Resource = ({ type, select, disabled, available, count, label, onClick })
);
};
export default Resource;
export { Resource };

View File

@ -67,64 +67,6 @@ const StartButton = ({ table, game }) => {
);
};
const GameOrder = ({table, game}) => {
const rollClick = (event) => {
table.throwDice();
}
if (!game) {
return (<></>);
}
let players = [], hasRolled = true;
for (let color in game.players) {
const item = game.players[color],
name = getPlayerName(game.sessions, color);
if (!name) {
continue;
}
if (!item.orderRoll) {
item.orderRoll = 0;
}
if (color === game.color) {
hasRolled = item.orderRoll !== 0;
}
players.push({ name: name, color: color, ...item });
}
players.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;
});
players = players.map(item =>
<div className="GameOrderPlayer" key={`player-${item.color}`}>
<PlayerColor color={item.color}/>
<div>{item.name}</div>
{ item.orderRoll !== 0 && <>rolled <Dice pips={item.orderRoll}/>. {item.orderStatus}</> }
{ item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}</>}
</div>
);
return (
<div className="GameOrder">
{ game && <Paper>
<div className="Title">Game Order</div>
<div className="PlayerList">
{ players }
</div>
<Button disabled={hasRolled} onClick={rollClick}>Roll Dice</Button>
</Paper> }
</div>
);
};
const SelectPlayer = ({table, game, players}) => {
const playerClick = (event) => {
table.stealResource(event.currentTarget.getAttribute('data-color'));
@ -160,78 +102,6 @@ const SelectPlayer = ({table, game, players}) => {
);
};
const Action = ({ table, game }) => {
const buildClicked = (event) => {
table.buildClicked(event);
};
const discardClick = (event) => {
const nodes = document.querySelectorAll('.Hand .Resource.Selected'),
discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 };
for (let i = 0; i < nodes.length; i++) {
discarding[nodes[i].getAttribute("data-type")]++;
nodes[i].classList.remove('Selected');
}
return table.discard(discarding);
}
const newTableClick = (event) => {
return table.shuffleTable();
};
const tradeClick = (event) => {
table.startTrading();
}
const rollClick = (event) => {
table.throwDice();
}
const passClick = (event) => {
return table.passTurn();
}
/*
const quitClick = (event) => {
table.setSelected("");
}
*/
if (!game.id) {
console.log("Why no game id?");
return (<Paper className="Action"/>);
}
const inLobby = game.state === 'lobby',
inGame = game.state === 'normal',
player = game ? game.player : undefined,
hasRolled = (game && game.turn && game.turn.roll) ? true : false,
isTurn = (game && game.turn && game.turn.color === game.color) ? true : false,
robberActions = (game && game.turn && game.turn.robberInAction),
haveResources = player ? player.haveResources : false,
placement = (game.state === 'initial-placement' || game.turn.active === 'road-building'),
placeRoad = placement && game.turn && game.turn.actions && game.turn.actions.indexOf('place-road') !== -1;
return (
<Paper className="Action">
{ inLobby && <>
<StartButton table={table} game={game}/>
<Button disabled={game.color ? false : true} onClick={newTableClick}>New table</Button>
<Button disabled={game.color ? true : false} onClick={() => {table.setState({ pickName: true})}}>Change name</Button> </> }
{ !inLobby && <>
<Button disabled={robberActions || !isTurn || hasRolled || !inGame} onClick={rollClick}>Roll Dice</Button>
<Button disabled={placeRoad || robberActions || !isTurn || !hasRolled || !haveResources} onClick={tradeClick}>Trade</Button>
<Button disabled={placeRoad || robberActions || !isTurn || !hasRolled || !haveResources} onClick={buildClicked}>Build</Button>
{ game.turn.roll === 7 && player && player.mustDiscard > 0 &&
<Button onClick={discardClick}>Discard</Button>
}
<Button disabled={placeRoad || robberActions || !isTurn || !hasRolled} onClick={passClick}>Done</Button>
</> }
{ /* inLobby &&
<Button onClick={quitClick}>Quit</Button>
*/ }
</Paper>
);
}
const PlayerName = ({table, game}) => {
const [name, setName] = useState(game.name ? game.name : "");

View File

@ -350,6 +350,7 @@ const Trade = ({table}) => {
/* Order direction is reversed for self */
source = {
name: item.name,
color: item.color,
gets: trade.gives,
gives: trade.gets
};

File diff suppressed because it is too large Load Diff