From 4ab1aa9220e3a135c8622880bd5b14a0e1977ab1 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Wed, 2 Mar 2022 23:27:39 -0800 Subject: [PATCH] Trading seems to be working Signed-off-by: James Ketrenos --- client/src/Resource.css | 9 +- client/src/Table.js | 20 +++- client/src/Trade.css | 73 +++++++----- client/src/Trade.js | 247 +++++++++++++++++++++++----------------- server/routes/games.js | 37 +++++- 5 files changed, 240 insertions(+), 146 deletions(-) diff --git a/client/src/Resource.css b/client/src/Resource.css index 07e64f0..f690abd 100644 --- a/client/src/Resource.css +++ b/client/src/Resource.css @@ -7,19 +7,20 @@ background-repeat: no-repeat; background-size: cover; margin: 0.25em; - cursor: pointer; display: inline-flex; justify-content: space-around; align-items: center; color: white; font-weight: bold; + pointer-events: all; } -.Resource[disabled] { - filter: grayscale(75%); +.Resource:not([disabled]) { + cursor: pointer; + pointer-events: all; } -.Resource:hover { +.Resource:not([disabled]):hover { filter: brightness(150%); } diff --git a/client/src/Table.js b/client/src/Table.js index e00a6b3..ebea03a 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -541,6 +541,10 @@ class Table extends React.Component { return this.sendAction('trade', 'accept', trade); } + cancelTrade(trade) { + return this.sendAction('trade', 'cancel', trade); + } + rejectTrade(trade) { return this.sendAction('trade', 'reject', trade); } @@ -743,7 +747,7 @@ class Table extends React.Component { if (isDead) { console.log(`Short circuiting keep-alive`); } else { - console.log(`${this.game.name} Resetting keep-alive: ${(Date.now() - this.game.startTime) / 1000}`); + console.log(`${this.game.name} Resetting keep-alive. Last ping: ${(Date.now() - this.lastPing) / 1000}`); } if (this.keepAlive) { @@ -754,7 +758,7 @@ class Table extends React.Component { } this.keepAlive = setTimeout(() => { - console.log(`${this.game.name} No ping after 10 seconds: ${(Date.now() - this.game.startTime) / 1000}`); + console.log(`${this.game.name} No ping after 10 seconds. Last ping: ${(Date.now() - this.lastPing) / 1000}`); this.setState({ noNetwork: true }); if (this.ws) { this.ws.close(); @@ -786,9 +790,10 @@ class Table extends React.Component { console.log(`Attempting WebSocket connection to ${new_uri}`); this.ws = new WebSocket(new_uri); + this.lastPing = this.game.timestamp; this.ws.addEventListener('open', (event) => { - console.log(`${this.game.name} WebSocket open: Sending game-update request: ${(Date.now() - this.game.startTime) / 1000}`); + console.log(`${this.game.name} WebSocket open: Sending game-update request: ${(Date.now() - this.lastPing) / 1000}`); this.ws.send(JSON.stringify({ type: 'game-update' })); this.resetKeepAlive(); }); @@ -814,6 +819,7 @@ class Table extends React.Component { this.setError(error); break; case 'ping': + this.lastPing = data.ping; this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping })); break; default: @@ -824,12 +830,12 @@ class Table extends React.Component { this.ws.addEventListener('error', (event) => { this.setState({ error: event.message }); - console.error(`${this.game.name} WebSocket error: ${(Date.now() - this.game.startTime) / 1000}`); + console.error(`${this.game.name} WebSocket error: ${(Date.now() - this.game.lastPing) / 1000}`); this.resetKeepAlive(true); }); this.ws.addEventListener('close', (event) => { - console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.startTime) / 1000}`); + console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.lastPing) / 1000}`); this.setState({ error: event.message }); this.resetKeepAlive(true); }); @@ -910,6 +916,10 @@ class Table extends React.Component { } componentWillUnmount() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } if (this.loadTimer) { clearTimeout(this.loadTimer); this.loadTimer = 0; diff --git a/client/src/Trade.css b/client/src/Trade.css index 2f02e28..e4edc95 100644 --- a/client/src/Trade.css +++ b/client/src/Trade.css @@ -32,12 +32,6 @@ margin: 0.5em 0; } -.Trade .Resource { - cursor: pointer; - width: 3.75em; /* 5x7 aspect ratio */ - height: 3.75em; -} - .Trade .Resource { display: inline-flex; align-items: center; @@ -53,6 +47,11 @@ margin: 0.75rem; } +.Trade b { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + .Trade .Direction { display: flex; flex-direction: column; @@ -64,6 +63,14 @@ } .Trade .Resource:hover { + filter: none; +} + +.Trade .Transfer { + pointer-events: none; +} + +.Trade .Transfer .Resource:hover { filter: brightness(125%); } @@ -110,42 +117,52 @@ user-select: none; } -.TradePlayer { - display: flex; - flex-direction: row; - width: 100%; - align-items: center; - padding: 2px 0; -} - -.TradePlayer > * { - margin-right: 0.25em; -} - .Trade .Transfers { display: flex; flex-direction: row; justify-items: flex-start; } -.Trade .ResourceCounter { - display: flex; - flex-direction: column; -} - -.TradeLine { +.Trade .TradeLine { + width: 100%; user-select: none; display: flex; flex-direction: row; align-items: center; flex: 1; + padding: 2px 0; } -.TradeLine > div { - display: flex; - flex-grow: 1; +.Trade .TradeLine > div { + display: inline-flex; + align-items: center; + margin-right: 0.25rem; } -.TradeLine > div > div { +.Trade .TradeLine > div > div { margin-left: 0.25em; } + +.Trade .TradeLine .Resource { + height: 1.5rem; + width: 1.5rem; + min-width: 1.5rem; + min-height: 1.5rem; +} + +.Trade .TradeLine .Resource > div { + font-size: 0.75rem; + width: 1rem; + height: 1rem; + line-height: 1rem; +} + +.Trade .TradeLine .TradeActions { + flex-grow: 1; + display: inline-flex; + justify-content: flex-end; +} + +.Trade .TradeLine .TradeActions > * { + margin-left: 0.5rem; +} \ No newline at end of file diff --git a/client/src/Trade.js b/client/src/Trade.js index 8a6c358..20c6b5c 100644 --- a/client/src/Trade.js +++ b/client/src/Trade.js @@ -71,21 +71,61 @@ const Trade = ({table}) => { gets: offer.gives.slice() }; let _gives = {}, _gets = {}; + console.log(gives, gets); trade.gives.forEach(give => _gives[give.type] = give.count); trade.gets.forEach(get => _gets[get.type] = get.count); table.offerTrade(trade); - setGives(_gives); - setGets(_gets); + console.log(_gives, _gets); + setGives(Object.assign({}, empty, _gives)); + setGets(Object.assign({}, empty, _gets)); }, [setGives, setGets, table]); let transfers = []; transfers = [ 'brick', 'wood', 'wheat', 'sheep', 'stone' ].map(resource => { return createTransfer(resource); }); - if (!table.game) { - return (<>); + if (!table.game || !player) { + return <>; } + const canMeetOffer = (player, offer) => { + for (let i = 0; i < offer.gets.length; i++) { + const get = offer.gets[i]; + if (player[get.type] < get.count) { + return false; + } + } + return true; + }; + + const isCompatibleOffer = (player, offer) => { + let valid = player.gets.length === offer.gives.length && + player.gives.length === offer.gets.length; + + if (!valid) { + return false; + } + + player.gets.forEach(get => { + if (!valid) { + return; + } + valid = offer.gives.find(item => + (item.type === get.type || item.type === '*') && + item.count === get.count) !== undefined; + }); + + if (valid) player.gives.forEach(give => { + if (!valid) { + return; + } + valid = offer.gets.find(item => + (item.type === give.type || item.type === 'bank') && + item.count === give.count) !== undefined; + }); + return valid; + }; + const game = table.game; const isTurn = (table.game.turn && table.game.turn.color === table.game.color) ? true : false; @@ -108,6 +148,10 @@ const Trade = ({table}) => { table.offerTrade(trade); } + const cancelOffer = (offer) => { + table.cancelTrade(offer); + } + const acceptClicked = (offer) => { table.acceptTrade(offer); }; @@ -116,32 +160,61 @@ const Trade = ({table}) => { table.cancelTrading(); } - /* Non-current player has rejected the active player's - * bid */ + /* Player has rejected the active player's bid or active player rejected + * the other player's bid */ const rejectClicked = (trade) => { table.rejectTrade(trade); } + /* Create list of players with active trades */ let players = []; for (let color in table.game.players) { const item = table.game.players[color], name = getPlayerName(table.game.sessions, color); - if (name && table.game.name !== name) { - players.push({ - name: name, - color: color, - valid: false, - gets: item.gets ? item.gets : [], - gives: item.gives ? item.gives : [], - offerRejected: item.offerRejected ? true : false - }); + + /* Only list players with an offer */ + if (table.game.turn.name !== name && + (!item.gets || item.gets.length === 0 + || !item.gives || item.gives.lenght === 0)) { + continue; } + + if (table.game.turn.name === name && item.negotiatorRejectedOffer) { + continue; + } + + const tmp = { + self: table.game.name === name, + name: name, + color: color, + valid: false, + gets: item.gets ? item.gets : [], + gives: item.gives ? item.gives : [], + offerRejected: (table.game.turn.name !== name) ? (item.offerRejected ? true : false) : false, + negotiatorRejectedOffer: item.negotiatorRejectedOffer + }; + tmp.canSubmit = (item.gets.length && item.gives.length); + players.push(tmp); } players.sort((A, B) => { return A.name.localeCompare(B.name); }); + const trade = {gives: [], gets: []}; + for (let type in gives) { + if (gives[type]) { + trade.gets.push({ type, count: gives[type]}); + } + } + for (let type in gets) { + if (gets[type]) { + trade.gives.push({ type, count: gets[type]}); + } + } + const isOfferSubmitted = isCompatibleOffer(table.game.player, trade), + isOfferValid = trade.gives.length && trade.gets.length; + if (isTurn && table.game.player && table.game.player.banks) { table.game.player.banks.forEach(bank => { const count = (bank === 'bank') ? 3 : 2; @@ -162,106 +235,70 @@ const Trade = ({table}) => { valid: false }); } - - if (!player) { - return <>; - } - let canAccept = false; - if (table.game.turn.offer) { - players.forEach(trade => { - trade.valid = trade.gets.length - && trade.gives.length - && trade.gets.length === game.turn.offer.gives.length - && trade.gives.length === game.turn.offer.gets.length; - if (!trade.valid) { - return; + if (isTurn) { + players.forEach(trade => trade.valid = isCompatibleOffer(table.game.turn.offer, trade)); + } else { + const found = players.find(item => item.name === table.game.turn.name); + if (found) { + found.valid = canMeetOffer(player, table.game.turn.offer); } - trade.gets.forEach(get => { - if (!trade.valid) { - return; - } - if (get.type !== 'bank') { - const offer = table.game.turn.offer.gives.find(give => give.type === get.type); - trade.valid = offer && (offer.count === get.count); - } else { - /* Doesn't matter what the resource type is so long as there - * are enough of the one kind */ - trade.valid = table.game.turn.offer.gives[0].count === get.count; - } - }); - - if (!trade.valid) { - return; - } - - trade.gives.forEach(give => { - if (!trade.valid) { - return; - } - if (give.type !== '*') { - const offer = table.game.turn.offer.gets.find(get => give.type === get.type); - trade.valid = offer && (offer.count === give.count); - } else { - /* Doesn't matter what the resource type is so long as there - * are enough of the one kind */ - trade.valid = table.game.turn.offer.gets[0].count === give.count; - } - }) - }); - - canAccept = true; - table.game.turn.offer.gets.forEach(item => { - if (!canAccept) { - canAccept = (item.type in game.player); - } - if (!canAccept) { - return; - } - canAccept = (game.player[item.type] >= item.count); - }); + } } players = players.map((item, index) => { - if (item.offerRejected) { - return
- -
{item.name}
-
- has rejected your offer. -
-
; + if (item.negotiatorRejectedOffer && isTurn) { + return <>; } - - const _gets = item.gets.map(get => - `${get.count} ${(get.type === 'bank') ? 'of any one resource' : get.type}`) - .join(', '), - _gives = item.gives.map(give => - `${give.count} ${(give.type === '*') ? 'of any resource' : give.type}`) - .join(', '); + + const rejected = (item.offerRejected || item.negotiatorRejectedOffer); + + const _gets = item.gets.length ? item.gets.map((get, index) =>
+ { get.type === 'bank' &&
4 of any resource
} + { get.type !== 'bank' && } +
) : undefined, + _gives = item.gives.length ? item.gives.map((give, index) =>
+ { give.type === '*' && <>1 of any resource} + { give.type !== '*' && } +
) : undefined + return ( -
+
-
{item.name}
-
- { _gets !== '' && _gives !== '' && -
wants {_gets} and will give {_gives}. -
+ { rejected && <>Your offer to give {_gets} in exchange for {_gives} was rejected. } + { !rejected && <>{item.self ? 'You' : item.name} } + { !rejected && _gets && _gives && + <>{item.self ? ' want' : ' wants'} {_gets} and will give {_gives}. + } + { !rejected && (_gets === undefined || _gives === undefined) && + <>{item.self ? ' have' : ' has'} not submitted a trade offer. + } + +
+ { !item.self && isTurn && + } - { (_gets === '' || _gives === '') && -
has not submitted a trade offer. -
+ + { !isTurn && item.color === table.game.turn.color && + + } + + { item.name !== 'The bank' && !item.self && + + } + + { item.self && + + } + + { item.self && + } - { isTurn && } - { !isTurn && item.color === table.game.turn.color && <> - - - }
); @@ -278,7 +315,7 @@ const Trade = ({table}) => {
{ !player.haveResources && You have no resources to participate in this trade. } + onClick={offerClicked}>Offer { player.haveResources &&
diff --git a/server/routes/games.js b/server/routes/games.js index 4cd0236..65d7a42 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -176,6 +176,16 @@ const playerFromColor = (game, color) => { return undefined; }; +const playerFromName = (game, name) => { + for (let id in game.sessions) { + if (game.sessions[id].name === name) { + return game.sessions[id].player; + } + } + return undefined; +}; + + const processGameOrder = (game, player, dice) => { let message; @@ -1554,6 +1564,7 @@ router.put("/:id/:action/:value?", async (req, res) => { game.players[key].gives = []; game.players[key].gets = []; delete game.players[key].offerRejected; + delete game.players[key].negotiatorRejectedOffer; } addActivity(game, session, `${name} requested to begin trading negotiations.`); break; @@ -1589,6 +1600,8 @@ router.put("/:id/:action/:value?", async (req, res) => { session.player.gives = offer.gives; session.player.gets = offer.gets; + delete session.player.offerRejected; + delete session.player.negotiatorRejectedOffer; if (game.turn.name === name) { /* This is a new offer from the active player -- reset everyone's @@ -1604,8 +1617,23 @@ router.put("/:id/:action/:value?", async (req, res) => { /* Any player can reject an offer */ if (value === 'reject') { - session.player.offerRejected = true; - addActivity(game, session, `${session.name} rejected ${game.turn.name}'s offer.`); + const offer = req.body; + + /* If the active player rejected an offer, they rejected another player */ + if (game.turn.name === name) { + console.log(`Rejected `, offer); + const other = playerFromName(game, offer.name); + if (other) { + other.negotiatorRejectedOffer = true; + } else { + console.log(`Could not find ${offer.name}`); + } + addActivity(game, session, `${session.name} rejected ${offer.name}'s offer.`); + } else { + session.player.offerRejected = true; + addActivity(game, session, `${session.name} rejected ${game.turn.name}'s offer.`); + } + break; } @@ -1674,10 +1702,11 @@ router.put("/:id/:action/:value?", async (req, res) => { player[item.type] -= item.count; }); + const from = (offer.name === 'The bank') ? 'the bank' : offer.name; addChatMessage(game, session, `${session.name} traded ` + ` ${offerToString(session.player)} ` + - `from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`); - + `from ${from}.`); + addActivity(game, session, `${session.name} accepted a trade from ${from}.`) delete game.turn.offer; if (target) { delete target.gives;