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'] > .Corner-Shape,
[data-color='O'] > .Road-Shape { [data-color='O'] > .Road-Shape {
background-color: rgba(255, 196, 0, 1); background-color: rgb(255, 128, 0);
} }
[data-color='W'] > .Corner-Shape, [data-color='W'] > .Corner-Shape,

View File

@ -70,7 +70,7 @@ const Board = ({ table, game }) => {
const Corner = ({corner}) => { const Corner = ({corner}) => {
const onClick = (event) => { 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') { if (event.currentTarget.getAttribute('data-type') === 'settlement') {
table.placeCity(corner.index); table.placeCity(corner.index);
} else { } else {
@ -91,7 +91,7 @@ const Board = ({ table, game }) => {
const Pip = ({pip}) => { const Pip = ({pip}) => {
const onClick = (event) => { 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); table.placeRobber(pip.index);
return; return;
}; };
@ -377,14 +377,23 @@ const Board = ({ table, game }) => {
} }
} }
if (game && game.turn && game.turn.roll) { if (game && game.turn) {
let nodes = document.querySelectorAll('.Pip.Active'); let nodes = document.querySelectorAll('.Active');
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
nodes[i].classList.remove('Active'); nodes[i].classList.remove('Active');
} }
nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`); if (game.turn.roll) {
for (let i = 0; i < nodes.length; i++) { nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`);
nodes[i].classList.add('Active'); 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 { .Resource {
position: relative; position: relative;
width: 4.9em; height: 7em;
height: 7.2em; width: 5em;
display: inline-block; display: inline-block;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
margin: 0.25em; margin: 0.25em;
cursor: pointer;
} }
.Resource:hover { .Resource:hover {

View File

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

View File

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

View File

@ -22,6 +22,7 @@
background-color:rgba(224, 224, 224); background-color:rgba(224, 224, 224);
margin: 0.5em 0; margin: 0.5em 0;
} }
.Trade > * { .Trade > * {
min-width: 40em; min-width: 40em;
display: inline-flex; display: inline-flex;
@ -29,6 +30,11 @@
flex-direction: column; flex-direction: column;
} }
.Trade .Resource {
width: 3.75em; /* 5x7 aspect ratio */
height: 5.25em;
}
.Trade .PlayerColor { .Trade .PlayerColor {
width: 0.5em; width: 0.5em;
height: 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 Button from '@material-ui/core/Button';
import Resource from './Resource.js'; import Resource from './Resource.js';
const ResourceCounter = ({type, onCount, max}) => { const ResourceCounter = ({type, count, onCount, max}) => {
const [count, setCount] = useState(0); count = count ? count : 0;
const plusClicked = (event) => { const plusClicked = (event) => {
if (max === undefined || max > count) { if (max === undefined || max > count) {
if (onCount) { if (onCount) {
onCount(type, count+1); onCount(type, count+1);
} }
setCount(count+1);
} }
}; };
const minusClicked = (event) => { const minusClicked = (event) => {
@ -21,7 +20,6 @@ const ResourceCounter = ({type, onCount, max}) => {
if (onCount) { if (onCount) {
onCount(type, count-1); onCount(type, count-1);
} }
setCount(count-1);
} }
}; };
@ -77,7 +75,7 @@ const Trade = ({table}) => {
} else { } else {
setGiveLine(items.join(', ')); setGiveLine(items.join(', '));
} }
}, [setGiveLine, setGives]); }, [setGiveLine, setGives, gives]);
const getCount = useCallback((type, count) => { const getCount = useCallback((type, count) => {
gets[type] = count; gets[type] = count;
@ -94,12 +92,25 @@ const Trade = ({table}) => {
} else { } else {
setGetLine(items.join(', ')); 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) { if (!table.game) {
return (<></>); return (<></>);
} }
const game = table.game;
const isTurn = (table.game.turn && table.game.turn.color === table.game.color) ? true : false; const isTurn = (table.game.turn && table.game.turn.color === table.game.color) ? true : false;
const offerClicked = (event) => { const offerClicked = (event) => {
@ -115,7 +126,7 @@ const Trade = ({table}) => {
} }
table.offerTrade(trade); table.offerTrade(trade);
} }
const acceptClicked = (offer) => { const acceptClicked = (offer) => {
table.acceptTrade(offer); table.acceptTrade(offer);
}; };
@ -124,6 +135,12 @@ const Trade = ({table}) => {
table.cancelTrading(); table.cancelTrading();
} }
/* Non-current player has rejected the active player's
* bid */
const rejectClicked = (trade) => {
table.rejectTrade(trade);
}
let players = []; let players = [];
for (let color in table.game.players) { for (let color in table.game.players) {
const item = table.game.players[color], const item = table.game.players[color],
@ -134,7 +151,8 @@ const Trade = ({table}) => {
color: color, color: color,
valid: false, valid: false,
gets: item.gets ? item.gets : [], gets: item.gets ? item.gets : [],
gives: item.gives ? item.gives : [] gives: item.gives ? item.gives : [],
offerRejected: item.offerRejected ? true : false
}); });
} }
} }
@ -163,7 +181,14 @@ const Trade = ({table}) => {
valid: false valid: false
}); });
} }
const player = (table.game && table.game.player) ? table.game.player : undefined;
if (!player) {
return <></>;
}
let canAccept = false;
if (table.game.turn.offer) { if (table.game.turn.offer) {
players.forEach(trade => { players.forEach(trade => {
let valid = trade.gets.length && trade.gives.length; let valid = trade.gets.length && trade.gives.length;
@ -187,9 +212,29 @@ const Trade = ({table}) => {
}); });
trade.valid = valid; 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) => { 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 => const gets = item.gets.map(get =>
`${get.count} ${(get.type === 'bank') ? 'of any one resource' : get.type}`) `${get.count} ${(get.type === 'bank') ? 'of any one resource' : get.type}`)
.join(', '), .join(', '),
@ -211,16 +256,18 @@ const Trade = ({table}) => {
} }
{ isTurn && <Button disabled={!item.valid} { isTurn && <Button disabled={!item.valid}
onClick={() => acceptClicked(item)}>accept</Button> } 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>
</div> </div>
); );
}); });
const player = (table.game && table.game.player) ? table.game.player : undefined;
if (!player) {
return <></>;
}
return ( return (
<div className="Trade"> <div className="Trade">
<Paper> <Paper>
@ -230,32 +277,35 @@ const Trade = ({table}) => {
<div className="PlayerList"> <div className="PlayerList">
{ players } { players }
</div> </div>
<div className='TradeLine' { !player.haveResources && <b>You have no resources to participate in this trade.</b> }
style={{ { player.haveResources &&
flexDirection: 'column', <div className='TradeLine'
alignItems: 'flex-start' style={{
}}> flexDirection: 'column',
<div style={{display: 'flex' }}> alignItems: 'flex-start'
<b>You want to receive {getLine}.</b> }}>
<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>
<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'} <Button disabled={getLine === 'nothing' || giveLine === 'nothing'}
onClick={offerClicked}>Offer</Button> onClick={offerClicked}>Offer</Button>
{ isTurn && <Button onClick={cancelClicked}>cancel</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 { corners } = require("./layout.js");
const layout = require('./layout.js'); const layout = require('./layout.js');
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
let gameDB; let gameDB;
require("../db/games").then(function(db) { require("../db/games").then(function(db) {
@ -115,14 +119,13 @@ const processTies = (players) => {
if (A.order === B.order) { if (A.order === B.order) {
return B.orderRoll - A.orderRoll; return B.orderRoll - A.orderRoll;
} }
return A.order - B.order; return B.order - A.order;
}); });
/* Sort the players into buckets based on their /* Sort the players into buckets based on their
* order, and their current roll. If a resulting * order, and their current roll. If a resulting
* roll array has more than one element, then there * roll array has more than one element, then there
* is a tie that must be resolved */ * is a tie that must be resolved */
let slots = []; let slots = [];
players.forEach(player => { players.forEach(player => {
if (!slots[player.order]) { if (!slots[player.order]) {
@ -135,14 +138,15 @@ const processTies = (players) => {
}); });
let ties = false, order = 0; let ties = false, order = 0;
slots.forEach((slot) => { /* Reverse from high to low */
slots.reverse().forEach((slot) => {
slot.forEach(pips => { slot.forEach(pips => {
if (pips.length !== 1) { if (pips.length !== 1) {
ties = true; ties = true;
pips.forEach(player => { pips.forEach(player => {
player.orderRoll = 0; player.orderRoll = 0;
player.order = order; player.order = order;
player.orderStatus = `Tied for ${order+1}.`; player.orderStatus = `Tied.`;
}); });
} else { } else {
pips[0].order = order; pips[0].order = order;
@ -258,7 +262,7 @@ const roll = (game, session) => {
break; break;
} }
if (player.order || player.orderRoll) { if (player.order && player.orderRoll) {
error = `Player ${name} has already rolled for player order.`; error = `Player ${name} has already rolled for player order.`;
break; break;
} }
@ -402,9 +406,9 @@ const processRoll = (game, dice) => {
const getPlayer = (game, color) => { const getPlayer = (game, color) => {
if (!game) { if (!game) {
return { return {
roads: 15, roads: MAX_ROADS,
cities: 4, cities: MAX_CITIES,
settlements: 5, settlements: MAX_SETTLEMENTS,
points: 0, points: 0,
status: "Not active", status: "Not active",
lastActive: 0, lastActive: 0,
@ -414,6 +418,7 @@ const getPlayer = (game, color) => {
sheep: 0, sheep: 0,
wood: 0, wood: 0,
brick: 0, brick: 0,
army: 0,
development: [] development: []
}; };
} }
@ -459,16 +464,33 @@ const loadGame = async (id) => {
return; return;
}); });
if (!game) { if (game) {
game = createGame(id);
} else {
try { try {
game = JSON.parse(game); game = JSON.parse(game);
console.log(`Creating backup of games/${id}`);
await writeFile(`games/${id}.bk`, JSON.stringify(game));
} catch (error) { } catch (error) {
console.error(error, game); console.log(`Attempting to load backup from games/${id}.bk`);
return null; 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) { if (!game.pipOrder || !game.borderOrder || !game.tileOrder) {
console.log("Shuffling old save file"); console.log("Shuffling old save file");
@ -508,6 +530,9 @@ const loadGame = async (id) => {
if (!game.players[color].development) { if (!game.players[color].development) {
game.players[color].development = []; game.players[color].development = [];
} }
if (!game.players[color].army) {
game.players[color].army = 0;
}
} }
games[id] = game; games[id] = game;
@ -595,6 +620,7 @@ const adminActions = (game, action, value) => {
name: next, name: next,
color: getColorFromName(game, next) color: getColorFromName(game, next)
}; };
game.turns++;
addChatMessage(game, null, `The admin skipped ${name}'s turn.`); addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`); addChatMessage(game, null, `It is ${next}'s turn.`);
break; break;
@ -965,8 +991,7 @@ const calculateRoadLengths = (game, session) => {
checkForTies = true; checkForTies = true;
} }
let longest = game.longestRoad ? game.players[game.longestRoad].roadLength : 4, let longest = 4, longestPlayers = [];
longestPlayers = [];
for (let key in game.players) { for (let key in game.players) {
if (game.players[key].status === 'Not active') { if (game.players[key].status === 'Not active') {
continue; continue;
@ -1120,12 +1145,43 @@ const isCompatibleOffer = (player, offer) => {
return; return;
} }
valid = offer.gets.find(item => valid = offer.gets.find(item =>
item.type === give.type && (item.type === give.type || item.type === 'bank') &&
item.count === give.count) !== undefined; item.count === give.count) !== undefined;
}); });
return valid; 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) => { const checkOffer = (player, offer) => {
let error = undefined; let error = undefined;
offer.gives.forEach(give => { offer.gives.forEach(give => {
@ -1212,7 +1268,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
const name = session.name; const name = session.name;
let message, index; let message, index;
let corners, corner; let corners, corner, card;
switch (action) { switch (action) {
case "trade": case "trade":
@ -1229,6 +1285,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
} }
game.turn.actions = [ 'trade' ]; game.turn.actions = [ 'trade' ];
game.turn.limits = {}; 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.`); addChatMessage(game, session, `${name} requested to begin trading negotiations.`);
break; break;
} }
@ -1254,15 +1315,36 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (error) { if (error) {
break; 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.gives = offer.gives;
session.player.gets = offer.gets; session.player.gets = offer.gets;
if (game.turn.name === name) { 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; game.turn.offer = offer;
} }
addChatMessage(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`); addChatMessage(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
break; 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 */ /* Only the active player can accept an offer */
if (value === 'accept') { if (value === 'accept') {
if (game.turn.name !== name) { if (game.turn.name !== name) {
@ -1273,6 +1355,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
const offer = req.body; const offer = req.body;
let target; let target;
error = checkOffer(session.player, offer);
if (error) {
break;
}
/* Verify that the offer sent by the active player matches what /* Verify that the offer sent by the active player matches what
* the latest offer was that was received by the requesting player */ * the latest offer was that was received by the requesting player */
if (!offer.name || offer.name !== 'The bank') { if (!offer.name || offer.name !== 'The bank') {
@ -1321,6 +1408,10 @@ router.put("/:id/:action/:value?", async (req, res) => {
player[item.type] -= item.count; 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; delete game.turn.offer;
if (target) { if (target) {
delete target.gives; delete target.gives;
@ -1331,8 +1422,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
game.turn.actions = []; game.turn.actions = [];
addChatMessage(game, session, `${session.name} has accepted a trade ` +
`offer from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
break; break;
} }
@ -1375,6 +1464,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
name: next, name: next,
color: getColorFromName(game, next) color: getColorFromName(game, next)
}; };
game.turns++;
addChatMessage(game, session, `${name} passed their turn.`); addChatMessage(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`); addChatMessage(game, null, `It is ${next}'s turn.`);
break; break;
@ -1444,7 +1534,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
} }
}); });
if (cards.length === 0) { 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.actions = [];
game.turn.limits = {}; game.turn.limits = {};
} else { } else {
@ -1473,6 +1563,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`; error = `You cannot build until you have rolled.`;
break; 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) { if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to purchase a development card.`; error = `You have insufficient resources to purchase a development card.`;
break; break;
@ -1488,9 +1584,78 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.stone--; player.stone--;
player.wheat--; player.wheat--;
player.sheep--; player.sheep--;
player.development.push(game.developmentCards.pop()); card = game.developmentCards.pop();
card.turn = game.turns;
player.development.push(card);
break; 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': case 'buy-settlement':
if (game.state !== 'normal') { if (game.state !== 'normal') {
error = `You cannot purchase a settlement unless the game is active.`; error = `You cannot purchase a settlement unless the game is active.`;
@ -1504,6 +1669,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`; error = `You cannot build until you have rolled.`;
break; 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) { if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to build a settlement.`; error = `You have insufficient resources to build a settlement.`;
break; 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]); player.maritime = player.banks.map(bank => game.borders[Math.floor(bank / 3) + bank % 3]);
game.turn.actions = ['place-road']; game.turn.actions = ['place-road'];
game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */ 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.`; error = `You have insufficient resources to build a city.`;
break; 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) { if (player.city < 1) {
error = `You have already built all of your cities.`; error = `You have already built all of your cities.`;
break; break;
@ -1704,6 +1882,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`; error = `You cannot build until you have rolled.`;
break; 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) { if (player.brick < 1 || player.wood < 1) {
error = `You have insufficient resources to build a road.`; error = `You have insufficient resources to build a road.`;
break; break;
@ -1792,7 +1976,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
color: getColorFromName(game, next) color: getColorFromName(game, next)
}; };
calculateRoadLengths(game, session); 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 { } else {
game.turn = { game.turn = {
actions: [], actions: [],
@ -1922,12 +2106,8 @@ const sendGame = async (req, res, game, error) => {
/* Enforce game limit of >= 2 players */ /* Enforce game limit of >= 2 players */
if (active < 2 && game.state != 'lobby' && game.state != 'invalid') { if (active < 2 && game.state != 'lobby' && game.state != 'invalid') {
let message = "Insufficient players in game. Setting back to lobby." let message = "Insufficient players in game. Setting back to lobby."
console.log(game);
addChatMessage(game, null, message); addChatMessage(game, null, message);
console.log(message); resetGame(game);
/* It is no one's turn in the lobby */
delete game.turn;
game.state = 'lobby';
} }
game.active = active; game.active = active;
@ -1950,11 +2130,6 @@ const sendGame = async (req, res, game, error) => {
} }
game.turn.limits.pips.push(i); 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; 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 /* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
* delete the player field from them */ * delete the player field from them */
const reducedGame = Object.assign({}, game, { sessions: {} }), const reducedGame = Object.assign({}, game, { sessions: {} }),
@ -2003,13 +2202,25 @@ const sendGame = async (req, res, game, error) => {
console.error(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, { const playerGame = Object.assign({}, reducedGame, {
timestamp: Date.now(), timestamp: Date.now(),
status: error ? error : "success", status: error ? error : "success",
name: session.name, name: session.name,
color: session.color, color: session.color,
order: (session.color in game.players) ? game.players[session.color].order : 0, order: (session.color in game.players) ? game.players[session.color].order : 0,
player: session.player, player: player,
sessions: reducedSessions, sessions: reducedSessions,
layout: layout layout: layout
}); });
@ -2018,9 +2229,9 @@ const sendGame = async (req, res, game, error) => {
} }
const resetGame = (game) => { const resetGame = (game) => {
delete game.turn;
game.state = 'lobby'; game.state = 'lobby';
game.turns = 0;
game.placements = { game.placements = {
corners: [], corners: [],
@ -2045,15 +2256,14 @@ const resetGame = (game) => {
stone: 0, stone: 0,
brick: 0, brick: 0,
wood: 0, wood: 0,
roads: 15, roads: MAX_ROADS,
cities: 4, cities: MAX_CITIES,
settlements: 5, settlements: MAX_SETTLEMENTS,
points: 0, points: 0,
development: [] development: []
}); });
} }
game.developmentCards = assetData.developmentCards.slice();
shuffle(game.developmentCards); shuffle(game.developmentCards);
for (let i = 0; i < layout.corners.length; i++) { for (let i = 0; i < layout.corners.length; i++) {