1
0

Almost done!

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-02-19 16:59:49 -08:00
parent 8cb3efc70f
commit dc2d97196e
10 changed files with 559 additions and 111 deletions

View File

@ -175,7 +175,7 @@
[data-color='O'] > .Corner-Shape,
[data-color='O'] > .Road-Shape {
background-color: rgba(255, 196, 0, 1);
background-color: rgb(255, 128, 0);
}
[data-color='W'] > .Corner-Shape,

View File

@ -70,7 +70,7 @@ const Board = ({ table, game }) => {
const Corner = ({corner}) => {
const onClick = (event) => {
console.log(`Corner ${corner.index}:`, game.layout.corners[corner.index]);
// console.log(`Corner ${corner.index}:`, game.layout.corners[corner.index]);
if (event.currentTarget.getAttribute('data-type') === 'settlement') {
table.placeCity(corner.index);
} else {
@ -91,7 +91,7 @@ const Board = ({ table, game }) => {
const Pip = ({pip}) => {
const onClick = (event) => {
console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]);
// console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]);
table.placeRobber(pip.index);
return;
};
@ -377,14 +377,23 @@ const Board = ({ table, game }) => {
}
}
if (game && game.turn && game.turn.roll) {
let nodes = document.querySelectorAll('.Pip.Active');
if (game && game.turn) {
let nodes = document.querySelectorAll('.Active');
for (let i = 0; i < nodes.length; i++) {
nodes[i].classList.remove('Active');
}
nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`);
for (let i = 0; i < nodes.length; i++) {
nodes[i].classList.add('Active');
if (game.turn.roll) {
nodes = document.querySelectorAll(`.Pip[data-roll="${game.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');
}
}
}

View File

@ -1,12 +1,13 @@
.Resource {
position: relative;
width: 4.9em;
height: 7.2em;
height: 7em;
width: 5em;
display: inline-block;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
margin: 0.25em;
cursor: pointer;
}
.Resource:hover {

View File

@ -196,6 +196,11 @@
filter: brightness(150%);
}
.Development.Selected {
filter: brightness(150%);
top: -1em;
}
.Game {
display: flex;
position: absolute;
@ -219,9 +224,6 @@
}
.Game.lobby {
max-width: 100vw;
width: 100vw;
position: absolute;
}
/*
@ -366,7 +368,9 @@
display: flex;
flex-direction: column;
justify-items: space-between;
cursor: pointer;
}
.Placard > div {
box-sizing: border-box;
margin: 0 0.9em;
@ -407,6 +411,7 @@
background-repeat: no-repeat;
background-size: cover;
margin: 0.25em;
cursor: pointer;
}
.Action {

View File

@ -14,8 +14,7 @@ import { assetsPath, base, getPlayerName, gamesPath } from './Common.js';
import PlayerColor from './PlayerColor.js';
import Dice from './Dice.js';
import Resource from './Resource.js';
//import moment from 'moment';
import ViewCard from './ViewCard.js';
/* Start of withRouter polyfill */
// https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it
@ -106,9 +105,10 @@ const Placard = ({table, type, active}) => {
);
};
const Development = ({table, type}) => {
const Development = ({table, type, card, onClick}) => {
return (
<div className="Development"
<div className={`Development ${card.played ? 'Selected' : ''}`}
onClick={onClick}
style={{
backgroundImage:`url(${assetsPath}/gfx/card-${type}.png)`
}}/>
@ -293,7 +293,7 @@ const GameOrder = ({table}) => {
<div className="GameOrderPlayer" key={`player-${item.color}`}>
<PlayerColor color={item.color}/>
<div>{item.name}</div>
{ item.orderRoll !== 0 && <>rolled <Dice pips={item.orderRoll}/>.</> }
{ item.orderRoll !== 0 && <>rolled <Dice pips={item.orderRoll}/>. {item.orderStatus}</> }
{ item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}</>}
</div>
);
@ -352,7 +352,7 @@ const Action = ({ table }) => {
};
const discardClick = (event) => {
const nodes = document.querySelectorAll('.Hand .Selected'),
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")]++;
@ -386,23 +386,26 @@ const Action = ({ table }) => {
return (<Paper className="Action"/>);
}
const inLobby = table.game.state === 'lobby',
player = table.game ? table.game.player : undefined,
hasRolled = (table.game && table.game.turn && table.game.turn.roll) ? true : false,
isTurn = (table.game && table.game.turn && table.game.turn.color === table.game.color) ? true : false,
robberActions = (table.game && table.game.turn && table.game.turn.roll === 7 && !table.game.turn.robberDone);
const game = table.game,
inLobby = game.state === 'lobby',
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.roll === 7 &&
!game.turn.robberDone),
haveResources = player ? player.haveResources : false;
return (
<Paper className="Action">
{ inLobby && <>
<StartButton table={table}/>
<Button disabled={table.game.color ? false : true} onClick={newTableClick}>New table</Button>
<Button disabled={table.game.color ? true : false} onClick={() => {table.setState({ pickName: true})}}>Change name</Button> </> }
{ table.game.state === 'normal' && <>
<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> </> }
{ game.state === 'normal' && <>
<Button disabled={robberActions || !isTurn || hasRolled} onClick={rollClick}>Roll Dice</Button>
<Button disabled={robberActions || !isTurn || !hasRolled} onClick={tradeClick}>Trade</Button>
<Button disabled={robberActions || !isTurn || !hasRolled} onClick={buildClicked}>Build</Button>
{ table.game.turn.roll === 7 && player && player.mustDiscard > 0 &&
<Button disabled={robberActions || !isTurn || !hasRolled || !haveResources} onClick={tradeClick}>Trade</Button>
<Button disabled={robberActions || !isTurn || !hasRolled || !haveResources} onClick={buildClicked}>Build</Button>
{ game.turn.roll === 7 && player && player.mustDiscard > 0 &&
<Button onClick={discardClick}>Discard</Button>
}
<Button disabled={robberActions || !isTurn || !hasRolled} onClick={passClick}>Done</Button>
@ -468,7 +471,12 @@ const Players = ({ table }) => {
}
const name = getPlayerName(table.game.sessions, color),
selectable = table.game.state === 'lobby' && (item.status === 'Not active' || table.game.color === color);
let toggleText = name ? name : "Available";
let toggleText;
if (name) {
toggleText = `${name} has ${item.points} VP`;
} else {
toggleText = "Available";
}
players.push((
<div
data-selectable={selectable}
@ -511,7 +519,8 @@ class Table extends React.Component {
message: "",
error: "",
signature: "",
buildActive: false
buildActive: false,
cardActive: undefined
};
this.componentDidMount = this.componentDidMount.bind(this);
this.updateDimensions = this.updateDimensions.bind(this);
@ -524,6 +533,7 @@ class Table extends React.Component {
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);
@ -534,6 +544,8 @@ class Table extends React.Component {
this.gameSignature = this.gameSignature.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.mouse = { x: 0, y: 0 };
this.radius = 0.317;
@ -556,6 +568,10 @@ class Table extends React.Component {
this.id = (props.router && props.router.params.id) ? props.router.params.id : 0;
}
closeCard() {
this.setState({cardActive: undefined});
}
sendAction(action, value, extra) {
if (this.loadTimer) {
window.clearTimeout(this.loadTimer);
@ -596,6 +612,11 @@ class Table extends React.Component {
return this.sendAction('chat', undefined, {message: message});
}
playCard(card) {
this.setState({ cardActive: undefined });
return this.sendAction('play-card', undefined, card);
}
setPlayerName(name) {
return this.sendAction('player-name', name)
.then(() => {
@ -626,6 +647,10 @@ class Table extends React.Component {
return this.sendAction('trade', 'accept', trade);
}
rejectTrade(trade) {
return this.sendAction('trade', 'reject', trade);
}
discard(resources) {
return this.sendAction('discard', undefined, resources);
}
@ -833,7 +858,7 @@ class Table extends React.Component {
break;
case 'game-order':
if (!player) {
message = <>{message}This game as an observer as &nbsp;<b>{name}</b>.</>;
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.game.name}</b>, but cannot play unless players go back to the Lobby.</>;
} else {
if (!player.order) {
@ -967,6 +992,14 @@ class Table extends React.Component {
}
}
cardClicked(card) {
const game = this.state.game;
if (!game) {
return;
}
this.setState({cardActive: card });
}
render() {
const game = this.state.game,
player = game ? game.player : undefined
@ -980,15 +1013,25 @@ class Table extends React.Component {
let development;
if (player) {
let stacks = {};
game.player.development.forEach(item => (item.type in stacks) ? stacks[item.type].push(item.card) : stacks[item.type] = [item.card]);
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].map(card => <Development table={this} key={`${type}-${card}`} type={`${type}-${card}`}/>);
const cards = stacks[type].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 = <>/</>;
}
return (
<div className="Table">
@ -1030,6 +1073,10 @@ class Table extends React.Component {
</> }
</div> }
{ this.state.cardActive &&
<ViewCard table={this} card={this.state.cardActive}/>
}
{ game && game.state === 'game-order' &&
<GameOrder table={this}/>
}

View File

@ -22,6 +22,7 @@
background-color:rgba(224, 224, 224);
margin: 0.5em 0;
}
.Trade > * {
min-width: 40em;
display: inline-flex;
@ -29,6 +30,11 @@
flex-direction: column;
}
.Trade .Resource {
width: 3.75em; /* 5x7 aspect ratio */
height: 5.25em;
}
.Trade .PlayerColor {
width: 0.5em;
height: 0.5em;

View File

@ -6,14 +6,13 @@ import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
import Resource from './Resource.js';
const ResourceCounter = ({type, onCount, max}) => {
const [count, setCount] = useState(0);
const ResourceCounter = ({type, count, onCount, max}) => {
count = count ? count : 0;
const plusClicked = (event) => {
if (max === undefined || max > count) {
if (onCount) {
onCount(type, count+1);
}
setCount(count+1);
}
};
const minusClicked = (event) => {
@ -21,7 +20,6 @@ const ResourceCounter = ({type, onCount, max}) => {
if (onCount) {
onCount(type, count-1);
}
setCount(count-1);
}
};
@ -77,7 +75,7 @@ const Trade = ({table}) => {
} else {
setGiveLine(items.join(', '));
}
}, [setGiveLine, setGives]);
}, [setGiveLine, setGives, gives]);
const getCount = useCallback((type, count) => {
gets[type] = count;
@ -94,12 +92,25 @@ const Trade = ({table}) => {
} else {
setGetLine(items.join(', '));
}
}, [setGetLine, setGets]);
}, [setGetLine, setGets, gets]);
const meetClicked = useCallback((offer) => {
const trade = {
gives: offer.gets.slice(),
gets: offer.gives.slice()
};
console.log(trade);
trade.gives.forEach(give => giveCount(give.type, give.count));
trade.gets.forEach(get => getCount(get.type, get.count));
table.offerTrade(trade);
}, [giveCount, getCount]);
if (!table.game) {
return (<></>);
}
const game = table.game;
const isTurn = (table.game.turn && table.game.turn.color === table.game.color) ? true : false;
const offerClicked = (event) => {
@ -124,6 +135,12 @@ const Trade = ({table}) => {
table.cancelTrading();
}
/* Non-current player has rejected the active player's
* bid */
const rejectClicked = (trade) => {
table.rejectTrade(trade);
}
let players = [];
for (let color in table.game.players) {
const item = table.game.players[color],
@ -134,7 +151,8 @@ const Trade = ({table}) => {
color: color,
valid: false,
gets: item.gets ? item.gets : [],
gives: item.gives ? item.gives : []
gives: item.gives ? item.gives : [],
offerRejected: item.offerRejected ? true : false
});
}
}
@ -164,6 +182,13 @@ const Trade = ({table}) => {
});
}
const player = (table.game && table.game.player) ? table.game.player : undefined;
if (!player) {
return <></>;
}
let canAccept = false;
if (table.game.turn.offer) {
players.forEach(trade => {
let valid = trade.gets.length && trade.gives.length;
@ -187,9 +212,29 @@ const Trade = ({table}) => {
});
trade.valid = valid;
});
canAccept = true;
table.game.turn.offer.gets.forEach(item => {
if (!canAccept) {
canAccept = (item.type in game.player);
}
if (!canAccept) {
return;
}
canAccept = (game.player[item.type] >= item.count);
});
}
players = players.map((item, index) => {
if (item.offerRejected) {
return <div className="TradePlayer" key={`player-${item.name}-${index}`}>
<PlayerColor color={item.color}/>
<div>{item.name}</div>
<div className='TradeLine'>
has rejected your offer.
</div>
</div>;
}
const gets = item.gets.map(get =>
`${get.count} ${(get.type === 'bank') ? 'of any one resource' : get.type}`)
.join(', '),
@ -211,16 +256,18 @@ const Trade = ({table}) => {
}
{ isTurn && <Button disabled={!item.valid}
onClick={() => acceptClicked(item)}>accept</Button> }
{ !isTurn && item.color === table.game.turn.color && <>
<Button disabled={!canAccept}
onClick={() => meetClicked(item)}>meet</Button>
<Button disabled={!item.gets.length ||
!item.gives.length || player.offerRejected}
onClick={() => rejectClicked(item)}>reject</Button>
</> }
</div>
</div>
);
});
const player = (table.game && table.game.player) ? table.game.player : undefined;
if (!player) {
return <></>;
}
return (
<div className="Trade">
<Paper>
@ -230,32 +277,35 @@ const Trade = ({table}) => {
<div className="PlayerList">
{ players }
</div>
<div className='TradeLine'
style={{
flexDirection: 'column',
alignItems: 'flex-start'
}}>
<div style={{display: 'flex' }}>
<b>You want to receive {getLine}.</b>
{ !player.haveResources && <b>You have no resources to participate in this trade.</b> }
{ player.haveResources &&
<div className='TradeLine'
style={{
flexDirection: 'column',
alignItems: 'flex-start'
}}>
<div style={{display: 'flex' }}>
<b>You want to receive {getLine}.</b>
</div>
<div style={{display: 'flex' }}>
<ResourceCounter count={gets.brick} onCount={getCount} type='brick'/>
<ResourceCounter count={gets.wood} onCount={getCount} type='wood'/>
<ResourceCounter count={gets.wheat} onCount={getCount} type='wheat'/>
<ResourceCounter count={gets.sheep} onCount={getCount} type='sheep'/>
<ResourceCounter count={gets.stone} onCount={getCount} type='stone'/>
</div>
<div style={{display: 'flex' }}>
<b>You are willing to give {giveLine}.</b>
</div>
<div style={{display: 'flex' }}>
{ player.brick > 0 && <ResourceCounter count={gives.brick} onCount={giveCount} type='brick' max={player.brick}/> }
{ player.wood > 0 && <ResourceCounter count={gives.wood} onCount={giveCount} type='wood' max={player.wood}/> }
{ player.wheat > 0 && <ResourceCounter count={gives.wheat} onCount={giveCount} type='wheat' max={player.wheat}/> }
{ player.sheep > 0 && <ResourceCounter count={gives.sheep} onCount={giveCount} type='sheep' max={player.sheep}/> }
{ player.stone > 0 && <ResourceCounter count={gives.stone} onCount={giveCount} type='stone' max={player.stone}/> }
</div>
</div>
<div style={{display: 'flex' }}>
<ResourceCounter onCount={getCount} type='brick'/>
<ResourceCounter onCount={getCount} type='wood'/>
<ResourceCounter onCount={getCount} type='wheat'/>
<ResourceCounter onCount={getCount} type='sheep'/>
<ResourceCounter onCount={getCount} type='stone'/>
</div>
<div style={{display: 'flex' }}>
<b>You are willing to give {giveLine}.</b>
</div>
<div style={{display: 'flex' }}>
{ player.brick > 0 && <ResourceCounter onCount={giveCount} type='brick' max={player.brick}/> }
{ player.wood > 0 && <ResourceCounter onCount={giveCount} type='wood' max={player.wood}/> }
{ player.wheat > 0 && <ResourceCounter onCount={giveCount} type='wheat' max={player.wheat}/> }
{ player.sheep > 0 && <ResourceCounter onCount={giveCount} type='sheep' max={player.sheep}/> }
{ player.stone > 0 && <ResourceCounter onCount={giveCount} type='stone' max={player.stone}/> }
</div>
</div>
}
<Button disabled={getLine === 'nothing' || giveLine === 'nothing'}
onClick={offerClicked}>Offer</Button>
{ isTurn && <Button onClick={cancelClicked}>cancel</Button> }

37
client/src/ViewCard.css Normal file
View File

@ -0,0 +1,37 @@
.ViewCard {
display: flex;
position: absolute;
left: 0;
right: 40vw;
bottom: 0;
top: 0;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.ViewCard .Title {
align-self: center;
padding: 2px;
font-weight: bold;
margin-bottom: 0.5em;
}
.ViewCard .Description {
padding: 1em;
max-width: 20vw;
box-sizing: border-box;
}
.ViewCard > * {
/* min-width: 40em;*/
display: inline-flex;
padding: 0.5em;
flex-direction: column;
}
.ViewCard .Resource {
width: 10em; /* 5x7 aspect ratio */
height: 14em;
}

83
client/src/ViewCard.js Normal file
View File

@ -0,0 +1,83 @@
import React, { useState, useCallback } from "react";
import "./ViewCard.css";
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
import Resource from './Resource.js';
const ViewCard = ({table, card}) => {
const playCard = (event) => {
table.playCard(card);
}
const close = (event) => {
table.closeCard();
};
const capitalize = (string) => {
if (string === 'vp') {
return 'Victory Point';
}
if (string === 'army') {
return 'Knight';
}
return string.charAt(0).toUpperCase() + string.slice(1);
};
const descriptions = {
army: <>When played, you <b>must</b> move the robber.
<p>Steal <b>1</b> resource card from the owner of an adjacent settlement or city.</p>
<p>You may only play one development card during your turn -- either one
knight or one progress card.</p></>,
vp: <><b>1</b> victory point.
<p>You only reveal your victory point cards when the game is over, either
when you or an opponent reaches <b>10+</b> victory points on their turn and declares
victory!</p></>
};
let description = descriptions[card.type];
let canPlay = false;
if (card.type === 'vp') {
let points = table.game.player.points;
table.game.player.development.forEach(item => {
if (item.type === 'vp') {
points++;
}
});
canPlay = points >= 10;
if (!canPlay && !card.played) {
description = <>{description}<p>You do not have enough victory points to play this card yet.</p></>;
}
} else {
canPlay = card.turn < table.game.turns;
if (!canPlay) {
description = <>{description}<p>You can not play this card until your next turn.</p></>;
}
if (canPlay) {
canPlay = table.game.player.playedCard !== table.game.turns;
}
}
if (card.played) {
description = <>{description}<p>You have already played this card.</p></>;
}
return (
<div className="ViewCard">
<Paper>
<div className="Title">{capitalize(card.type)}</div>
<div style={{display: 'flex', flexDirection: 'row'}}>
<Resource type={`${card.type}-${card.card}`} disabled count={1}/>
<div className="Description">{description}</div>
</div>
{ !card.played &&
<Button disabled={!canPlay}
onClick={playCard}>play</Button>
}
<Button onClick={close}>close</Button>
</Paper>
</div>
);
};
export default ViewCard;

View File

@ -10,6 +10,10 @@ const express = require("express"),
const { corners } = require("./layout.js");
const layout = require('./layout.js');
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
let gameDB;
require("../db/games").then(function(db) {
@ -115,14 +119,13 @@ const processTies = (players) => {
if (A.order === B.order) {
return B.orderRoll - A.orderRoll;
}
return A.order - B.order;
return B.order - A.order;
});
/* Sort the players into buckets based on their
* order, and their current roll. If a resulting
* roll array has more than one element, then there
* is a tie that must be resolved */
let slots = [];
players.forEach(player => {
if (!slots[player.order]) {
@ -135,14 +138,15 @@ const processTies = (players) => {
});
let ties = false, order = 0;
slots.forEach((slot) => {
/* Reverse from high to low */
slots.reverse().forEach((slot) => {
slot.forEach(pips => {
if (pips.length !== 1) {
ties = true;
pips.forEach(player => {
player.orderRoll = 0;
player.order = order;
player.orderStatus = `Tied for ${order+1}.`;
player.orderStatus = `Tied.`;
});
} else {
pips[0].order = order;
@ -258,7 +262,7 @@ const roll = (game, session) => {
break;
}
if (player.order || player.orderRoll) {
if (player.order && player.orderRoll) {
error = `Player ${name} has already rolled for player order.`;
break;
}
@ -402,9 +406,9 @@ const processRoll = (game, dice) => {
const getPlayer = (game, color) => {
if (!game) {
return {
roads: 15,
cities: 4,
settlements: 5,
roads: MAX_ROADS,
cities: MAX_CITIES,
settlements: MAX_SETTLEMENTS,
points: 0,
status: "Not active",
lastActive: 0,
@ -414,6 +418,7 @@ const getPlayer = (game, color) => {
sheep: 0,
wood: 0,
brick: 0,
army: 0,
development: []
};
}
@ -459,17 +464,34 @@ const loadGame = async (id) => {
return;
});
if (!game) {
game = createGame(id);
} else {
if (game) {
try {
game = JSON.parse(game);
console.log(`Creating backup of games/${id}`);
await writeFile(`games/${id}.bk`, JSON.stringify(game));
} catch (error) {
console.error(error, game);
return null;
console.log(`Attempting to load backup from games/${id}.bk`);
game = await readFile(`games/${id}.bk`)
.catch(() => {
console.error(error, game);
});
if (game) {
try {
game = JSON.parse(game);
console.log(`Restoring backup to games/${id}`);
await writeFile(`games/${id}`, JSON.stringify(game, null, 2));
} catch (error) {
console.error(error);
game = null;
}
}
}
}
if (!game) {
game = createGame(id);
}
if (!game.pipOrder || !game.borderOrder || !game.tileOrder) {
console.log("Shuffling old save file");
shuffleBoard(game);
@ -508,6 +530,9 @@ const loadGame = async (id) => {
if (!game.players[color].development) {
game.players[color].development = [];
}
if (!game.players[color].army) {
game.players[color].army = 0;
}
}
games[id] = game;
@ -595,6 +620,7 @@ const adminActions = (game, action, value) => {
name: next,
color: getColorFromName(game, next)
};
game.turns++;
addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
break;
@ -965,8 +991,7 @@ const calculateRoadLengths = (game, session) => {
checkForTies = true;
}
let longest = game.longestRoad ? game.players[game.longestRoad].roadLength : 4,
longestPlayers = [];
let longest = 4, longestPlayers = [];
for (let key in game.players) {
if (game.players[key].status === 'Not active') {
continue;
@ -1120,12 +1145,43 @@ const isCompatibleOffer = (player, offer) => {
return;
}
valid = offer.gets.find(item =>
item.type === give.type &&
(item.type === give.type || item.type === 'bank') &&
item.count === give.count) !== undefined;
});
return valid;
};
const isSameOffer = (player, offer) => {
const isBank = offer.name === 'The bank';
if (isBank) {
return false;
}
let same = player.gets && player.gives &&
player.gets.length === offer.gets.length &&
player.gives.length === offer.gives.length;
if (!same) {
return false;
}
player.gets.forEach(get => {
if (!same) {
return;
}
same = offer.gets.find(item =>
item.type === get.type && item.count === get.count) !== undefined;
});
if (same) player.gives.forEach(give => {
if (!same) {
return;
}
same = offer.gives.find(item =>
item.type === give.type && item.count === give.count) !== undefined;
});
return same;
};
const checkOffer = (player, offer) => {
let error = undefined;
offer.gives.forEach(give => {
@ -1212,7 +1268,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
const name = session.name;
let message, index;
let corners, corner;
let corners, corner, card;
switch (action) {
case "trade":
@ -1229,6 +1285,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
game.turn.actions = [ 'trade' ];
game.turn.limits = {};
for (let key in game.players) {
game.players[key].gives = [];
game.players[key].gets = [];
delete game.players[key].offerRejected;
}
addChatMessage(game, session, `${name} requested to begin trading negotiations.`);
break;
}
@ -1254,15 +1315,36 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (error) {
break;
}
if (isSameOffer(session.player, offer)) {
console.log(session.player);
error = `You already have a pending offer submitted for ${offerToString(offer)}.`;
break;
}
session.player.gives = offer.gives;
session.player.gets = offer.gets;
if (game.turn.name === name) {
/* This is a new offer from the active player -- reset everyone's
* 'offerRejected' flag */
for (let key in game.players) {
delete game.players[key].offerRejected;
}
game.turn.offer = offer;
}
addChatMessage(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
break;
}
/* Any player can reject an offer */
if (value === 'reject') {
const offer = req.body;
session.player.offerRejected = true;
addChatMessage(game, session, `${session.name} rejected ${game.turn.name}'s offer.`);
break;
}
/* Only the active player can accept an offer */
if (value === 'accept') {
if (game.turn.name !== name) {
@ -1273,6 +1355,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
const offer = req.body;
let target;
error = checkOffer(session.player, offer);
if (error) {
break;
}
/* Verify that the offer sent by the active player matches what
* the latest offer was that was received by the requesting player */
if (!offer.name || offer.name !== 'The bank') {
@ -1321,6 +1408,10 @@ router.put("/:id/:action/:value?", async (req, res) => {
player[item.type] -= item.count;
});
addChatMessage(game, session, `${session.name} has accepted a trade ` +
`offer for ${offerToString(session.player)} ` +
`from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
delete game.turn.offer;
if (target) {
delete target.gives;
@ -1331,8 +1422,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
game.turn.actions = [];
addChatMessage(game, session, `${session.name} has accepted a trade ` +
`offer from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
break;
}
@ -1375,6 +1464,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
name: next,
color: getColorFromName(game, next)
};
game.turns++;
addChatMessage(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
break;
@ -1444,7 +1534,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
});
if (cards.length === 0) {
addChatMessage(game, session, `Victim did not have any cards to steal.`);
addChatMessage(game, session, `${playerNameFromColor(game, value)} did not have any cards to steal.`);
game.turn.actions = [];
game.turn.limits = {};
} else {
@ -1473,6 +1563,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
break;
}
if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to purchase a development card.`;
break;
@ -1488,7 +1584,76 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.stone--;
player.wheat--;
player.sheep--;
player.development.push(game.developmentCards.pop());
card = game.developmentCards.pop();
card.turn = game.turns;
player.development.push(card);
break;
case 'play-card':
if (game.state !== 'normal') {
error = `You cannot purchase a settlement unless the game is active.`;
break;
}
if (session.color !== game.turn.color) {
error = `It is not your turn! It is ${game.turn.name}'s turn.`;
break;
}
if (!game.turn.roll) {
error = `You cannot play a card until you have rolled.`;
break;
}
if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
error = `Robber is in action. You can not play a card until all Robber tasks are resolved.`;
break;
}
card = req.body;
card = player.development.find(item => item.type == card.type && item.card == card.card);
if (!card) {
error = `The card you want to play was not found in your hand!`;
break;
}
if (player.playedCard === game.turns && card.type !== 'vp') {
error = `You can only play one development card per turn!`;
break;
}
if (card.played) {
error = `You have already played this card.`;
break;
}
/* Check if this is a victory point */
if (card.type === 'vp') {
let points = player.points;
player.development.forEach(item => {
if (item.type === 'vp') {
points++;
}
});
if (points < 10) {
error = `You can not play victory point cards until you can reach 10!`;
break;
}
}
card.played = true;
player.playedCard = game.turns;
addChatMessage(game, session, `${session.name} played a ${card.type}-${card.card} development card.`);
if (card.type === 'army') {
player.army++;
}
if (player.army > 2 &&
(!game.largestArmy || game.players[game.largestArmy].army < player.army)) {
if (game.largestArmy !== session.color) {
game.largestArmy = session.color;
addChatMessage(game, session, `${session.name} now has the largest army (${player.army})!`)
}
}
break;
case 'buy-settlement':
@ -1504,6 +1669,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
break;
}
if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to build a settlement.`;
break;
@ -1606,6 +1777,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
});
}
player.settlements--;
player.maritime = player.banks.map(bank => game.borders[Math.floor(bank / 3) + bank % 3]);
game.turn.actions = ['place-road'];
game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */
@ -1630,6 +1802,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You have insufficient resources to build a city.`;
break;
}
if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
break;
}
if (player.city < 1) {
error = `You have already built all of your cities.`;
break;
@ -1704,6 +1882,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
break;
}
if (player.brick < 1 || player.wood < 1) {
error = `You have insufficient resources to build a road.`;
break;
@ -1792,7 +1976,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
color: getColorFromName(game, next)
};
calculateRoadLengths(game, session);
addChatMessage(game, null, `It is ${next}'s turn. Place a settlement.`);
addChatMessage(game, null, `It is ${next}'s turn to place a settlement.`);
} else {
game.turn = {
actions: [],
@ -1922,12 +2106,8 @@ const sendGame = async (req, res, game, error) => {
/* Enforce game limit of >= 2 players */
if (active < 2 && game.state != 'lobby' && game.state != 'invalid') {
let message = "Insufficient players in game. Setting back to lobby."
console.log(game);
addChatMessage(game, null, message);
console.log(message);
/* It is no one's turn in the lobby */
delete game.turn;
game.state = 'lobby';
resetGame(game);
}
game.active = active;
@ -1950,11 +2130,6 @@ const sendGame = async (req, res, game, error) => {
}
game.turn.limits.pips.push(i);
}
} else {
/*
game.turn.limits = {};
game.turn.actions = [];
*/
}
}
@ -1981,6 +2156,30 @@ const sendGame = async (req, res, game, error) => {
lastTime = message.date;
});
/* Calculate points and determine if there is a winner */
for (let key in game.players) {
const player = game.players[key];
if (player.status === 'Not active') {
continue;
}
player.points = 0;
if (key === game.longestRoad) {
player.points += 2;
}
if (key === game.largestArmy) {
player.points += 2;
}
player.points += MAX_SETTLEMENTS - player.settlements;
player.points += 2 * (MAX_CITIES - player.cities);
if (!game.winner && player.points > 10 && session.color === key) {
addChatMessage(game, null, `${playerNameFromColor(game, key)} won the game with ${player.points} victory points!`);
game.winner = player;
game.state = 'winner';
}
}
/* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
* delete the player field from them */
const reducedGame = Object.assign({}, game, { sessions: {} }),
@ -2003,13 +2202,25 @@ const sendGame = async (req, res, game, error) => {
console.error(error);
});
const player = session.player ? session.player : undefined;
if (player) {
player.haveResources = player.wheat > 0 ||
player.brick > 0 ||
player.sheep > 0 ||
player.stone > 0 ||
player.wood > 0;
}
/* Strip out data that should not be shared with players */
delete reducedGame.developmentCards;
const playerGame = Object.assign({}, reducedGame, {
timestamp: Date.now(),
status: error ? error : "success",
name: session.name,
color: session.color,
order: (session.color in game.players) ? game.players[session.color].order : 0,
player: session.player,
player: player,
sessions: reducedSessions,
layout: layout
});
@ -2018,9 +2229,9 @@ const sendGame = async (req, res, game, error) => {
}
const resetGame = (game) => {
delete game.turn;
game.state = 'lobby';
game.turns = 0;
game.placements = {
corners: [],
@ -2045,15 +2256,14 @@ const resetGame = (game) => {
stone: 0,
brick: 0,
wood: 0,
roads: 15,
cities: 4,
settlements: 5,
roads: MAX_ROADS,
cities: MAX_CITIES,
settlements: MAX_SETTLEMENTS,
points: 0,
development: []
});
}
game.developmentCards = assetData.developmentCards.slice();
shuffle(game.developmentCards);
for (let i = 0; i < layout.corners.length; i++) {