diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..a636aa6 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2017", + "allowSyntheticDefaultImports": true, + "noEmit": true, + "checkJs": true, + "jsx": "react", + "lib": [ "dom", "es2017" ], + "include": [ + "src", + "assets/game/acf" + ] + } +} diff --git a/package.json b/package.json index 2230a08..57fc53e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "GPL-3.0-or-later", "devDependencies": { "@babel/core": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3", diff --git a/src/components/farm/Board.jsx b/src/components/farm/Board.jsx index 0229347..adf6e97 100644 --- a/src/components/farm/Board.jsx +++ b/src/components/farm/Board.jsx @@ -30,8 +30,8 @@ import ReactDOM from 'react-dom' import { connect } from 'react-redux' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faUser, faUsers, faTractor, faWindowRestore, - faDollarSign, faTimes, faAsterisk - } from '@fortawesome/free-solid-svg-icons' + faDollarSign, faTimes, faAsterisk, faExchangeAlt, + faInfoCircle } from '@fortawesome/free-solid-svg-icons' import { GroupBox, Row, Col, Button } from '../widgets.jsx' import SpaceNode from './SpaceNode.jsx' @@ -40,7 +40,7 @@ import Tractor from '../tractor/Tractor.jsx' import { GAME_STATES, ALERTS } from '../../constants.js' import { itemCard, itemCardShort, fateCard, ridgeNames } from 'game.js' import { setSelectedCard, setMessagePanelSpace, setMPDims, movePlayer, - nextUIAction, alert } from './actions.js' + nextUIAction, alert, alertHandled } from './actions.js' import { buy, roll, endTurn, loan, trade, submitTradeAccept, submitTradeDeny, submitTradeCancel, audit, buyUncleBert, skip } from './interface.js' @@ -69,7 +69,8 @@ function getString(id) { } function getInt(id) { - return parseInt(getElementValue(id)); + const i = parseInt(getElementValue(id)); + return isNaN(i) ? 0 : i; } function getChecked(id) { @@ -102,7 +103,7 @@ function tradeString(player, invert) { var r = ''; let mult = invert ? -1 : 1; for (var k in player.trade) { - if (k !== 'player' && k !== 'originator' && k !== 'cards') { + if (k !== 'player' && k !== 'originator' && k !== 'cards' && k !== 'trade-number') { r += (player.trade[k] === true ? '' : player.trade[k] * mult) + ' ' + k + ', '; } else if (k === 'cards') { r += 'cards: ' + player.trade[k] + ', '; @@ -113,13 +114,15 @@ function tradeString(player, invert) { class PlayerTradeProposed extends React.Component { render () { - if (this.props.player.trade.player === undefined) { - return (
); + if (this.props.player.trade.error) { + return (

ERROR: {this.props.player.trade.error}

); + } else if (this.props.player.trade.player === undefined) { + return (

You GAIN something if it is POSITIVE and you GIVE something if it is NEGATIVE.

); } else if (this.props.player.trade.originator === this.props.player.name) { return (

You proposed a trade with {this.props.player.trade.player} for {'\u00A0'}{tradeString(this.props.player, false)}.

); } else { - return (

{this.props.player.trade.player} proposed a trade for + return (

{this.props.player.trade.originator} proposed a trade for {'\u00A0'}{tradeString(this.props.player, true)}.

); } } @@ -159,7 +162,7 @@ class PlayerTradeButton extends React.Component { } else { return ( - ); @@ -312,7 +315,9 @@ class PlayerTurnContainer extends React.Component { worth = netWorth(player), auditButton = (this.props.game.calledAudit === false && worth >= this.props.game.settings.auditThreshold) ? ( ) : ''; - if (player.state === GAME_STATES.preTurn) { + if (this.props.game.state === 'finished') { + view = (Game finished!); + } else if (player.state === GAME_STATES.preTurn) { view = ( {' '}{auditButton} @@ -344,8 +349,6 @@ class PlayerTurnContainer extends React.Component { ); } } - } else if (this.props.game.state === 'finished') { - view = (Game finished!); } else { view = ( - ) : ()} + + {isCurrentPlayer ? + ( +
+ +
+ ) : ()} ); break; @@ -1047,161 +1069,170 @@ class Action extends React.Component { render() { let view, buttons; const currentPlayer = this.props.player.name === this.props.game.currentPlayer ? - this.props.player : this.props.game.otherPlayers - .find(p => p.player.name === this.props.game.currentPlayer).player; + this.props.player : this.props.game.otherPlayers + .find(p => p.player.name === this.props.game.currentPlayer).player; switch (this.props.ui.action) { - case 'otb': - if (this.props.player.name === this.props.game.currentPlayer) { + case 'otb': + if (this.props.player.name === this.props.game.currentPlayer) { + view = ( +
+ +
+ +
+ ); + } else { + view = ( +
+

{currentPlayer.name} drew an {itemCard}

+
+ ); + } + buttons = (); + break; + case 'farmers-fate': view = (
- -
- + +
+
); - } else { + buttons = (); + break; + case 'ff-uncle-bert': + const ffButtons = ( + + {this.props.player.cash >= 10000 ? ( + + ) : ()} + + + ); view = ( -
-

{currentPlayer.name} drew an {itemCard}

-
+ +
+

+ {currentPlayer.cash >= 10000 ? + `You have enough cash to take over Uncle Bert's farm!` : + `You must raise another $` + + formatMoney(10000 - currentPlayer.cash) + + ` to be able to take over Uncle Berts farm!` + } +

+

+ {(this.props.player.name === this.props.game.currentPlayer) ? + ffButtons : ()} +

+
+
); - } - buttons = (); - break; - case 'farmers-fate': - view = ( -
- -
- -
- ); - buttons = (); - break; - case 'ff-uncle-bert': - view = ( - -
-

- {this.props.player.cash >= 10000 ? - `You have enough cash to take over Uncle Bert's farm!` : - `You must raise another $` + - formatMoney(10000 - this.props.player.cash) + - ` to be able to take over Uncle Berts farm!` - } -

-

- {this.props.player.cash >= 10000 ? - () : - ()} - -

-
-
- ); - buttons = (); - break; - case 'money': - view = (
-
- + buttons = (); + break; + case 'money': + const { amount, player } = this.props.ui.actionValue; + view = ( +
+
+ +
+ {this.props.player.name === player ? 'You' : + player} {amount <= 0 ? 'lost ' : 'gained '} + ${formatMoney(Math.abs(amount))}! +
+
+
); + buttons = (); + break; + case 'info': + view = (
- {this.props.player.name === this.props.game.currentPlayer ? 'You' : - this.props.game.currentPlayer} {this.props.ui.actionValue < 0 ? 'lost ' : 'gained '} - ${formatMoney(Math.abs(this.props.ui.actionValue))}! -
+

{this.props.ui.actionValue}

-
); - buttons = (); - break; - case 'info': - view = ( -
-

{this.props.ui.actionValue}

-
- ); - buttons = (); - break; - case 'harvest': - view = ( this.props.showNextAction()} />); - buttons = (); - break; - case 'move': - view = ( this.props.showNextAction()} - key={this.props.game.currentPlayer + '|' + this.props.ui.actionValue.from + '|' + this.props.ui.actionValue.to} - player={this.props.player} - game={this.props.game} - spaces={this.props.spaces} - movePlayer={this.props.movePlayer} - autoSkip={this.props.ui.autoSkip} - ui={this.props.ui}/>); - buttons = (); - break; - case 'goto': - view = ( this.props.showNextAction()} - key={this.props.game.currentPlayer + '|' + this.props.ui.actionValue.from + '|' + this.props.ui.actionValue.to} - player={this.props.player} - game={this.props.game} - spaces={this.props.spaces} - movePlayer={this.props.movePlayer} - autoSkip={this.props.ui.autoSkip} - ui={this.props.ui} />); - buttons = (); - break; - case 'pre-rolling': - view = (); - buttons = (); - break; - case 'roll': - const roll = this.props.ui.actionValue.to - - (this.props.ui.actionValue.to < this.props.ui.actionValue.from ? - this.props.ui.actionValue.from - 49 : this.props.ui.actionValue.from); - view = ( this.props.showNextAction() - : false} - autoSkip={this.props.ui.autoSkip} - skip={this.props.player.name === this.props.game.currentPlayer} - showScreenDelay={2000} />); - buttons = (); - break; - default: - if (this.props.player.name === this.props.game.currentPlayer) { - view = (); - } else { - view = (Waiting for {this.props.game.currentPlayer}); - } + ); + buttons = (); + break; + case 'harvest': + view = ( this.props.showNextAction()} />); + buttons = (); + break; + case 'move': + view = ( this.props.showNextAction()} + key={this.props.game.currentPlayer + '|' + this.props.ui.actionValue.from + '|' + this.props.ui.actionValue.to} + player={this.props.player} + game={this.props.game} + spaces={this.props.spaces} + movePlayer={this.props.movePlayer} + autoSkip={this.props.ui.autoSkip} + ui={this.props.ui} />); + buttons = (); + break; + case 'goto': + view = ( this.props.showNextAction()} + key={this.props.game.currentPlayer + '|' + this.props.ui.actionValue.from + '|' + this.props.ui.actionValue.to} + player={this.props.player} + game={this.props.game} + spaces={this.props.spaces} + movePlayer={this.props.movePlayer} + autoSkip={this.props.ui.autoSkip} + ui={this.props.ui} />); + buttons = (); + break; + case 'pre-rolling': + view = (); + buttons = (); + break; + case 'roll': + const roll = this.props.ui.actionValue.to - + (this.props.ui.actionValue.to < this.props.ui.actionValue.from ? + this.props.ui.actionValue.from - 49 : this.props.ui.actionValue.from); + view = ( this.props.showNextAction() + : false} + autoSkip={this.props.ui.autoSkip} + skip={this.props.player.name === this.props.game.currentPlayer} + showScreenDelay={2000} />); + buttons = (); + break; + default: + if (this.props.player.name === this.props.game.currentPlayer) { + view = (); + } else { + view = (Waiting for {this.props.game.currentPlayer}); + } } return ( - -
- {view} -
-
-
- {this.props.otherPlayersTurn ? () : buttons} -
- + +
+ {view} +
+
+
+ {this.props.otherPlayersTurn ? () : buttons} +
+
); } @@ -1211,7 +1242,7 @@ class Board extends React.Component { render() { const rh = this.props.height || '20px'; // height={h} const renderSpace = (s, h, o) => - (); + (); return ( @@ -1277,6 +1308,7 @@ class AlertOverlay extends React.Component { hide = e => { e.preventDefault(); this.setState({ visible: false }); + this.props.alertHandled(this.props.id); this.props.hideHandler(); } @@ -1284,6 +1316,7 @@ class AlertOverlay extends React.Component { this.hide(e); this.props.handler(); } + // render() { return ( @@ -1295,7 +1328,6 @@ class AlertOverlay extends React.Component { {this.props.children}
- close
@@ -1317,26 +1349,126 @@ class InfoBar extends React.Component { } } +function ceilHundred(n) { + return Math.ceil(n + (n % 100)); +} + +class HarvestTable extends React.Component { + state = { + selected: 'hay' + } + + selectCrop = e => { + this.setState({ selected: e.target.value }); + } + + render() { + const table = this.props.table, + selectedCrop = this.state.selected; + return ( +
+

Crop

+ + + + + {table[selectedCrop].slice(1).map((x, idx) => ( + + ))} + + + + {table[selectedCrop].slice(1).map((mult, row) => ( + + {new Array(7).fill(0).map((x, idx) => ( + + ))} + + ))} + +
Dice{(idx + 1) * table[selectedCrop][0]} {selectedCrop === 'cows' ? 'COWS' : 'ACRES'}
{idx === 0 ? row + 1 : formatMoney(ceilHundred(mult * idx))}
+
+ ); + } +} + +class Info extends React.Component { + render() { + // TODO change $250,000 to actual value in settings + return ( + <> +

Game Instructions

+

Winner

+

The winner is the player with the highest net worth.

+

Game Ending

+

When a player reaches a net worth of $250,000 or more they may 'Call Audit'. Once they call an audit all players continue playing until they finish their current year. Once all players have finished their last year the game is over and the player with the highest net worth is the winner.

+

Harvests

+

Harvests occur once per block on the matching asset type. For example: hay shows as green and is harvested the first time landing on a block of green spaces each time around the board.

+

When harvesting a resource the die is first rolled then the harvest amount is calculated based on the following table and how many units of the asset you own:

+ {this.props.harvestTable ? () : (<>)} +

After a harvest an Operating Expense is drawn.

+

Purchashing

+

To increase harvest amounts new assets must be bought. This is done via {itemCard} cards. Click on , select the card for the asset you wish to purchase, and enter the amount of cash you desire to spend to purchace the selected asset. The remainder will be taken out in debit, if available.

+

Purchashing may only take places on Purchashing spaces (Christmas - Spring Celebration).

+ + + ); + } +} + const SCREENS = { summary: 'summary', misc: 'misc', farms: 'farms', cards: 'cards', trade: 'trade', loans: 'loans', - action: 'action' }; + action: 'action', info: 'info' }; class BoardApp extends React.Component { iconToScreen = { user: SCREENS.summary, 'window-restore': SCREENS.cards, 'dollar-sign': SCREENS.loans, users: SCREENS.farms, 'exchange-alt': SCREENS.trade, asterisk: SCREENS.misc, - tractor: SCREENS.action } + tractor: SCREENS.action, 'info-circle': SCREENS.info } unsubscribeAlert = () => null constructor(props) { super(props); this.state = { - screen: SCREENS.summary + screen: SCREENS.summary, + card: props.ui.card }; + this.myRef = React.createRef(); } showScreen = screen => { - this.setState({ screen: screen }); + if (!(window.innerWidth >= 1024 && screen === SCREENS.action)) { + this.setState({ screen: screen }); + } + } + + setCard = card => { + this.setState({ card }); + } + + componentDidUpdate() { + if (this.state.card.type === 'no-card' && + this.props.ui.cards.length > 0) { + this.setState({ card: this.props.ui.cards[0] }); + } else if (this.state.card.type !== 'no-card' && + this.props.ui.cards.length === 0) { + this.setState({ card: { type: 'no-card', contents: '', total: 0 } }); + } else if (this.state.card.type !== 'no-card' && + this.props.ui.cards.map(x => x.id).indexOf(this.state.card.id) < 0) { + this.setState({ card: this.props.ui.cards[0] }); + } + + // we don't show this alert when on the actions screen so mark + // it as handled. + if (this.props.ui.unhandledAlert && + this.props.ui.unhandledAlert.type === ALERTS.beginTurn && + this.state.screen === SCREENS.action) { + this.props.alertHandled(this.props.ui.unhandledAlert.id); + } } componentDidMount() { @@ -1360,7 +1492,10 @@ class BoardApp extends React.Component { } iconClass = icon => { - return this.iconToScreen[icon] === this.state.screen ? 'is-active' : ' '; + return this.iconToScreen[icon] === this.state.screen ? 'is-active' : + (this.iconToScreen[icon] === SCREENS.trade ? + (this.props.game.settings.trade ? ' ' : ' hidden ') + : (icon === 'tractor' ? ' hide-for-large ' : ' ')); } tabClass = screen => { @@ -1376,137 +1511,198 @@ class BoardApp extends React.Component { render() { let alertOverlay; - if (!this.props.ui.alertHandled && this.props.ui.alert === ALERTS.endOfGame) { + const alert = this.props.ui.unhandledAlert; + if (alert && alert.type === ALERTS.endOfGame) { alertOverlay = ( this.props.alert(false)} + hideHandler={() => 'nothing'} handler={() => { return false; }}>

Game Over!

- {this.props.ui.alertContents.map((e, i) => ( + {alert.contents.map((e, i) => (

{e}

))}
); - } else if (!this.props.ui.alertHandled && this.state.screen !== SCREENS.action) { - switch (this.props.ui.alert) { - case ALERTS.beginTurn: - alertOverlay = ( - this.props.alert(false)} - handler={() => { - roll(); - this.showScreen(SCREENS.action); }}> -

{`It's your turn!`}

-
- ); - break; - case ALERTS.otherPlayersTurn: - alertOverlay = ( - this.props.alert(false)} - handler={() => { - this.showScreen(SCREENS.action); }}> -

It's {this.props.game.currentPlayer}'s turn

-
- ); - break; - default: - alertOverlay = (); + } else if (alert && alert.type === ALERTS.auditCalled) { + alertOverlay = ( + 'nothing'} + handler={() => { return false; }}> + +

Audit Called!

+

{alert.contents} called an audit!

+

This is your last trip around the board.

+
+
+ ); + } else if (alert && alert.type === ALERTS.proposedTrade) { + alertOverlay = ( + 'nothing'} + handler={() => { + this.showScreen(SCREENS.trade) + }}> + +

{alert.contents} proposed a trade!

+
+
+ ); + } else if (alert && this.state.screen !== SCREENS.action) { + switch (alert.type) { + case ALERTS.beginTurn: + alertOverlay = ( + 'nothing'} + handler={() => { + roll(); + this.showScreen(SCREENS.action); }}> +

{`It's your turn!`}

+
+ ); + break; + case ALERTS.otherPlayersTurn: + alertOverlay = ( + 'nothing'} + handler={() => { + this.showScreen(SCREENS.action); }}> +

It's {this.props.game.currentPlayer}'s turn

+
+ ); + break; + default: + alertOverlay = (); } } else { alertOverlay = (); } - if (this.props.ui.alert === ALERTS.raiseMoney) { + if (alert && alert.type === ALERTS.raiseMoney && !this.props.ui.action) { alertOverlay = ( this.props.alert(false)} + hideHandler={() => 'nothing'} handler={() => { - this.showScreen(SCREENS.loans); }}> - -

Out of cash!

-

You have less than $0 cash and must raise money to continue.

-
+ this.showScreen(SCREENS.loans); }}> + +

