@dumpster-fire/game
Advanced tools
Comparing version 1.3.1 to 1.4.0
{ | ||
"name": "@dumpster-fire/game", | ||
"version": "1.3.1", | ||
"version": "1.4.0", | ||
"main": "src/index", | ||
@@ -23,3 +23,3 @@ "dependencies": { | ||
}, | ||
"gitHead": "19fb1330a526ced3b1922555b77cf7e9f7077813" | ||
"gitHead": "996b127dd4118fd0511f9aaa51d9f8934bb67f89" | ||
} |
396
src/index.js
@@ -1,75 +0,37 @@ | ||
import uuid from "uuid/v4"; | ||
import { Game, PlayerView } from "boardgame.io/core"; | ||
import { CardType, getCardFromType } from "@dumpster-fire/cards"; | ||
import { CardType } from "@dumpster-fire/cards"; | ||
import { MAX_HAND_SIZE, STARTING_HAND_SIZE } from "@dumpster-fire/constants"; | ||
import createDeck from "./util/createDeck"; | ||
import createPublicCard from "./util/createPublicCard"; | ||
import insertIntoDeck from "./util/insertIntoDeck"; | ||
import insertRandomlyIntoDeck from "./util/insertRandomlyIntoDeck"; | ||
import isPlayerUnderHandLimit from "./util/isPlayerUnderHandLimit"; | ||
import revealPreviewCards from "./util/revealPreviewCards"; | ||
import findCard from "./util/findCard"; | ||
import discardCard from "./util/discardCard"; | ||
import drawCard from "./util/drawCard"; | ||
import addCardToHand from "./util/addCardToHand"; | ||
import validators from "./validators"; | ||
const STARTING_HAND_SIZE = 3; | ||
const MAX_HAND_SIZE = 3; | ||
const DISTRIBUTION = { | ||
4: { | ||
[CardType.INCIDENT]: 11, | ||
[CardType.FORCE_PUSH]: 3, | ||
[CardType.PULL_REQUEST]: 7, | ||
[CardType.CODE_REVIEW]: 7, | ||
[CardType.RETRO]: 3, | ||
[CardType.BLAME]: 4, | ||
[CardType.QUICK_MEETING]: 3, | ||
[CardType.MONITOR]: 4, | ||
[CardType.REPRIORITIZE]: 3, | ||
}, | ||
const defaultPhaseState = { | ||
played: [], | ||
}; | ||
const createDeck = numPlayers => { | ||
if (!DISTRIBUTION[numPlayers]) { | ||
throw new Error("Invalid number of players"); | ||
const doesPlayerHaveCard = (G, ctx, card, playerID) => | ||
G.players[playerID || ctx.currentPlayer].hand.find(findCard(card)); | ||
const checkPlayerHasCard = (...args) => { | ||
if (!doesPlayerHaveCard(...args)) { | ||
throw new Error("You don't own that card, cheater."); | ||
} | ||
const createCard = cardType => { | ||
return { ...getCardFromType(cardType), id: uuid() }; | ||
}; | ||
const createCards = ([cardType, number]) => | ||
[...Array(number)].map(() => createCard(cardType)); | ||
const entries = Object.entries(DISTRIBUTION[numPlayers]); | ||
const incidents = entries.find(([type]) => type === CardType.INCIDENT); | ||
const actions = entries.filter(([type]) => type !== CardType.INCIDENT); | ||
const incidentCards = createCards(incidents); | ||
const actionCards = actions | ||
.map(createCards) | ||
.reduce((acc, cards) => [...acc, ...cards], []); | ||
return [incidentCards, actionCards]; | ||
}; | ||
// Remove card from player's hand and add to discard pile | ||
const discard = (G, ctx, card) => { | ||
G.players[ctx.currentPlayer].hand = G.players[ctx.currentPlayer].hand.filter( | ||
({ id }) => id !== card.id | ||
); | ||
G.discard.push(card); | ||
const checkHasPlayedCard = (G, ctx, card) => { | ||
if (G.phaseState.played.find(findCard(card))) { | ||
throw new Error("You have already played this card, cheater."); | ||
} | ||
}; | ||
const isPlayerUnderHandLimit = (G, ctx) => { | ||
// Don't count incidents as part of hand | ||
// Don't count played cards as part of hand | ||
console.log(G.players[ctx.currentPlayer].hand, ctx.currentPlayer); | ||
const actions = G.players[ctx.currentPlayer].hand.filter( | ||
({ type }) => type !== CardType.INCIDENT | ||
); | ||
const played = G.phaseState.played; | ||
return actions.length - played.length <= MAX_HAND_SIZE; | ||
}; | ||
const defaultPhaseState = { | ||
played: [], | ||
preview: [], | ||
}; | ||
const game = Game({ | ||
@@ -86,29 +48,59 @@ name: "dumpster-fire", | ||
const incidentState = {}; | ||
const publicHands = {}; | ||
[...Array(ctx.numPlayers)].forEach((_, i) => { | ||
const incident = incidents.shift(); | ||
const hand = [ | ||
incident, | ||
...[...Array(STARTING_HAND_SIZE)].map(() => actions.shift()), | ||
]; | ||
const hand = [...Array(STARTING_HAND_SIZE)].map(() => actions.shift()); | ||
incidentState[`${i}`] = [incident]; | ||
playerState[`${i}`] = { | ||
hand, | ||
preview: [], | ||
}; | ||
publicHands[`${i}`] = hand.map(createPublicCard); | ||
}); | ||
const deck = ctx.random.Shuffle([...incidents, ...actions]); | ||
const publicDeck = deck.map(createPublicCard); | ||
return { | ||
// Holds state for each player | ||
players: playerState, | ||
// Public incidents state | ||
incidents: incidentState, | ||
// This does not get shared to clients at all | ||
secret: { | ||
deck, | ||
// Full deck details | ||
/// Map of card id -> card | ||
deck: new Map(deck.map(card => [card.id, card])), | ||
}, | ||
deck, | ||
// The public deck, is ordered and only contains the id | ||
deck: publicDeck, | ||
// This is a map of playerID -> their public hand (e.g. only ids) | ||
publicHands, | ||
// The detailed last played hand | ||
lastPlayed: [], | ||
// Discard pile (public) | ||
discard: [], | ||
// Pull request list | ||
pullRequests: [], | ||
// State that gets reset on every turn | ||
phaseState: { | ||
...defaultPhaseState, | ||
}, | ||
// The direction of play (-1 for reverse order) | ||
turnDirection: 1, | ||
incidents: incidentState, | ||
// Maximum number of cards you can have at the end of the draw phase | ||
maxHandSize: MAX_HAND_SIZE, | ||
@@ -120,12 +112,3 @@ }; | ||
drawCard: (G, ctx) => { | ||
let card; | ||
if (G.phaseState.retro) { | ||
// Draw from discard | ||
card = G.discard.shift(); | ||
} else { | ||
card = G.deck.shift(); | ||
} | ||
G.players[ctx.currentPlayer].hand.push(card); | ||
drawCard(G, ctx, ctx.currentPlayer); | ||
}, | ||
@@ -136,8 +119,18 @@ | ||
if (!G.phaseState.canSkipDraw) { | ||
throw new Error("You need to draw a card first."); | ||
throw new Error("You need to draw a card."); | ||
} | ||
ctx.events.endPhase(); | ||
}, | ||
endTurn: (G, ctx) => {}, | ||
discard: (G, ctx, card) => { | ||
if (card.type === CardType.INCIDENT) { | ||
throw new Error("You can't discard an Incident, cheater."); | ||
} | ||
checkPlayerHasCard(G, ctx, card); | ||
// TODO: make sure player owns card and that it is not an incident | ||
discardCard(G, ctx, card, ctx.currentPlayer); | ||
}, | ||
finishAction: (G, ctx) => { | ||
@@ -148,27 +141,85 @@ // finish playing cards, go to draw phase | ||
reshuffle: (G, ctx, newOrder) => { | ||
const { preview, reshuffle } = G.phaseState; | ||
[CardType.BLAME]: (G, ctx, target) => { | ||
if (!G.incidents.hasOwnProperty(target)) { | ||
throw new Error("Invalid player to blame"); | ||
} | ||
if (!reshuffle) { | ||
throw new Error("Not allowed to reshuffle right now"); | ||
if (G.incidents[target].length > 1) { | ||
throw new Error("You cannot blame a player with more than 1 incident."); | ||
} | ||
const shuffled = [ | ||
preview.find(card => card.id === newOrder.bottom), | ||
preview.find(card => card.id === newOrder.middle), | ||
preview.find(card => card.id === newOrder.top), | ||
]; | ||
shuffled.forEach(card => G.deck.unshift(card)); | ||
const incident = G.incidents[ctx.currentPlayer].shift(); | ||
G.incidents[target].push(incident); | ||
ctx.events.endPhase(); | ||
}, | ||
G.phaseState.preview = []; | ||
G.phaseState.reshuffle = false; | ||
[CardType.MONITOR]: (G, ctx, newOrder) => { | ||
const { preview } = G.players[ctx.currentPlayer]; | ||
const previewCardIds = preview.map(({ id }) => id); | ||
// make sure the shuffled cards match the old cards | ||
if (!newOrder.every(newCard => previewCardIds.includes(newCard.id))) { | ||
throw new Error("Stop cheating."); | ||
} | ||
G.deck = [...newOrder.map(({ id }) => ({ id })), ...G.deck]; | ||
// TODO: Clean preview by creating new ids for cards | ||
G.players[ctx.currentPlayer].preview = []; | ||
ctx.events.endPhase(); | ||
}, | ||
[CardType.QUICK_MEETING]: (G, ctx) => { | ||
// TODO: Clean preview by creating new ids for cards | ||
G.players[ctx.currentPlayer].preview = []; | ||
ctx.events.endPhase(); | ||
}, | ||
[CardType.CODE_REVIEW]: (G, ctx, card, pullRequestObj, deckLocation) => { | ||
// Choose any Pull Request in play. | ||
// If you are the author of that Pull Request: | ||
// - resolve the Incident by shuffling it into the deck | ||
// If you are not the author: | ||
// - resolve the Incident by placing it anywhere in the deck | ||
// - take the Pull Request and put it in your hand | ||
if (!pullRequestObj) { | ||
throw new Error("Select a Pull Request to Code Review"); | ||
} | ||
if (deckLocation < 0 || deckLocation >= G.deck.length) { | ||
throw new Error("Invalid deck location"); | ||
} | ||
const [author, originalPullRequestCard, incident] = pullRequestObj; | ||
if (ctx.currentPlayer === author) { | ||
insertRandomlyIntoDeck(G, ctx, incident); | ||
discardCard(G, ctx, originalPullRequestCard, ctx.currentPlayer); | ||
} else { | ||
insertIntoDeck(G, ctx, incident, deckLocation); | ||
// Player should pick up the PR into their hand | ||
addCardToHand(G, ctx, originalPullRequestCard, ctx.currentPlayer); | ||
} | ||
// Remove pull request | ||
G.pullRequests = G.pullRequests.filter( | ||
pullRequest => | ||
pullRequest[1].id !== card.id && pullRequest[2].id !== incident.id | ||
); | ||
}, | ||
playCard: (G, ctx, card, ...args) => { | ||
console.log(`Player #${ctx.playerID} playing: ${card.type}`); | ||
// TODO: Check that player actually owns the card (in `flow`) | ||
checkPlayerHasCard(G, ctx, card); | ||
// Card gets added to `phaseState.played` at the end of the function | ||
// so check here if card has been played this turn already | ||
checkHasPlayedCard(G, ctx, card); | ||
// Check validation logic first | ||
if (validators[card.type] === "function") { | ||
if (typeof validators[card.type] === "function") { | ||
validators[card.type](G, ctx, card, ...args); | ||
@@ -180,4 +231,4 @@ } | ||
[CardType.RETRO]: (G, ctx, card) => { | ||
G.phaseState.canDrawFromDiscard = true; | ||
G.phaseState.retro = true; | ||
G.phaseState.played.push(card); | ||
}, | ||
@@ -187,3 +238,3 @@ | ||
G.turnDirection = G.turnDirection * -1; | ||
G.phaseState.played.push(card); | ||
ctx.events.endTurn(); | ||
}, | ||
@@ -201,66 +252,27 @@ | ||
[CardType.CODE_REVIEW]: ( | ||
G, | ||
ctx, | ||
[author, card, incident, turn], | ||
deckLocation | ||
) => { | ||
// Choose any Pull Request in play. | ||
// If you are the author of that Pull Request: | ||
// - resolve the Incident by shuffling it into the deck | ||
// If you are not the author: | ||
// - resolve the Incident by placing it anywhere in the deck | ||
// - take the Pull Request and put it in your hand | ||
if (ctx.playerId === author) { | ||
insertRandomlyIntoDeck(G, ctx, incident); | ||
discard(G, ctx, card); | ||
} else { | ||
insertIntoDeck(G, ctx, incident, deckLocation); | ||
// Player should pick up the PR into their hand | ||
G.player[ctx.playerId].hand.push(card); | ||
} | ||
// Remove pull request | ||
G.pullRequests = G.pullRequests.filter( | ||
pullRequest => | ||
pullRequest[1].id !== card.id && pullRequest[2].id !== incident.id | ||
); | ||
[CardType.CODE_REVIEW]: (G, ctx, card) => { | ||
ctx.events.endPhase({ next: CardType.CODE_REVIEW }); | ||
}, | ||
[CardType.BLAME]: (G, ctx, card, target) => { | ||
G.phaseState.played.push(card); | ||
const incident = G.incidents[ctx.playerID].shift(); | ||
// Remove incident from player's hand | ||
G.players[ctx.playerID].hand = G.players[ctx.playerID].hand.filter( | ||
({ id }) => id !== incident.id | ||
); | ||
// Add to target player | ||
if (G.players[target]) { | ||
G.players[target].hand.push(incident); | ||
} | ||
G.incidents[target].push(incident); | ||
ctx.events.endPhase({ next: CardType.BLAME }); | ||
}, | ||
[CardType.QUICK_MEETING]: (G, ctx, card) => { | ||
G.phaseState.played.push(card); | ||
const nextCard = G.deck[0]; | ||
// TODO handle security with card (e.g. player can see card id and trace | ||
// the card throughout future turns) | ||
G.phaseState.preview.push(nextCard); | ||
G.players[ctx.currentPlayer].preview = [nextCard]; | ||
G.phaseState.canSkipDraw = true; | ||
ctx.events.endPhase({ next: CardType.QUICK_MEETING }); | ||
}, | ||
[CardType.MONITOR]: (G, ctx, card) => { | ||
G.phaseState.played.push(card); | ||
// TODO handle security with card (e.g. player can see card id and trace | ||
// the card throughout future turns) | ||
// Prob just needs new ids when we preview | ||
G.phaseState.preview = G.deck.splice(0, 3); | ||
G.players[ctx.currentPlayer].preview = G.deck.splice(0, 3); | ||
G.phaseState.reshuffle = true; | ||
ctx.events.endPhase({ next: "monitor" }); | ||
}, | ||
@@ -276,3 +288,3 @@ | ||
// Discard incident | ||
discard(G, ctx, incident); | ||
discardCard(G, ctx, incident, ctx.currentPlayer); | ||
@@ -290,12 +302,5 @@ // Insert incident back into the deck | ||
G.phaseState.played.push(card); | ||
logic[card.type](G, ctx, card, ...args); | ||
G.phaseState._cardPlayed = card.type; | ||
}, | ||
discard: (G, ctx, card) => { | ||
console.log("discard"); | ||
// TODO: make sure player owns card and that it is not an incident | ||
discard(G, ctx, card); | ||
}, | ||
}, | ||
@@ -313,8 +318,3 @@ | ||
onTurnBegin: (G, ctx) => { | ||
// Always start in "play" phase | ||
// if (ctx.phase !== "play") { | ||
// ctx.events.endPhase({ next: "play" }); | ||
// } | ||
}, | ||
onTurnBegin: (G, ctx) => {}, | ||
@@ -324,5 +324,9 @@ onTurnEnd: (G, ctx) => { | ||
G.phaseState.played.forEach(card => { | ||
discard(G, ctx, card); | ||
discardCard(G, ctx, card); | ||
// Save lastPlayed hand | ||
G.lastPlayed.push(card); | ||
}); | ||
// TODO: Look for `preview` hand and give them new ids so they can't be tracked | ||
// Reset phaseState | ||
@@ -335,20 +339,46 @@ G.phaseState = { | ||
onMove: (G, ctx, { type, args, playerID }) => { | ||
console.log("on move", ctx.currentPlayer, playerID, args); | ||
if (type === "drawCard") { | ||
const { drawnCard } = G.phaseState; | ||
if (type === "finishAction") { | ||
console.log("finishAction called, ending phase"); | ||
// ctx.events.endPhase(); | ||
} else if (type === "skipDraw") { | ||
// Fetch the actual card from master deck | ||
const fullCard = | ||
G.secret.deck.get(drawnCard.id) || | ||
(G.phaseState.drawFromDiscard && | ||
((!!drawnCard.type && drawnCard) || | ||
G.discard.find(findCard(drawnCard)))); | ||
if (!fullCard) { | ||
throw new Error("Stop cheating."); | ||
} | ||
const cardIndex = G.players[playerID].hand.findIndex( | ||
findCard(fullCard) | ||
); | ||
// Full card not found in players hand | ||
if (cardIndex === -1) { | ||
throw new Error("Stop cheating."); | ||
} | ||
// Check if incident and add to incidents array | ||
if (fullCard.type === CardType.INCIDENT) { | ||
G.incidents[playerID].push(fullCard); | ||
// Remove from hand because incidents do not reside in player's hand | ||
G.players[playerID].hand = G.players[playerID].hand.filter( | ||
findCard(fullCard) | ||
); | ||
} else { | ||
// Update players hand with the full card | ||
G.players[playerID].hand[cardIndex] = { ...fullCard }; | ||
} | ||
ctx.events.endPhase(); | ||
} else if (type === "drawCard") { | ||
ctx.events.endPhase(); | ||
} else if (type === "discard") { | ||
} else if (type === "playCard") { | ||
if ( | ||
G.phaseState._cardPlayed === CardType.REPRIORITIZE || | ||
G.phaseState._cardPlayed === CardType.PULL_REQUEST | ||
) { | ||
console.log(G.phaseState._cardPlayed, " end turn immediately"); | ||
ctx.events.endTurn(); | ||
const [card] = args; | ||
if (card.type === CardType.MONITOR) { | ||
revealPreviewCards(G, ctx, playerID); | ||
} | ||
if (card.type === CardType.QUICK_MEETING) { | ||
revealPreviewCards(G, ctx, playerID); | ||
} | ||
} | ||
@@ -359,3 +389,3 @@ }, | ||
play: { | ||
allowedMoves: ["playCard", "reshuffle", "finishAction"], | ||
allowedMoves: ["playCard", "finishAction"], | ||
next: "draw", | ||
@@ -384,2 +414,22 @@ onPhaseBegin: (G, ctx) => { | ||
}, | ||
[CardType.BLAME]: { | ||
allowedMoves: [CardType.BLAME], | ||
next: "play", | ||
}, | ||
[CardType.CODE_REVIEW]: { | ||
allowedMoves: [CardType.CODE_REVIEW], | ||
next: "play", | ||
}, | ||
[CardType.MONITOR]: { | ||
allowedMoves: [CardType.MONITOR], | ||
next: "play", | ||
}, | ||
[CardType.QUICK_MEETING]: { | ||
allowedMoves: [CardType.QUICK_MEETING], | ||
next: "play", | ||
}, | ||
}, | ||
@@ -386,0 +436,0 @@ |
@@ -1,3 +0,5 @@ | ||
export default function insertIntoDeck(G, ctx, card, location) { | ||
G.deck.splice(location, 0, card); | ||
import createPublicCard from "./createPublicCard"; | ||
export default function insertIntoDeck(G, _ctx, card, location) { | ||
G.deck.splice(location, 0, createPublicCard(card)); | ||
} |
@@ -13,5 +13,5 @@ import { CardType } from "@dumpster-fire/cards"; | ||
[CardType.PULL_REQUEST]: (G, ctx, card) => { | ||
if (G.incidents[ctx.playerID].length < 1) { | ||
if (G.incidents[ctx.currentPlayer].length < 1) { | ||
throw new Error( | ||
"You can only submit a Pull Request if you have an incident" | ||
"You can only submit a Pull Request if you have an Incident" | ||
); | ||
@@ -21,32 +21,18 @@ } | ||
[CardType.CODE_REVIEW]: ( | ||
G, | ||
ctx, | ||
[author, card, incident, turn], | ||
deckLocation | ||
) => { | ||
// Choose any Pull Request in play. | ||
// If you are the author of that Pull Request: | ||
// - resolve the Incident by shuffling it into the deck | ||
// If you are not the author: | ||
// - resolve the Incident by placing it anywhere in the deck | ||
// - take the Pull Request and put it in your hand | ||
if (!G.pullRequests.length) { | ||
[CardType.CODE_REVIEW]: (G, ctx, card, pullRequestObj, deckLocation) => { | ||
if ( | ||
!G.pullRequests.length || | ||
!G.pullRequests.filter( | ||
([author, card, incident, turn]) => | ||
author !== ctx.currentPlayer || turn > 0 | ||
).length | ||
) { | ||
throw new Error("There are no Pull Requests to Code Review"); | ||
} | ||
if (deckLocation < 0 || deckLocation >= G.deck.length) { | ||
throw new Error("Invalid deck location"); | ||
} | ||
}, | ||
[CardType.BLAME]: (G, ctx, card, target) => { | ||
if (!target) { | ||
throw new Error("You need a player to blame."); | ||
[CardType.BLAME]: (G, ctx, card) => { | ||
if (G.incidents[ctx.currentPlayer].length < 1) { | ||
throw new Error("You don't have any Incidents to blame someone for"); | ||
} | ||
if (G.incidents[target].length > 1) { | ||
throw new Error("You cannot blame a player with more than 1 incident."); | ||
} | ||
}, | ||
@@ -53,0 +39,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
22735
17
530
1