1
0
James Ketrenos eb5f15f366 Set empty strings to "" instead of undefined
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-03-13 14:39:55 -07:00

817 lines
24 KiB
JavaScript
Executable File

import React, { useState } from "react";
import "./Table.css";
import history from "./history.js";
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import List from '@material-ui/core/List';
import Board from './Board.js';
import Trade from './Trade.js';
import PlayerColor from './PlayerColor.js';
import Dice from './Dice.js';
import Resource from './Resource.js';
import ViewCard from './ViewCard.js';
import Winner from './Winner.js';
import ChooseCard from './ChooseCard.js';
import Chat from './Chat.js';
import { CircularProgress } from "@material-ui/core";
import 'moment-timezone';
import Activities from './Activities.js';
import Placard from './Placard.js';
import PlayersStatus from './PlayersStatus.js';
import { MediaAgent, MediaControl, MediaContext } from './MediaControl.js';
import { base, assetsPath, getPlayerName, gamesPath } from './Common.js';
/* Start of withRouter polyfill */
// https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it
import {
useLocation,
useNavigate,
useParams
} from "react-router-dom";
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return (
<Component
{...props}
router={{ location, navigate, params }}
/>
);
}
return ComponentWithRouterProp;
}
/* end of withRouter polyfill */
const StartButton = ({ table, game }) => {
const startClick = (event) => {
table.setGameState("game-order");
};
return (
<Button disabled={!game.color || game.active < 2} onClick={startClick}>Start game</Button>
);
};
/* This needs to take in a mechanism to declare the
* player's active item in the game */
const Players = ({ table, game }) => {
const toggleSelected = (key) => {
table.setSelected(game.color === key ? "" : key);
}
const players = [];
if (!game.id) {
return (<></>);
}
for (let color in game.players) {
const item = game.players[color], inLobby = game.state === 'lobby';
if (!inLobby && item.status === 'Not active') {
continue;
}
let name = getPlayerName(game.sessions, color),
selectable = game.state === 'lobby' && (item.status === 'Not active' || game.color === color);
players.push((
<div
data-selectable={selectable}
data-selected={game.color === color}
className="PlayerEntry"
onClick={() => { inLobby && selectable && toggleSelected(color) }}
key={`player-${color}`}>
<PlayerColor color={color}/>{name ? name : 'Available' }
{ name && <MediaContext.Provider value={game.peers}>
<MediaControl peer={name} isSelf={game.color === color}/>
</MediaContext.Provider> }
{ !name && <div></div> }
</div>
));
}
return (
<Paper className="Players">
<List className="PlayerSelector">
{ players }
</List>
</Paper>
);
}
console.log("TODO: Convert this to a function component!!!!");
class Table extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "",
error: "",
signature: "",
buildActive: false,
cardActive: undefined,
loading: 0,
noNetwork: false,
ws: undefined,
peers: {}
};
this.componentDidMount = this.componentDidMount.bind(this);
this.throwDice = this.throwDice.bind(this);
this.rollDice = this.rollDice.bind(this);
this.setGameState = this.setGameState.bind(this);
this.shuffleTable = this.shuffleTable.bind(this);
this.startTrading = this.startTrading.bind(this);
this.offerTrade = this.offerTrade.bind(this);
this.acceptTrade = this.acceptTrade.bind(this);
this.rejectTrade = this.rejectTrade.bind(this);
this.cancelTrading = this.cancelTrading.bind(this);
this.discard = this.discard.bind(this);
this.passTurn = this.passTurn.bind(this);
this.updateGame = this.updateGame.bind(this);
this.setPlayerName = this.setPlayerName.bind(this);
this.setSelected = this.setSelected.bind(this);
this.updateMessage = this.updateMessage.bind(this);
this.sendAction = this.sendAction.bind(this);
this.buildClicked = this.buildClicked.bind(this);
this.closeCard = this.closeCard.bind(this);
this.playCard = this.playCard.bind(this);
this.selectResources = this.selectResources.bind(this);
this.buildItem = this.buildItem.bind(this);
this.loadTimer = null;
this.peers = {};
this.id = (props.router && props.router.params.id) ? props.router.params.id : 0;
this.setPeers = this.setPeers.bind(this);
}
setPeers(update) {
for (let key in this.peers) {
if (!(key in update)) {
delete this.peers[key];
}
}
this.setState({ peers: Object.assign({}, this.peers, update)});
}
closeCard() {
this.setState({cardActive: undefined});
}
sendAction(action, value, extra) {
if (this.loadTimer) {
window.clearTimeout(this.loadTimer);
this.loadTimer = null;
}
if (value === undefined || value === null) {
value = '';
}
this.setState({ loading: this.state.loading + 1 });
return window.fetch(`${base}/api/v1/games/${this.state.id}/${action}/${value}`, {
method: "PUT",
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: extra ? JSON.stringify(extra) : undefined
}).then((res) => {
if (res.status >= 400) {
throw new Error(`Unable to perform ${action}!`);
}
return res.json();
}).then((game) => {
const error = (game.status !== 'success') ? game.status : undefined;
this.updateGame(game);
this.updateMessage();
this.setError(error);
}).catch((error) => {
console.error(error);
this.setError(error.message);
}).then(() => {
this.setState({ loading: this.state.loading - 1 });
});
}
setSelected(key) {
return this.sendAction('player-selected', key);
}
sendChat(message) {
return this.sendAction('chat', undefined, {message: message});
}
selectResources(cards) {
return this.sendAction('select-resources', undefined, cards);
}
playCard(card) {
this.setState({ cardActive: undefined });
return this.sendAction('play-card', undefined, card);
}
setPlayerName(name) {
return this.sendAction('player-name', name)
.then(() => {
this.setState({ pickName: false });
});
}
shuffleTable() {
return this.sendAction('shuffle')
.then(() => {
this.setError("Table shuffled!");
});
}
startTrading() {
return this.sendAction('trade');
}
cancelTrading() {
return this.sendAction('trade', 'cancel');
}
offerTrade(trade) {
return this.sendAction('trade', 'offer', trade);
}
acceptTrade(trade) {
return this.sendAction('trade', 'accept', trade);
}
cancelTrade(trade) {
return this.sendAction('trade', 'cancel', trade);
}
rejectTrade(trade) {
return this.sendAction('trade', 'reject', trade);
}
discard(resources) {
return this.sendAction('discard', undefined, resources);
}
passTurn() {
return this.sendAction('pass');
};
rollDice() {
return this.sendAction('roll');
}
setError(error) {
if (!error) {
return;
}
if (this.errorTimeout) {
clearTimeout(this.errorTimeout);
}
setTimeout(() => { this.setState({error: undefined}) }, 3000);
if (this.state.error !== error) {
this.setState({ error });
}
}
setGameState(state) {
if (this.loadTimer) {
window.clearTimeout(this.loadTimer);
this.loadTimer = null;
}
this.setState({ loading: this.state.loading + 1 });
return window.fetch(`${base}/api/v1/games/${this.state.id}/state/${state}`, {
method: "PUT",
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
}).then((res) => {
if (res.status >= 400) {
console.log(res);
throw new Error(`Unable to set state to ${state}`);
}
return res.json();
}).then((game) => {
console.log (`Game state set to ${game.state}!`);
this.updateGame(game);
this.updateMessage();
}).catch((error) => {
console.error(error);
this.setError(error.message);
}).then(() => {
this.setState({ loading: this.state.loading - 1 });
return this.state.state;
});
}
buildClicked(event) {
console.log("Build clicked");
this.setState({ buildActive: this.state.buildActive ? false : true });
};
placeRobber(robber) {
return this.sendAction('place-robber', robber);
};
buyDevelopment() {
return this.sendAction('buy-development');
}
buySettlement() {
return this.sendAction('buy-settlement');
}
placeSettlement(settlement) {
return this.sendAction('place-settlement', settlement);
}
buyCity() {
return this.sendAction('buy-city');
}
placeCity(city) {
return this.sendAction('place-city', city);
}
buyRoad() {
return this.sendAction('buy-road');
}
placeRoad(road) {
return this.sendAction('place-road', road);
}
stealResource(color) {
return this.sendAction('steal-resource', color);
}
throwDice() {
return this.rollDice();
}
updateGame(game) {
if (this.state.signature !== game.signature) {
this.setState( { signature: game.signature });
}
console.log("Update Game", game);
/* Only update fields that are changing */
for (let key in game) {
if (game[key] === this.state[key]) {
delete game[key];
}
}
console.log(`Updating: `, { ...game });
this.setState( { ...game } );
}
updateMessage() {
const player = (this.state.id && this.state.color) ? this.state.players[this.state.color] : undefined,
name = this.state ? this.state.name : "";
let message = <></>;
if (this.state.pickName || !name) {
message = <>{message}Enter the name you would like to be known by, then press&nbsp;<b>ENTER</b>&nbsp;or select &nbsp;<b>SET</b>.</>;
} else {
switch (this.state.state) {
case 'lobby':
message = <>{message}You are in the lobby as&nbsp;<b>{name}</b>.</>;
if (!this.state.color) {
message = <>{message}You select one of the <b>Available</b> colors below.</>;
} else {
message = <>{message}You have selected <PlayerColor color={this.state.color}/>.</>;
}
message = <>{message}You can chat with other players below.</>;
if (this.state.active < 2) {
message = <>{message}Once there are two or more players, you can select <StartButton table={this} game={this.state}/>.</>;
} else {
message = <>{message}There are enough players to start the game. Select <StartButton table={this} game={this.state}/> when ready.</>;
}
break;
case 'game-order':
if (!player) {
message = <>{message}You are an observer in this game as &nbsp;<b>{name}</b>.</>;
message = <>{message}You can chat with other players below as&nbsp;<b>{this.state.name}</b>, but cannot play unless players go back to the Lobby.</>;
} else {
if (!player.order) {
message = <>{message}You need to roll for game order. Click&nbsp;<b>Roll Dice</b>&nbsp;below.</>;
} else {
message = <>{message}You rolled <Dice pips={player.order}/> for game order. Waiting for all players to roll.</>;
}
}
break;
case 'initial-placement':
message = <>{message}It is time for all the players to place their initial two settlements, with one road connected to each settlement.</>;
break;
case 'active':
if (!player) {
message = <>{message}This game is no longer in the lobby.<br/><b>TODO: Override game state to allow Lobby mode while in-game</b></>;
}
break;
case null:
case undefined:
case '':
message = <>{message}The game is in a wonky state. Sorry :(</>;
break;
case 'normal':
if (this.state.turn) {
if (this.state.turn.roll === 7) {
message = <>{message}Robber was rolled!</>;
let move = true;
for (let color in this.state.players) {
let name = '';
for (let i = 0; i < this.state.sessions.length; i++) {
if (this.state.sessions[i].color === color) {
name = this.state.sessions[i].name;
}
}
const discard = this.state.players[color].mustDiscard;
if (discard) {
move = false;
message = <>{message}<PlayerColor color={color}/> {name} needs to discard {discard} resources.</>;
}
}
if (move && (this.state.turn && !this.state.turn.placedRobber)) {
message = <>{message}<PlayerColor color={this.state.turn.color}/> {this.state.turn.name} needs to move the robber.</>
}
} else {
message = <>It is <PlayerColor color={this.state.turn.color}/> {this.state.turn.name}'s turn.</>;
}
}
break;
default:
message = <>{message}Game state is: {this.state.state}</>;
break;
}
}
this.setState({ message: message });
}
resetKeepAlive(isDead) {
if (isDead) {
console.log(`Short circuiting keep-alive`);
if (this.ws) {
this.ws.close();
delete this.ws;
}
} else {
// console.log(`${this.game.name} Resetting keep-alive. Last ping: ${(Date.now() - this.lastPing) / 1000}`);
}
if (this.keepAlive) {
clearTimeout(this.keepAlive);
this.keepAlive = 0;
} else {
console.log(`No keep-alive active`);
}
this.keepAlive = setTimeout(() => {
console.log(`${this.state.name} No ping after 10 seconds. Last ping: ${(Date.now() - this.lastPing) / 1000}`);
this.setState({ noNetwork: true });
if (this.ws) {
this.ws.close();
delete this.ws;
}
this.connectWebSocket();
}, isDead ? 3000 : 10000);
if (this.state.noNetwork !== false && !isDead) {
this.setState({ noNetwork: false });
} else if (this.state.noNetwork !== true && isDead) {
this.setState({ noNetwork: true });
}
}
connectWebSocket() {
if (!this.state.id) {
console.log(`Cannot initiate websocket connection while no game is set.`);
this.resetKeepAlive(true);
return;
}
if (this.ws) {
return;
}
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/${this.state.id}/`;
console.log(`Attempting WebSocket connection to ${new_uri}`);
this.ws = new WebSocket(new_uri);
this.setState({ ws: this.ws });
this.lastPing = this.state.timestamp;
this.ws.addEventListener('message', (event) => {
this.resetKeepAlive();
let data;
try {
data = JSON.parse(event.data);
} catch (error) {
this.setError(error);
return;
}
let update;
switch (data.type) {
case 'game-update':
console.log(`Game update received`);
update = data.update;
const error = (update.status !== 'success') ? update.status : undefined;
this.updateGame(update);
this.updateMessage();
this.setError(error);
break;
case 'ping':
this.lastPing = data.ping;
this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping }));
break;
default:
break;
}
});
this.ws.addEventListener('error', (event) => {
this.setState({ error: event.message });
console.error(`${this.state.name} WebSocket error: ${(Date.now() - this.state.lastPing) / 1000}`);
this.resetKeepAlive(true);
});
this.ws.addEventListener('close', (event) => {
console.log(`${this.state.name} WebSocket close: ${(Date.now() - this.state.lastPing) / 1000}`);
this.setState({ error: event.message });
this.resetKeepAlive(true);
});
this.ws.addEventListener('open', (event) => {
console.log(`${this.state.name} WebSocket open: Sending game-update request: ${(Date.now() - this.lastPing) / 1000}`);
this.ws.send(JSON.stringify({ type: 'game-update' }));
this.resetKeepAlive();
});
}
componentDidMount() {
this.start = new Date();
console.log(`Mounted: ${base}`);
const params = {};
if (this.id) {
console.log(`Loading game: ${this.id}`);
params.url = `${base}/api/v1/games/${this.id}`;
params.method = "GET"
} else {
console.log("Requesting new game.");
params.url = `${base}/api/v1/games/`;
params.method = "POST";
}
this.setState({ loading: this.state.loading + 1 });
window.fetch(params.url, {
method: params.method,
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
// body: JSON.stringify(data) // body data type must match "Content-Type" header
}).then((res) => {
if (res.status < 400) {
return res;
}
let error;
if (!this.id) {
error = `Unable to create new game.`;
throw new Error(error);
}
error = `Unable to find game ${this.id}. Starting new game.`
console.log(error);
this.setError(error);
params.url = `${base}/api/v1/games/${this.id}`;
params.method = "POST";
return window.fetch(params.url, {
method: params.method,
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
});
}).then((res) => {
return res.json();
}).then((game) => {
if (!this.id) {
history.push(`${gamesPath}/${game.id}`);
}
this.id = game.id;
this.updateGame(game);
/* Connect to the WebSocket (after the game is setup) */
this.connectWebSocket();
this.updateMessage();
this.setState({ error: "" });
}).catch((error) => {
console.error(error);
this.setError(error.message);
}).then(() => {
this.setState({ loading: this.state.loading - 1 });
});
}
buildItem(item) {
return this.sendAction(`buy-${item}`);
}
componentWillUnmount() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
if (this.loadTimer) {
clearTimeout(this.loadTimer);
this.loadTimer = 0;
}
if (this.keepAlive) {
clearTimeout(this.keepAlive);
this.keepAlive = 0;
}
if (this.updateSizeTimer) {
clearTimeout(this.updateSizeTimer);
this.updateSizeTimer = 0;
}
if (this.errorTimeout) {
clearTimeout(this.errorTimeout);
this.errorTimeout = 0;
}
}
cardClicked(card) {
this.setState({cardActive: card });
}
render() {
const game = this.state,
player = game ? game.player : undefined,
isTurn = (game && game.turn && game.turn.color === game.color) ? true : false,
showMessage = (game && (game.state === 'lobby' || !game.name));
let color;
switch (game ? game.color : undefined) {
case "O": color = "orange"; break;
case "R": color = "red"; break;
case "B": color = "blue"; break;
default: case "W": color = "white"; break;
}
let development;
if (player) {
let stacks = {};
game.player.development.forEach(card =>
(card.type in stacks)
? stacks[card.type].push(card)
: stacks[card.type] = [card]);
development = [];
for (let type in stacks) {
const cards = stacks[type]
.sort((A, B) => {
if (A.played) {
return -1;
}
if (B.played) {
return +1;
}
return 0;
}).map(card => <Development
onClick={() => this.cardClicked(card)}
card={card}
table={this}
key={`${type}-${card.card}`}
type={`${type}-${card.card}`}/>);
development.push(<div key={type} className="Stack">{ cards }</div>);
}
} else {
development = <>/</>;
}
if (!this.state.id) {
return <></>;
}
return (<>
<MediaContext.Provider value={{peers: this.peers, setPeers: this.setPeers}}>
<MediaAgent ws={this.state.ws} name={this.state.name}/>
</MediaContext.Provider>
<div className="Table">
{ this.state.loading > 0 && <CircularProgress className='Loading'/> }
{ this.state.noNetwork && <div className='NoNetwork'/> }
<Activities table={this.state}/>
<div style={{display: "inline-flex", flex: 1, flexDirection: "column"}}>
<Board table={this} game={this.state}/>
{ player !== undefined && <>
<PlayersStatus table={this} game={this.state}/>
<PlayersStatus active={true}
onClick={this.buildItem}
table={this}
game={this.state}
color={this.state.color}/>
<div className="BottomBar">
<div className="Hand">
<Resource type="wood" count={player.wood}/>
<Resource type="wheat" count={player.wheat}/>
<Resource type="stone" count={player.stone}/>
<Resource type="brick" count={player.brick}/>
<Resource type="sheep" count={player.sheep}/>
</div>
<div className="Hand">
{ development }
</div>
{ game.longestRoad && game.longestRoad === game.color &&
<Placard
active={false}
type='longest-road'
table={this}
/>
}
{ game.largestArmy && game.largestArmy === game.color &&
<Placard
active={false}
type='largest-army'
table={this}
/>
}
<Placard
active={this.state.buildActive}
disabled={!game || !game.turn || !game.turn.roll}
table={this} type={`${color}`}/>
</div>
</> }
{ player === undefined && <div className="BottomBar"></div>}
</div>
{ game && <div className={'Game ' + game.state}>
{ showMessage && <Paper className="Message">{ this.state.message }</Paper> }
{(this.state.pickName || !game.name) && <PlayerName table={this} game={this.state}/> }
{(!this.state.pickName && game.name) && <>
<Players table={this} game={this.state}/>
<Chat table={this} game={this.state}/>
<Action table={this} game={this.state}/>
</> }
</div> }
{ game && game.state === 'winner' &&
<Winner table={this} color={game.winner}/>
}
{ this.state.cardActive &&
<ViewCard table={this} card={this.state.cardActive}/>
}
{ game && game.state === 'game-order' &&
<GameOrder table={this}/>
}
{ game && game.state === 'normal' &&
game.turn.actions && game.turn.actions.indexOf('trade') !== -1 &&
<Trade table={this}/>
}
{ game
&& isTurn
&& game.turn.actions
&& game.turn.actions.indexOf('select-resources') !== -1 &&
<ChooseCard table={this} type={game.turn.active}/>
}
{ game && game.state === 'normal' &&
game.turn &&
isTurn &&
game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1 &&
<SelectPlayer table={this} game={this.state} players={game.turn.limits.players}/>
}
{ this.state.error && <Paper onClick={() => this.setState({ error: undefined })} className="Error"><div>{this.state.error}</div></Paper> }
</div>
</>);
}
}
export default withRouter(props => <Table {...props}/>);