Out of cash!

+

You have less than $0 cash and must raise money to continue.

+
); } // faExchangeAlt -> trade icon, hidden for now return ( -
- {alertOverlay} - - -
-
    - {[faUser, faTractor, faWindowRestore, faDollarSign, faUsers, faAsterisk] - .map((icon, i) => - (
  • -
    - -
  • ))} -
-
    - {[faUser, faTractor, faWindowRestore, faDollarSign, faUsers, faAsterisk] - .map((icon, i) => - (
  • - -
  • ))} -
-
-
- -
-
- this.showScreen(screen)} - ui={this.props.ui} /> -
-
- -
- +
+ {alertOverlay} + + +
+
    + {[faUser, faTractor, faWindowRestore, faDollarSign, faUsers, faExchangeAlt, faInfoCircle] + .map((icon, i) => + (
  • +
    + +
  • ))} +
+
    + {[faUser, faTractor, faWindowRestore, faDollarSign, faUsers, faExchangeAlt, faInfoCircle] + .map((icon, i) => + (
  • + +
  • ))} +
+
+
+ +
+
+ this.showScreen(screen)} + ui={this.props.ui} /> +
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- +
+
+ this.showScreen(screen)} + ui={this.props.ui} /> +
- -
-
- -
-
- -
-
- -
-
- -
-
-
- +
+
); } @@ -1514,7 +1710,7 @@ class BoardApp extends React.Component { export default connect( state => state.farm, - { setMessagePanelSpace, setMPDims, nextUIAction, movePlayer, alert } + { setMessagePanelSpace, setMPDims, nextUIAction, movePlayer, alert, alertHandled } )(BoardApp) class Card extends React.Component { @@ -1543,45 +1739,55 @@ class Card extends React.Component { handleSubmit = e => { e.preventDefault(); - buy(this.props.ui.card.id, this.state.cash); + buy(this.props.card.id, this.state.cash); } render () { - const card = this.props.ui.card; + const card = this.props.card; let action = (null); switch (card.type) { - case 'otb': action = ( -
-
- - - - - - -
- $: - - {'\u00A0'},000 -
- -
-
-
); break; + case 'otb': action = ( +
+
+ + + You: Cash: ${formatMoney(this.props.playerCash)} Debt: ${formatMoney(this.props.playerDebt)} + + + + + Buy using: ${this.state.cash}K cash ${this.props.cost - this.state.cash}K debt + + + + + + + +
+ with cash $ + + {'\u00A0'}K +
+ +
+ +
+
); break; case 'no-card': action = (); break; } return (
- +
{card.id}
@@ -1604,8 +1810,8 @@ class CardListComp extends React.Component { const ui = this.props.ui, cards = ui.cards, cardOps = cards.map((c, i) => - (
  • this.props.setSelectedCard(c)}> + (
  • this.props.setCard(c)}> {c.summary}
  • )); diff --git a/src/components/farm/SpaceNode.jsx b/src/components/farm/SpaceNode.jsx index 44a72f3..3b5093f 100644 --- a/src/components/farm/SpaceNode.jsx +++ b/src/components/farm/SpaceNode.jsx @@ -20,6 +20,8 @@ import React from 'react' import { connect } from 'react-redux' import PlayerIcon from './PlayerIcon.jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCreditCard, faQuestionCircle } from '@fortawesome/free-solid-svg-icons' import { setMessagePanelSpace, mpMouse } from './actions.js' @@ -55,8 +57,10 @@ class SpaceNode extends React.Component { : (null)} { this.props.space.players.length ? : ''}
    - {this.props.space.description} -
    + {this.props.space.description} +
    + {this.props.showIcons && space.card === 'otb' ? (
    ) : (<>)} + {this.props.showIcons && space.card === 'fate' ? (
    ) : (<>)}
    ); diff --git a/src/components/farm/actionTypes.js b/src/components/farm/actionTypes.js index 18f334c..3a13924 100644 --- a/src/components/farm/actionTypes.js +++ b/src/components/farm/actionTypes.js @@ -36,3 +36,4 @@ export const ALERT = 'alert' export const ALERT_HANDLED = 'alert-handled' export const AUTO_SKIP = 'auto-skip' export const MESSAGE = 'message' +export const SET_HARVEST_TABLE = 'set-harvest-table' diff --git a/src/components/farm/actions.js b/src/components/farm/actions.js index 21626de..803c34a 100644 --- a/src/components/farm/actions.js +++ b/src/components/farm/actions.js @@ -20,13 +20,13 @@ import { UPDATE_GAME, UPDATE_PLAYER, GAME_STATE, SET_SELECTED_CARD, SET_CARDS, SPACE_PUSH_PLAYER, SPACE_CLEAR_PLAYERS, SET_OLD_MESSAGES, MESSAGE_PANEL_SPACE, MP_MOUSE, SET_MP_DIMS, MARK_ACTION_CHANGE_HANDLED, SET_NEXT_ACTION, MOVE_PLAYER, NEXT_UI_ACTION, NEXT_UI_ACTION_SILENT, ALERT, ALERT_HANDLED, - AUTO_SKIP, MESSAGE } from './actionTypes.js' + AUTO_SKIP, MESSAGE, SET_HARVEST_TABLE } from './actionTypes.js' export { updateGame, updatePlayer, gameState, setSelectedCard, setCards, spacePushPlayer, spaceClearPlayers, setOldMessages, setMessagePanelSpace, mpMouse, setMPDims, movePlayer, setNextAction, nextUIAction, markActionChangeHandled, nextUIActionSilent, alert, alertHandled, - autoSkip, message } + autoSkip, message, setHarvestTable } function updateGame(update) { return { type: UPDATE_GAME, @@ -107,12 +107,12 @@ function markActionChangeHandled() { return { type: MARK_ACTION_CHANGE_HANDLED }; } -function alert(value, contents) { - return { type: ALERT, value, contents }; +function alert(value, contents, id) { + return { type: ALERT, value, contents, id }; } -function alertHandled() { - return { type: ALERT_HANDLED }; +function alertHandled(id) { + return { type: ALERT_HANDLED, id }; } function autoSkip(component) { @@ -122,3 +122,7 @@ function autoSkip(component) { function message(message) { return { type: MESSAGE, message }; } + +function setHarvestTable(table) { + return { type: SET_HARVEST_TABLE, table }; +} diff --git a/src/components/farm/interface.js b/src/components/farm/interface.js index ecae5bd..0dd3825 100644 --- a/src/components/farm/interface.js +++ b/src/components/farm/interface.js @@ -24,7 +24,7 @@ import * as websocket from '../../websocket.js' import { updateGame, updatePlayer, gameState, setSelectedCard, setCards, movePlayer, setOldMessages, markActionChangeHandled, mpMouse, rolled, setNextAction, nextUIAction, nextUIActionSilent, alert, - autoSkip, message, alertHandled } from './actions.js' + autoSkip, message, alertHandled, setHarvestTable } from './actions.js' import { itemCard, fateCard } from 'game.js' export { initialize, buy, roll, endTurn, loan, trade, submitTradeAccept, @@ -47,12 +47,16 @@ function handleMessage(evt) { if (data.player.state === GAME_STATES.preTurn && data.game.otherPlayers.length > 0 && store.getState().farm.player.state !== GAME_STATES.preTurn) { - store.dispatch(alert(ALERTS.beginTurn)); + store.dispatch(alert(ALERTS.beginTurn, '', 'beginTurn' + data.game.turn)); + } + if (data.game.calledAudit && !store.getState().farm.game.calledAudit) { + store.dispatch(alert(ALERTS.auditCalled, data.game.calledAudit, + 'auditCalled' + data.game.turn)); + } + if (data.player.trade.player && data.player.trade.originator !== data.player.name) { + store.dispatch(alert(ALERTS.proposedTrade, data.player.trade.originator, + 'tradeProposed' + data.player.trade.number)); } - // else if (data.game.otherPlayers.length > 0 && - // data.game.currentPlayer !== store.getState().farm.game.currentPlayer) { - // store.dispatch(alert(ALERTS.otherPlayersTurn)); - // } // new turn clear actions if (data.event === 'update' && data.game.currentPlayer !== store.getState().farm.game.currentPlayer) { @@ -62,6 +66,7 @@ function handleMessage(evt) { store.dispatch(updatePlayer(data.player)); if (data.event === 'init') { store.dispatch(movePlayer(data.player.space, 0, data.player.color)); + store.dispatch(setHarvestTable(data.harvestTable)); } // new player(s) added to game, put them on the board if (data.game.otherPlayers.length !== store.getState().farm.game.otherPlayers.length) { @@ -75,11 +80,6 @@ function handleMessage(evt) { const oldMessages = store.getState().farm.game.messages.slice(0, 20); store.dispatch(updateGame(data.game)); store.dispatch(setOldMessages(oldMessages)); - if (data.player.cards.length > 0) { - store.dispatch(setSelectedCard(data.player.cards[0])); - } else { - store.dispatch(setSelectedCard()); - } store.dispatch(setCards(data.player.cards)); if (data.event === 'action') { if (data.player.name !== data.game.currentPlayer && @@ -94,15 +94,25 @@ function handleMessage(evt) { if (data.player.state === GAME_STATES.midTurn && data.player.cash < 0 && !store.getState().farm.ui.nextAction) { - store.dispatch(alert(ALERTS.raiseMoney)); - } else if (data.player.state === GAME_STATES.midTurn) { - store.dispatch(alertHandled()); - } + store.dispatch(alert(ALERTS.raiseMoney, '', 'raiseMoney' + data.game.turn)); + }// else if (data.player.state === GAME_STATES.midTurn && + // !data.game.state === 'finished') { + // store.dispatch(alertHandled()); + // } if (data.event === 'auto-skip') { store.dispatch(autoSkip(data.component)); } if (data.event === 'end-of-game') { - store.dispatch(alert(ALERTS.endOfGame, data.results)); + store.dispatch(alert(ALERTS.endOfGame, data.results, 'endOfGame' + data.game.turn)); + } + if (data.event === 'action' && data.action === false) { + const playerSpaces = store.getState().farm.ui.playerSpaces; + data.game.otherPlayers.map(x => x.player).concat([data.player]).forEach(player => { + if (player.space !== playerSpaces[player.color]) { + store.dispatch(movePlayer(player.space, playerSpaces[player.color], + player.color)); + } + }); } }); }; diff --git a/src/components/farm/reducers.js b/src/components/farm/reducers.js index 95f53cc..a5fb4dc 100644 --- a/src/components/farm/reducers.js +++ b/src/components/farm/reducers.js @@ -21,62 +21,63 @@ import { UPDATE_GAME, UPDATE_PLAYER, GAME_STATE, SET_SELECTED_CARD, SET_CARDS, SET_OLD_MESSAGES, MESSAGE_PANEL_SPACE, MP_MOUSE, SET_MP_DIMS, MOVE_PLAYER, SET_NEXT_ACTION, NEXT_UI_ACTION, MARK_ACTION_CHANGE_HANDLED, NEXT_UI_ACTION_SILENT, ALERT, ALERT_HANDLED, - AUTO_SKIP, MESSAGE } from './actionTypes.js' + AUTO_SKIP, MESSAGE, SET_HARVEST_TABLE } from './actionTypes.js' import { GAME_STATES } from '../../constants.js' import { spaceContent, corners } from 'game.js' const spaces = - [[corners[0], 'buy'], - ['January', 'buy'], - ['January', 'buy'], - ['January', 'buy'], - ['January', 'buy'], - ['February','buy'], - ['February', 'buy'], - ['February', 'buy'], - ['February', 'buy'], - ['March', 'buy'], - ['March', 'buy'], - ['March', 'buy'], - ['March', 'buy'], - ['April', 'buy'], - [corners[1], 'buy'], - ['April', 'none'], - ['April', 'none'], - ['May', 'none'], - ['May', 'none'], - ['May', 'hay'], - ['May', 'hay'], - ['June', 'hay'], - ['June', 'hay'], - ['June', 'cherry'], - ['June', 'cherry'], - [corners[2], 'cherry'], - ['July', 'hay'], - ['July', 'hay'], - ['July', 'hay'], - ['July', 'wheat'], - ['August', 'wheat'], - ['August', 'wheat'], - ['August', 'wheat'], - ['August', 'wheat'], - ['September', 'hay'], - ['September', 'hay'], - ['September', 'cows'], - [corners[3], 'cows'], - ['September', 'cows'], - ['October', 'cows'], - ['October', 'hay'], - ['October', 'hay'], - ['October', 'apple'], - ['November', 'apple'], - ['November', 'apple'], - ['November', 'apple'], - ['November', 'corn'], - ['December', 'corn'], - ['December', 'corn']] + [[corners[0], 'buy', false], + ['January', 'buy', false], + ['January', 'buy', 'otb'], + ['January', 'buy', false], + ['January', 'buy', false], + ['February','buy', false], + ['February', 'buy', 'fate'], + ['February', 'buy', false], + ['February', 'buy', 'otb'], + ['March', 'buy', false], + ['March', 'buy', false], + ['March', 'buy', false], + ['March', 'buy', false], + ['April', 'buy', 'otb'], + [corners[1], 'buy', false], + ['April', 'none', false], + ['April', 'none', false], + ['May', 'none', false], + ['May', 'none', false], + ['May', 'hay', false], + ['May', 'hay', 'otb'], + ['June', 'hay', false], + ['June', 'hay', false], + ['June', 'cherry', false], + ['June', 'cherry', 'fate'], + [corners[2], 'cherry', false], + ['July', 'hay', false], + ['July', 'hay', 'otb'], + ['July', 'hay', false], + ['July', 'wheat', false], + ['August', 'wheat', false], + ['August', 'wheat', false], + ['August', 'wheat', false], + ['August', 'wheat', false], + ['September', 'hay', false], + ['September', 'hay', 'otb'], + ['September', 'cows', false], + [corners[3], 'cows', false], + ['September', 'cows', false], + ['October', 'cows', false], + ['October', 'hay', 'fate'], + ['October', 'hay', 'otb'], + ['October', 'apple', 'fate'], + ['November', 'apple', 'otb'], + ['November', 'apple', false], + ['November', 'apple', false], + ['November', 'corn', false], + ['December', 'corn', false], + ['December', 'corn', 'fate']] .map((s, i) => { return { month: s[0], description: spaceContent[i], + card: s[2], type: s[1], key: i, players: [] }}); const initialState = { @@ -112,10 +113,11 @@ const initialState = { nextActionValue: null, actionChangeHandled: true, message: '', - alert: false, - alertContents: false, + alerts: {}, + unhandledAlert: false, autoSkip: false, - alertHandled: false }, + playerSpaces: {}, + harvestTable: false }, spaces: spaces, space: null, // message panel dimenions @@ -150,7 +152,10 @@ export default function(state = initialState, action) { .filter(x => x !== action.player) }; } return item; - }) + }), + ui: { ...state.ui, + playerSpaces: { ...state.ui.playerSpaces, + [action.player]: action.newSpace }} }; case SET_OLD_MESSAGES: return { ...state, oldMessages: action.messages }; @@ -179,16 +184,34 @@ export default function(state = initialState, action) { case MARK_ACTION_CHANGE_HANDLED: return { ...state, ui: { ...state.ui, actionChangeHandled: true }}; case ALERT: + let alerts = { ...state.ui.alerts, + [action.id]: { type: action.value, + id: action.id, + contents: action.contents, + handled: false } + }; return { ...state, ui: { ...state.ui, - alert: action.value, - alertContents: action.contents, - alertHandled: action.value === false ? true : false }}; + alerts, + unhandledAlert: Object.values(alerts).find(x => !x.handled) + } + }; case ALERT_HANDLED: - return { ...state, ui: { ...state.ui, alertHandled: true }}; + alerts = { ...state.ui.alerts, + [action.id]: { ...state.ui.alerts[action.id], + handled: true + } + }; + return { ...state, ui: { ...state.ui, + alerts, + unhandledAlert: Object.values(alerts).find(x => !x.handled) + } + }; case AUTO_SKIP: return { ...state, ui: { ...state.ui, autoSkip: action.component }}; case MESSAGE: return { ...state, ui: { ...state.ui, message: action.message }}; + case SET_HARVEST_TABLE: + return { ...state, ui: { ...state.ui, harvestTable: action.table }}; default: return state; } diff --git a/src/components/new-game/NewGame.jsx b/src/components/new-game/NewGame.jsx index 86ad6de..a526453 100644 --- a/src/components/new-game/NewGame.jsx +++ b/src/components/new-game/NewGame.jsx @@ -57,19 +57,23 @@ class NewGame extends React.Component { checkedColor: props.colors[0], gameId: typeof props.gameId === 'undefined' ? -1 : props.gameId, gameName: props.gameName || '', - downPayment: 0.2, - loanInterest: 0.2, + downPayment: 0, + loanInterest: 0, maxDebt: 50000, auditThreshold: 250000, startingCash: 5000, - startingDebt: 5000 + startingDebt: 5000, + trade: true }; } handleInputChange = e => { const target = e.target, - value = target.type === 'checkbox' ? target.name : target.value, - name = target.type === 'checkbox' ? 'checkedColor' : target.name; + value = target.type === 'checkbox' && target.name !== 'trade' + ? target.name : target.value, + + name = target.type === 'checkbox' && target.name !== 'trade' + ? 'checkedColor' : target.name; this.setState({ [name]: value @@ -183,6 +187,17 @@ class NewGame extends React.Component { step={1000} value={this.state.startingDebt} onChange={this.handleInputChange} /> + + + + +
    diff --git a/src/constants.js b/src/constants.js index 9e82850..94d6d30 100644 --- a/src/constants.js +++ b/src/constants.js @@ -32,4 +32,6 @@ export const messagePanelId = 'message-panel'; export const ALERTS = { beginTurn: 'begin-turn', otherPlayersTurn: 'other-players-turn', endOfGame: 'end-of-game', - raiseMoney: 'raise-money' } + auditCalled: 'audit-called', + raiseMoney: 'raise-money', + proposedTrade: 'proposed-trade' } diff --git a/src/server/farm.scm b/src/server/farm.scm index 16ed0b8..90ad452 100644 --- a/src/server/farm.scm +++ b/src/server/farm.scm @@ -60,6 +60,7 @@ (space initform: 0 accessor: player-space) (previous-space initform: 0 accessor: player-previous-space) (state initform: 'turn-ended accessor: player-state) + (finished initform: #f accessor: player-finished) (assets initform: '((hay . 10) (grain . 10) (fruit . 0) (cows . 0) (harvester . 0) (tractor . 0)) @@ -94,6 +95,7 @@ (state initform: 'playing accessor: game-state) (name initform: "game" accessor: game-name) (turn initform: 1 accessor: game-turn) + (current-player initform: #f accessor: game-current-player) (actions initform: '() accessor: game-actions) (settings initform: '((down-payment . 0.2) @@ -101,7 +103,8 @@ (max-debt . 50000) (audit-threshold . 250000) (starting-cash . 5000) - (starting-debt . 5000)) + (starting-debt . 5000) + (trade . #t)) accessor: game-settings))) (define (game-setting setting game) @@ -223,37 +226,34 @@ 'state (if (= (length (game-players game)) 0) 'pre-turn 'turn-ended)))) (set! (game-players game) (append (game-players game) (list player))) + (when (= (length (game-players game)) 1) + (set! (game-current-player game) player)) player)) (define (all-players-finished game) (null? (filter (lambda (p) - (not (eq? (player-state p) 'finished-game))) + (not (player-finished p))) (game-players game)))) -(define (next-player game player) - (let ((tail (cdr (filter (lambda (p) - (not (eq? (player-state p) 'finished-game))) - (find-tail (cut eq? <> player) (game-players game)))))) - (if (null? tail) - (car (game-players game)) - (car tail)))) +(define (next-player game) + (let ((tail (filter (lambda (p) + (not (player-finished p))) + (find-tail (cut eq? <> (game-current-player game)) + (game-players game))))) + (if (or (null? tail) (null? (cdr tail))) + (car (filter (lambda (p) + (not (player-finished p))) + (game-players game))) + (car (cdr tail))))) (define (advance-turn game player) (if (all-players-finished game) (set! (game-state game) 'finished) - (begin (set! (player-state player) 'turn-ended) - (set! (player-state (next-player game player)) 'pre-turn) - (set! (game-turn game) (+ (game-turn game) 1))))) - -(define (current-players-turn game) - (let loop ((players (game-players game))) - (cond ((null? players) ;; game finished use player 0 as a dummy player - (car (game-players game))) - ((or (eq? (player-state (car players)) 'turn-ended) - (eq? (player-state (car players)) 'finished-game)) - (loop (cdr players))) - (else - (car players))))) + (let ((next (next-player game))) + (set! (player-state player) 'turn-ended) + (set! (player-state next) 'pre-turn) + (set! (game-current-player game) next) + (set! (game-turn game) (+ (game-turn game) 1))))) (define (ridge-available? game ridge) (let loop ((players (game-players game))) @@ -326,7 +326,7 @@ (define (game->list g player) `((game . ((messages . ,(list->vector (reverse (game-messages g)))) - (currentPlayer . ,(player-name (current-players-turn g))) + (currentPlayer . ,(player-name (game-current-player g))) (otherPlayers . ,(list->vector (map @@ -340,7 +340,8 @@ (settings . ((downPayment . ,(game-setting 'down-payment g)) (loanInterest . ,(game-setting 'loan-interest g)) (maxDebt . ,(game-setting 'max-debt g)) - (auditThreshold . ,(game-setting 'audit-threshold g)))))))) + (auditThreshold . ,(game-setting 'audit-threshold g)) + (trade . ,(game-setting 'trade g)))))))) (define (push-message player msg #!key (game (session-ref (sid) 'game))) (if player @@ -405,6 +406,9 @@ (push-message player (conc "You bought " amount " " crop ".")) #t))))) +(define (make-player-year-rule id rule) + `((id . ,id) (rule . ,rule))) + (define (finish-year player #!optional (collect-wages #t)) (let ((game (session-ref (sid) 'game))) (when collect-wages @@ -417,10 +421,11 @@ (?value . "You earned $5,000 from your city job!")) (game-actions game)))) (when (game-called-audit game) - (set! (player-state player) 'finished-game) - (advance-turn game player) - ;; advance turn resets state back to turn ended - (set! (player-state player) 'finished-game)) + (set! (game-actions game) + (append (game-actions game) + `(((?action . end-game) + (?value . ,(lambda () + (set! (player-finished player) #t)))))))) (set! (player-year-rules player) (player-next-year-rules player)) (set! (player-next-year-rules player) '()) (when (not (null? (player-farmers-fates player))) @@ -436,12 +441,16 @@ (set! (game-farmers-fates game) (filter (lambda (c) (not (eq? (alist-ref 'internal-id c) 'cows-15))) (game-farmers-fates game))) - (push! `((?p cows player-action-post-harvest - ,(make-remove-farmers-fate-from-hand 'cows-15)) - (?p cows)) + (push! (make-player-year-rule + 0 + `((?p cows player-action-post-harvest + ,(make-remove-farmers-fate-from-hand 'cows-15)) + (?p cows))) (player-year-rules player)) - (push! `((?d player-action ?p - ,(make-remove-farmers-fate-after 'cows-15 40))) + (push! (make-player-year-rule + 1 + `((?d player-action ?p + ,(make-remove-farmers-fate-after 'cows-15 40)))) (player-year-rules player)))))) (define (find-player-by-name game name) @@ -489,50 +498,47 @@ '() cards))) (cond (basics - (push-message player (conc "You don't have enough " basics " to trade!")) - #f) + (conc "You don't have enough " basics " to trade!")) (other-basics - (push-message player (conc (player-name other-player) - " doesn't have enough " other-basics " to trade!")) - #f) + (conc (player-name other-player) + " doesn't have enough " other-basics " to trade!")) (ridges - (push-message player (conc ridges " ridge not available to trade!")) - #f) + (conc ridges " ridge not available to trade!")) ((< (+ (player-cash player) (alist-ref 'money params eqv? 0)) 0) - (push-message player "You don't have enough cash to trade!") - #f) + "You don't have enough cash to trade!") ((< (+ (player-cash other-player) (* (alist-ref 'money params eqv? 0) -1)) 0) - (push-message player (conc (player-name other-player) - " doesn't have enough cash to trade!")) - #f) + (conc (player-name other-player) + " doesn't have enough cash to trade!")) ((not (null? missing-cards)) - (push-message player (conc "Nobody has cards: " - (string-intersperse - (map number->string missing-cards) - ", ") ".")) - #f) + (conc "Nobody has cards: " + (string-intersperse + (map number->string missing-cards) + ", ") ".")) (else other-player)))) +(define *trade-number* 0) + (define (propose-trade game player params) (let ((other-player (validate-trade game player params))) - (if other-player + (if (not (string? other-player)) (let ((to-trade (filter (lambda (x) (and (not (equal? (cdr x) 0)) (not (equal? (cdr x) "")) (cdr x))) params))) - (push-message player - (conc "Trade proposed to " (player-name other-player) "!")) + (set! *trade-number* (+ *trade-number* 1)) (set! (player-trade other-player) (append `((player . ,(player-name player)) - (originator . ,(player-name player))) + (originator . ,(player-name player)) + (trade-number . ,*trade-number*)) to-trade)) (set! (player-trade player) (append `((player . ,(player-name other-player)) - (originator . ,(player-name player))) + (originator . ,(player-name player)) + (trade-number . ,*trade-number*)) to-trade)) #t) - #f))) + other-player))) (define (otb-by-id player id) (find (lambda (card) @@ -689,6 +695,7 @@ (when game (set! (game-messages game) '()) (set! (player-last-cash player) (player-cash player))) + (print "message type: " type) (cond ((string=? type "roll") (let ((num (+ (random 6) 1))) (when *next-roll* (set! num *next-roll*)) @@ -704,8 +711,6 @@ (when (and (> (player-previous-space player) 40) (< (player-space player) 10)) (finish-year player)) - (when (eq? (game-state game) 'finished) - (do-end-of-game game)) ;; TODO check (set! (player-harvest-mult player) 1) (let ((resp `((from . ,(player-previous-space player)) (to . ,(player-space player))))) @@ -719,9 +724,11 @@ (create-ws-response player "action" `((action . "roll") (value . ,resp)))))) ((and (string=? type "next-action") (not (eq? (player-state player) 'turn-ended))) - (let loop () - (if (null? (game-actions game)) + (let loop ((i 0)) + (if (or (null? (game-actions game)) + (>= i 15)) (begin + (set! (game-actions game) '()) (message-players! game player `((action . #f) (value . #f))) (create-ws-response player "action" '((action . #f)))) (let* ((action (car (game-actions game))) @@ -756,7 +763,7 @@ (let ((res (do-action action player))) (set! (game-actions game) (cdr (game-actions game))) (if (eq? res 'nothing) - (loop) + (loop (+ i 1)) (begin (message-players! game player @@ -772,21 +779,27 @@ (set! (game-actions game) (cdr (game-actions game))) (if (= (- (player-cash player) previous-cash) 0) - (loop) - (begin - (message-players! game player - `((action . "money") - (value . ,(- (player-cash player) - previous-cash)))) - (create-ws-response player "action" - `((action . "money") - (value . ,(- (player-cash player) - previous-cash)))))))) + (loop (+ i 1)) + (let ((res `((action . "money") + (value . ((amount . ,(- (player-cash player) + previous-cash)) + (player . ,(player-name player))))))) + (message-players! game player res) + (create-ws-response player "action" res))))) + ((eq? name 'end-game) + (if (null? (cdr (game-actions game))) + (begin + (value) + (set! (game-actions game) '())) + (set! (game-actions game) + (append (cdr (game-actions game)) + (list (car (game-actions game)))))) + (loop (+ i 1))) ((or (eq? name 'harvest-mult) (eq? name 'player-action-post-harvest)) (set! (game-actions game) (cdr (game-actions game))) (do-action action player) - (loop)) + (loop (+ i 1))) ((eq? value 'farmers-fate) (let ((ff (do-action action player))) (set! (game-actions game) @@ -800,14 +813,13 @@ (value . ,(alist-ref 'contents ff)))))) ((eq? name 'ff-money) (set! (game-actions game) (cdr (game-actions game))) - (if (= value 0) - (loop) - (begin - (message-players! game player - `((action . "money") (value . ,value))) - (create-ws-response player "action" - `((action . "money") - (value . ,value)))))) + (if (= (alist-ref 'amount value) 0) + (loop (+ i 1)) + (let ((res `((action . "money") + (value . ((amount . ,(alist-ref 'amount value)) + (player . ,(alist-ref 'name value))))))) + (message-players! game player res) + (create-ws-response player "action" res)))) ((eq? name 'ff-uncle-bert) (set! (game-actions game) (cdr (game-actions game))) (message-players! game player @@ -838,7 +850,7 @@ ((eq? name 'add-rule) (do-action action player) (set! (game-actions game) (cdr (game-actions game))) - (loop)) + (loop (+ i 1))) (else ;; TODO make error (create-ws-response player "action" `((action . ,name))))))))) ((string=? type "skip") @@ -904,9 +916,13 @@ )) (create-ws-response player "loan" '())) ((string=? type "trade") - (propose-trade game player (alist-ref 'parameters msg)) - (message-players! game player '() type: "update") - (create-ws-response player "trade" '())) + (let ((res (propose-trade game player (alist-ref 'parameters msg)))) + (if (eq? res #t) + (begin + (message-players! game player '() type: "update") + (create-ws-response player "trade" '())) + (begin (set! (player-trade player) `((error . ,res))) + (create-ws-response player "trade-error" `()))))) ((string=? type "trade-accept") (accept-trade game player) (message-players! game player '() type: "update") @@ -934,11 +950,15 @@ (message-players! game player '() type: "update") (create-ws-response player "called-audit" '())) ((string=? type "init") - (create-ws-response player "init" '())) + (create-ws-response player "init" `((harvestTable . ,(map (lambda (row) + `(,(car row) . ,(list->vector (cdr row)))) + *harvest-table*))))) ((string=? type "turn-ended") (if (>= (player-cash player) 0) (begin (advance-turn game player) - (message-players! game player '() type: "update") + (if (eq? (game-state game) 'finished) + (do-end-of-game game) + (message-players! game player '() type: "update")) (create-ws-response player "update" '())) (begin (push-message player "Cannot end a turn with negative cash!") (create-ws-response player "update" '())))) @@ -953,17 +973,18 @@ 'id (next-game-id *app*) 'otbs (setup-otbs) 'operating-expenses (setup-operating-expenses) - 'farmers-fates (setup-farmers-fates) + 'farmers-fates (setup-farmers-fates #t) 'settings - `((down-payment . ,(->pct (alist-ref 'downPayment msg) 0.2)) - (loan-interest . ,(->pct (alist-ref 'loanInterest msg) 0.2)) + `((down-payment . ,(->pct (alist-ref 'downPayment msg) 0)) + (loan-interest . ,(->pct (alist-ref 'loanInterest msg) 0)) (max-debt . ,(->i (alist-ref 'maxDebt msg) 50000)) (audit-threshold . ,(->i (alist-ref 'auditThreshold msg) 250000)) (starting-cash . ,(->i (alist-ref 'startingCash msg) - 5000)) + 0)) (starting-debt . ,(->i (alist-ref 'startingDebt msg) - 5000))))) + 0)) + (trade . ,(or (alist-ref 'trade msg) #t))))) (player (add-player-to-game game color (alist-ref 'playerName msg)))) @@ -973,11 +994,14 @@ (set-startup-otbs game player 2) (create-start-response "new-game-started"))) ((string=? type "join-game") - (let* ((color (string->symbol (alist-ref 'checkedColor msg))) - (name (alist-ref 'gameName msg)) + (let* ((name (alist-ref 'gameName msg)) (id (alist-ref 'gameId msg)) (game (find (lambda (g) (= (game-id g) id)) (app-games *app*))) + (color-raw (string->symbol (alist-ref 'checkedColor msg))) + (color (if (not (member color-raw (game-colors game))) + (car (game-colors game)) + color-raw)) (player (add-player-to-game game color (alist-ref 'playerName msg)))) @@ -1156,7 +1180,7 @@ (define (players-with asset game) (filter (lambda (player) - (player-has-asset? 'harvester player)) + (player-has-asset? asset player)) (game-players game))) (define (player-asset-binary-count asset game) @@ -1256,88 +1280,105 @@ (iota (list-ref spec 0)))) farmers-fates ff-texts))) +(define (ff-money-response amount player-name) + `((?action . ff-money) + (?value . ((amount . ,amount) + (name . ,player-name))))) + (define-syntax with-ff-money-action - (syntax-rules (?action ff-money ?value) - ((_ (player) body ...) - (let ((previous-cash (player-cash player))) + (syntax-rules () + ((_ (player game) body ...) + (let ((previous-cash (map (lambda (p) + (cons p (player-cash p))) (game-players game)))) body ... - `(((?action . ff-money) - (?value . ,(- (player-cash player) previous-cash)))))))) + `(,(ff-money-response (- (player-cash player) + (cdr (find (lambda (x) (eq? (car x) player)) previous-cash))) + (player-name player)) + ,@(map (lambda (x) + (ff-money-response (- (player-cash (car x)) (cdr x)) + (player-name (car x)))) + (filter (lambda (x) + (and (not (eq? (car x) player)) + (not (= (- (player-cash (car x)) + (cdr x)) + 0)))) + previous-cash)) + ))))) (define *farmers-fates-specs* - ;; xxx multiplayer interaction - `((1 ,(lambda (player) - (for-each (lambda (p) - (let ((roll (+ (random 6) 1))) - (if (odd? roll) - (push-message p (conc "You rolled a " roll " and escaped!")) - (begin (push-message p (conc "You rolled a " roll " and were hit!")) - ((make-player-pays (* (player-acres p) 100)) p))))) - (filter (lambda (x) (not (eq? x player))) - (game-players (session-ref (sid) 'game)))) - (with-ff-money-action (player) - ((make-player-gains-per-unit 'hay 500) player))) + `((1 ,(lambda (player game) + (with-ff-money-action (player game) + (for-each (lambda (p) + (let ((roll (+ (random 6) 1))) + (if (odd? roll) + ((make-player-pays (* (player-acres p) 100)) p)))) + (filter (lambda (x) (not (eq? x player))) + (game-players (session-ref (sid) 'game)))) + ((make-player-gains-per-unit 'hay 500) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-gains-per-unit 'grain 100) player))) #f) - (1 ,(lambda (player) - (push! '((?p wheat harvest-mult 0.5) (?p grain)) (player-year-rules player)) - (push! `((?p wheat player-action-post-harvest - ,(make-remove-farmers-fate-from-hand 'windy-spring)) - (?p grain)) + (1 ,(lambda (player game) + (push! (make-player-year-rule 2 '((?p wheat harvest-mult 0.5) (?p grain))) (player-year-rules player)) - (push! `((?d player-action ?p - ,(make-remove-farmers-fate-after 'windy-spring 34))) + (push! (make-player-year-rule + 3 + `((?p wheat player-action-post-harvest + ,(make-remove-farmers-fate-from-hand 'windy-spring)) + (?p grain))) + (player-year-rules player)) + (push! (make-player-year-rule + 4 + `((?d player-action ?p + ,(make-remove-farmers-fate-after 'windy-spring 34)))) (player-year-rules player)) '()) #t windy-spring) - (1 ,(lambda (player) + (1 ,(lambda (player game) (if (player-has-asset? 'cows player) - (with-ff-money-action (player) ((make-player-gains 2000) player)) + (with-ff-money-action (player game) ((make-player-gains 2000) player)) '())) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-gains-per-unit 'hay 100) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) ((make-player-gains 1000) player))) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-gains 1000) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) ((make-player-pays 7000) player))) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-pays 7000) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-pays-per-unit 'fruit 500) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) (let ((to-earn (* (player-acres player) 100))) (push-message player (conc "You earned $" to-earn "!")) (set! (player-cash player) (+ (player-cash player) to-earn))))) #f) - (2 ,(lambda (player) + (2 ,(lambda (player game) `(((?action . player-action) (?value . ,(lambda (player) (finish-year player #f)))) ((?action . goto) (?value . jan2)))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-pays-per-unit 'fruit 300) player))) #f) - ;; xxx multiplayer interaction - (2 ,(lambda (player) - (with-ff-money-action (player) + (2 ,(lambda (player game) + (with-ff-money-action (player game) (equipment-payout 'tractor player 3000 (session-ref (sid) 'game)))) #f) - ;; xxx multiplayer interaction - (1 ,(lambda (player) + (1 ,(lambda (player game) (if (player-has-asset? 'harvester player) - (with-ff-money-action (player) + (with-ff-money-action (player game) (for-each (lambda (from-player) (when (not (eq? player from-player)) (when (not (player-has-asset? 'harvester from-player)) @@ -1348,42 +1389,49 @@ (game-players (session-ref (sid) 'game)))) '())) #f) - (1 ,(lambda (player) - (push! '((?p ?any harvest-mult 0) (?p ?crop)) (player-year-rules player)) + (1 ,(lambda (player game) + (push! (make-player-year-rule 5 '((?p ?any harvest-mult 0) (?p ?crop))) + (player-year-rules player)) '()) #t) - (1 ,(lambda (player) + (1 ,(lambda (player game) `(((?action . ff-uncle-bert) (?value . #f)))) #f) - ;; xxx multiplayer interaction - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) (equipment-payout 'harvester player 2500 (session-ref (sid) 'game)))) #f) - (1 ,(lambda (player) - (push! `((?p cows harvest-mult 1.5) (?p cows)) (player-year-rules player)) - (push! `((?p cows harvest-mult 1.5) (?p cows)) (player-next-year-rules player)) + (1 ,(lambda (player game) + (push! (make-player-year-rule 6 `((?p cows harvest-mult 1.5) (?p cows))) + (player-year-rules player)) + (push! (make-player-year-rule 7 `((?p cows harvest-mult 1.5) (?p cows))) + (player-next-year-rules player)) '()) #t cows-15) - (1 ,(lambda (player) - (with-ff-money-action (player) ((make-player-gains 2000) player))) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-gains 2000) player))) #f) - (1 ,(lambda (player) + (1 ,(lambda (player game) (when (< (player-space player) 26) - (push! '((?p cherries harvest-mult 0.5) (?p fruit)) (player-year-rules player)) - (push! `((?p cherries player-action-post-harvest - ,(make-remove-farmers-fate-from-hand 'cherries-05)) - (?p fruit)) + (push! (make-player-year-rule 7 '((?p cherries harvest-mult 0.5) (?p fruit))) + (player-year-rules player)) + (push! (make-player-year-rule + 8 + `((?p cherries player-action-post-harvest + ,(make-remove-farmers-fate-from-hand 'cherries-05)) + (?p fruit))) (player-year-rules player))) - (push! `((?d player-action ?p - ,(make-remove-farmers-fate-after 'cherries-05 26))) + (push! (make-player-year-rule + 8 + `((?d player-action ?p + ,(make-remove-farmers-fate-after 'cherries-05 26)))) (player-year-rules player)) '()) #t cherries-05) - (1 ,(lambda (player) + (1 ,(lambda (player game) (let ((cows (player-asset 'cows player)) (ridge-cows (cows-on-ridges player))) (if (> cows ridge-cows) @@ -1398,16 +1446,19 @@ " cows slaughtered on your farm."))))) '()))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-player-pays-per-unit 'fruit 1000) player))) #f) - (1 ,(lambda (player) - (with-ff-money-action (player) ((make-semi-annual-interest-due) player))) + (1 ,(lambda (player game) + (with-ff-money-action (player game) ((make-semi-annual-interest-due) player))) #f))) -(define (setup-farmers-fates) - (shuffle (farmers-fate-spec-list->farmers-fate-cards *farmers-fates-specs* *ff-text*))) +(define (setup-farmers-fates shuffle?) + (let ((cards (farmers-fate-spec-list->farmers-fate-cards *farmers-fates-specs* *ff-text*))) + (if shuffle? + (shuffle cards) + cards))) (define *farmers-fates-cards* (farmers-fate-spec-list->farmers-fate-cards *farmers-fates-specs* *ff-text*)) ;; (define *farmers-fates* (setup-farmers-fates)) @@ -1466,10 +1517,9 @@ (length (operating-expenses-spec-list->operating-expenses-cards *operating-expenses-specs* *oe-text*))) -(define (draw-operating-expense) - (let* ((game (session-ref (sid) 'game)) - (card (list-ref (game-operating-expenses game) - (game-operating-expense-index game)))) +(define (draw-operating-expense game) + (let ((card (list-ref (game-operating-expenses game) + (game-operating-expense-index game)))) (if (= (+ (game-operating-expense-index game) 1) *total-operating-expenses*) (set! (game-operating-expense-index game) 0) (set! (game-operating-expense-index game) @@ -1497,7 +1547,7 @@ ((jan1 player-action ?p ,(make-semi-annual-interest-due))) ((jan2 draw ?p otb)) ((jan3 money ?p ,(pays 500)) (?p cows)) - ((jan4 add-rule ?p ((?p hay harvest-mult 2) (?p hay)))) + ((jan4 add-rule ?p ,(make-player-year-rule 9 '((?p hay harvest-mult 2) (?p hay))))) ((feb1 money ?p ,(gains 1000))) ((feb2 draw ?p farmers-fate)) ((feb3 goto ?p apr2)) @@ -1507,7 +1557,8 @@ ((mar3 goto ?p jan2)) ((mar4 money ?p ,(pays 2000)) (?p fruit)) ((apr1 draw ?p otb)) - ((apr2 add-rule ?p ((?p corn harvest-mult 2) (?p grain)))) + ((apr2 add-rule ?p ,(make-player-year-rule + 10 '((?p corn harvest-mult 2) (?p grain))))) ((apr3 money ?p ,(pays 500))) ((apr4 money ?p ,(pays 1000))) ((may1 money ?p ,(gains 500))) @@ -1574,7 +1625,7 @@ ((dec2 harvest ?p corn) (?p grain)) ((dec3 money ?p ,(gains 1000))) - ,@(player-year-rules player) + ,@(map (lambda (x) (alist-ref 'rule x)) (player-year-rules player)) ((?date harvest-mult ?p ?val) (?date harvest ?p ?crop) (?p ?crop harvest-mult ?val)) ((?date player-action-post-harvest ?p ?val) (?date harvest ?p ?crop) (?p ?crop player-action-post-harvest ?val)) @@ -1647,7 +1698,8 @@ (car new-otb)))) (define (do-action action player) - (let ((a (alist-ref '?action action))) + (let ((a (alist-ref '?action action)) + (game (session-ref (sid) 'game))) (cond ((eq? a 'money) (let ((changed ((alist-ref '?value action) 0))) (push-message player (conc "You " (if (>= changed 0) "earned" "paid") " $" @@ -1655,10 +1707,11 @@ (set! (player-cash player) ((alist-ref '?value action) (player-cash player)))) ((eq? a 'add-rule) - (set! (player-year-rules player) - (cons (alist-ref '?value action) (player-year-rules player))) - ;; TODO handle being added multiple times - ) + (when (not (member (alist-ref 'id (alist-ref '?value action)) + (map (lambda (x) (alist-ref 'id x)) + (player-year-rules player)))) + (set! (player-year-rules player) + (cons (alist-ref '?value action) (player-year-rules player))))) ((eq? a 'goto) (set! (player-previous-space player) (player-space player)) (set! (player-space player) @@ -1676,7 +1729,7 @@ (begin (push! (car new-ff) (player-farmers-fates player)) (set! (game-farmers-fates game) remaining-ffs)) (set! (game-farmers-fates game) (append remaining-ffs new-ff))) - `((actions . ,((alist-ref 'action (car new-ff)) player)) + `((actions . ,((alist-ref 'action (car new-ff)) player game)) (contents . ,(alist-ref 'contents (car new-ff))))))) ((or (eq? a 'player-action) (eq? a 'player-action-post-harvest)) ((alist-ref '?value action) player)) @@ -1696,22 +1749,36 @@ (player-harvest-mult player))))) (if (not (already-harvested? (alist-ref '?value action) player)) (begin - (push-message player (conc crop " Harvest! You rolled a " rolled - " and earned $" income "!")) - (when (not (= (player-harvest-mult player) 1)) - (push-message player (conc "Harvest multiplied by " (player-harvest-mult player) "!"))) (set! (player-cash player) (+ (player-cash player) income)) (set! (player-harvest-mult player) 1) - (let ((operating-expense (draw-operating-expense)) - (previous-cash (player-cash player))) + (let ((operating-expense (draw-operating-expense game)) + (previous-cash (player-cash player)) + (other-previous-cash (map (lambda (p) + (cons p (player-cash p))) + (filter + (lambda (p) + (not (string=? (player-name p) + (player-name player)))) + (game-players game))))) ((alist-ref 'action operating-expense) player) - (push-message player (alist-ref 'summary operating-expense)) `((rolled . ,rolled) (income . ,income) (operatingExpense . ,(alist-ref 'contents operating-expense)) - (operatingExpenseValue . ,(- (player-cash player) - previous-cash)) + (operatingExpenseValue . ((,(string->symbol (player-name player)) + . ,(- (player-cash player) + previous-cash)) + ,@(map (lambda (p/c) + (let ((p (car p/c))) + `(,(string->symbol (player-name p)) + . ,(- (player-cash p) + (cdr p/c))))) + (filter + (lambda (p/c) + (not (= 0 + (- (player-cash (car p/c)) + (cdr p/c))))) + other-previous-cash)))) (crop . ,(symbol->string (alist-ref '?value action))) (acres . ,acres)))) 'nothing)))))) @@ -1754,6 +1821,12 @@ ((eq? b 'goto) #f) (else #f)))))) +(define (first-game) + (car (app-games *app*))) + +(define (gp i) + (list-ref (game-players (first-game)) i)) + (cond-expand (geiser '()) @@ -1762,11 +1835,34 @@ (repl)) (compiling ;; production (run-awful) - (thread-join! *server-thread*))) + (repl) + ;; (thread-join! *server-thread*) + )) ;; TODO -;; make game finished display results. ;; make sure two players can't have the same name ;; info actions should look better ;; you can get $50 from harvest -;; bug: new websocket messages should not reset IFS card selection +;; moving bug 5 from oct 2 saw by player 2 +;; from harvest moon rolled 5 +;; finished +;; infinite loop ((?action . end-game) (?value . #)) + +;; interface.js:172 Uncaught TypeError: Cannot read property 'toFixed' of undefined +;; at formatMoney (interface.js:172) +;; at interface.js:248 +;; at Object.dispatch (redux.js:222) +;; at interface.js:94 +;; at bk (react-dom.production.min.js:224) +;; at WebSocket.handleMessage (interface.js:46) + +;; auto-skip loop +;; harvester / tractor don't say total price + +;; trade screen: person who porposed trade is wrong +;; mark spaces + +;; decling trade doesn't work +;; support trading farmers fates +;; repay loan box 1 more than max can repay +;; test tractor/harvester a lot better diff --git a/src/style.scss b/src/style.scss index 632b6ec..c8e1ee5 100644 --- a/src/style.scss +++ b/src/style.scss @@ -267,6 +267,7 @@ $tab-margin: 0.3rem; margin: 0; } .player-trade-resources .resource-unit { + display: inline-block; width: 4rem; } .card-id { @@ -546,14 +547,26 @@ $tab-margin: 0.3rem; max-width: 30rem; } @include breakpoint(large) { font-size: 23px; - max-width: 45rem; } + width: 100%; } margin-left: auto; margin-right: auto; } .tab-container { overflow-y: auto; max-height: 80vh; - flex-grow: 2; } + @include breakpoint(medium down) { + flex-grow: 2; + } + @include breakpoint(large) { + width: 40rem; + } +} + +.static-tab-container { + overflow-y: auto; + max-height: 80vh; + width: 26rem; +} .tab { $tab-border: 0.3rem solid $primary-color; @@ -581,6 +594,11 @@ $tab-margin: 0.3rem; .turn-container .button { margin: 0; } +.harvest-table { + overflow: auto; + max-width: 75vw; +} + .board-heading { z-index: -1; position: absolute; @@ -780,3 +798,9 @@ $intro-time: 6s; .hidden { display: none; } + +.space-icon { + position: absolute; + bottom: 0; + opacity: 0.5; + right: 4px; }