From 171883db98dde502aeb4325bfeb5d2a5000d4050 Mon Sep 17 00:00:00 2001 From: Ben Liu Date: Sun, 30 Jun 2024 01:17:40 +0700 Subject: [PATCH] refacotr(webapp): rearrange game state and moves --- packages/webapp/src/components/BoardGame.tsx | 5 +- .../src/components/Project/ActiveProject.tsx | 2 +- .../webapp/src/components/Table/Table.tsx | 2 +- packages/webapp/src/game/cards/cards.test.ts | 36 --- packages/webapp/src/game/cards/cards.ts | 40 --- packages/webapp/src/game/decks/deck.test.ts | 102 ------ packages/webapp/src/game/decks/deck.ts | 40 --- packages/webapp/src/game/decks/decks.ts | 18 -- packages/webapp/src/game/game.ts | 94 +++--- packages/webapp/src/game/index.ts | 3 +- packages/webapp/src/game/moves/actionMoves.ts | 297 +----------------- .../game/moves/contributeJoinedProjects.ts | 51 +++ .../src/game/moves/contributeOwnedProjects.ts | 52 +++ .../webapp/src/game/moves/createProject.ts | 75 +++++ packages/webapp/src/game/moves/mirror.ts | 45 +++ packages/webapp/src/game/moves/recruit.ts | 69 ++++ .../src/game/moves/removeAndRefillJobs.ts | 30 ++ packages/webapp/src/game/players/players.ts | 34 -- .../activeProject/activeProject.mutators.ts | 22 ++ .../activeProject/activeProject.selectors.ts | 46 +++ .../slice/activeProject/activeProject.ts | 31 ++ .../activeProject/activeProject.utils.ts | 5 + .../src/game/store/slice/activeProjects.ts | 52 +++ .../src/game/{cards => store/slice}/card.d.ts | 0 packages/webapp/src/game/store/slice/cards.ts | 49 +++ packages/webapp/src/game/store/slice/deck.ts | 60 ++++ packages/webapp/src/game/store/slice/decks.ts | 22 ++ .../webapp/src/game/store/slice/players.ts | 46 +++ packages/webapp/src/game/store/slice/table.ts | 33 ++ packages/webapp/src/game/store/store.ts | 26 ++ .../src/game/table/activeProjects.test.ts | 264 ---------------- .../webapp/src/game/table/activeProjects.ts | 94 ------ packages/webapp/src/game/table/table.ts | 38 --- 33 files changed, 774 insertions(+), 1009 deletions(-) delete mode 100644 packages/webapp/src/game/cards/cards.test.ts delete mode 100644 packages/webapp/src/game/cards/cards.ts delete mode 100644 packages/webapp/src/game/decks/deck.test.ts delete mode 100644 packages/webapp/src/game/decks/deck.ts delete mode 100644 packages/webapp/src/game/decks/decks.ts create mode 100644 packages/webapp/src/game/moves/contributeJoinedProjects.ts create mode 100644 packages/webapp/src/game/moves/contributeOwnedProjects.ts create mode 100644 packages/webapp/src/game/moves/createProject.ts create mode 100644 packages/webapp/src/game/moves/mirror.ts create mode 100644 packages/webapp/src/game/moves/recruit.ts create mode 100644 packages/webapp/src/game/moves/removeAndRefillJobs.ts delete mode 100644 packages/webapp/src/game/players/players.ts create mode 100644 packages/webapp/src/game/store/slice/activeProject/activeProject.mutators.ts create mode 100644 packages/webapp/src/game/store/slice/activeProject/activeProject.selectors.ts create mode 100644 packages/webapp/src/game/store/slice/activeProject/activeProject.ts create mode 100644 packages/webapp/src/game/store/slice/activeProject/activeProject.utils.ts create mode 100644 packages/webapp/src/game/store/slice/activeProjects.ts rename packages/webapp/src/game/{cards => store/slice}/card.d.ts (100%) create mode 100644 packages/webapp/src/game/store/slice/cards.ts create mode 100644 packages/webapp/src/game/store/slice/deck.ts create mode 100644 packages/webapp/src/game/store/slice/decks.ts create mode 100644 packages/webapp/src/game/store/slice/players.ts create mode 100644 packages/webapp/src/game/store/slice/table.ts create mode 100644 packages/webapp/src/game/store/store.ts delete mode 100644 packages/webapp/src/game/table/activeProjects.test.ts delete mode 100644 packages/webapp/src/game/table/activeProjects.ts delete mode 100644 packages/webapp/src/game/table/table.ts diff --git a/packages/webapp/src/components/BoardGame.tsx b/packages/webapp/src/components/BoardGame.tsx index 26b84407..2fb57dde 100644 --- a/packages/webapp/src/components/BoardGame.tsx +++ b/packages/webapp/src/components/BoardGame.tsx @@ -1,11 +1,10 @@ import { Client, BoardProps } from 'boardgame.io/react'; import { SocketIO, Local } from 'boardgame.io/multiplayer' -import { OpenStarTerVillage } from '@/game'; +import game, { GameState } from '@/game'; import Table from '@/components/Table/Table'; import Players from '@/components/Players/Players'; import DevActions from '@/components/DevActions/DevActions'; import ActionBoard from './ActionBoard/ActionBoard'; -import { GameState } from '@/game/game'; const Board: React.FC> = (props) => { const { G, ctx, debug } = props; @@ -23,7 +22,7 @@ const Boardgame: React.FC<{ isLocal: boolean} & React.ComponentProps { - describe('GetById', () => { - it('should return card', () => { - const cards = ['K', 'Q', 'J', 'T']; - const card = Cards.GetById(cards, 2); - expect(card).toEqual('J'); - }); - }); - - describe('Add', () => { - it('should merge new card into cards', () => { - const cards = ['knight', 'king', 'bishop']; - const newCards = ['queen', 'pawn', 'bishop']; - Cards.Add(cards, newCards); - expect(cards).toEqual(['knight', 'king', 'bishop', 'queen', 'pawn', 'bishop']); - }); - }); - - describe('Remove', () => { - it('should remove discard card from cards', () => { - const cards = ['knight', 'pawn', 'king', 'bishop', 'queen', 'pawn', 'bishop', 'pawn']; - const discardCards = ['pawn', 'pawn', 'bishop']; - Cards.Remove(cards, discardCards); - expect(cards).toEqual(['knight', 'king', 'queen', 'bishop', 'pawn']); - }); - - it('should not remove invalid discard from cards', () => { - const cards = ['knight', 'pawn', 'king', 'bishop', 'queen', 'pawn', 'bishop', 'pawn']; - const discardCards = ['invalid_item']; - Cards.Remove(cards, discardCards); - expect(cards).toEqual(['knight', 'pawn', 'king', 'bishop', 'queen', 'pawn', 'bishop', 'pawn']); - }); - }); -}); diff --git a/packages/webapp/src/game/cards/cards.ts b/packages/webapp/src/game/cards/cards.ts deleted file mode 100644 index e26e5327..00000000 --- a/packages/webapp/src/game/cards/cards.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { filterInplace } from '../utils'; - -export interface ICards { - GetById(cards: T[], index: number): T; - Add(cards: T[], newCards: T[]): void; - AddOne(cards: T[], newCard: T): void; - Remove(cards: T[], discardCards: T[]): void; - RemoveOne(cards: T[], discardCard: T): void; -} - -export const Cards: ICards = { - GetById(cards, index) { - return cards[index]; - }, - Add(cards: T[], newCards: T[]): void { - cards.push(...newCards); - }, - AddOne(cards, newCard) { - return Cards.Add(cards, [newCard]); - }, - Remove(cards: T[], discardCards: T[]): void { - // O(M+N) - // convert discard cards to card => number map - const discardCardsMap = new Map(); - for (let discardCard of discardCards) { - discardCardsMap.set(discardCard, (discardCardsMap.get(discardCard) ?? 0) + 1); - } - // remove discard card as empty in cards - filterInplace(cards, (card) => { - if ((discardCardsMap.get(card) ?? 0) > 0) { - discardCardsMap.set(card, discardCardsMap.get(card)! - 1); - return false; - } - return true; - }); - }, - RemoveOne(cards, discardCard) { - return Cards.Remove(cards, [discardCard]); - }, -} diff --git a/packages/webapp/src/game/decks/deck.test.ts b/packages/webapp/src/game/decks/deck.test.ts deleted file mode 100644 index a267e9f8..00000000 --- a/packages/webapp/src/game/decks/deck.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Deck, newCardDeck } from './deck'; - -describe('CardDeck', () => { - it('should return empty draw pile and discard pile as default', () => { - const cardDeck = newCardDeck(); - expect(cardDeck.drawPile).toEqual([]); - expect(cardDeck.discardPile).toEqual([]); - }); - - it('should return given draw pile and empty discard pile', () => { - const cardDeck = newCardDeck(['abc', 'xyz', 'ijk']); - expect(cardDeck.drawPile).toEqual(['abc', 'xyz', 'ijk']); - expect(cardDeck.discardPile).toEqual([]); - }); - - it('should return empty draw pile and given discard pile', () => { - const cardDeck = newCardDeck(undefined, ['abc', 'xyz', 'ijk']); - expect(cardDeck.drawPile).toEqual([]); - expect(cardDeck.discardPile).toEqual(['abc', 'xyz', 'ijk']); - }); -}); - -describe('Deck', () => { - const reverse = (array: T[]) => array.slice().reverse(); - const sort = (array: T[]) => array.slice().sort(); - - describe('ShuffleDrawPile', () => { - const drawPile = [1, 3, 2, 6, 4, 5]; - describe(`given draw pile ${drawPile}`, () => { - it.each([ - [reverse, [5, 4, 6, 2, 3, 1]], - [sort, [1, 2, 3, 4, 5, 6]], - ])('should shuffle by %s to be %j', (shuffler, expected) => { - const cardDeck = newCardDeck(drawPile); - Deck.ShuffleDrawPile(cardDeck, shuffler); - expect(cardDeck.drawPile).toEqual(expected); - }); - }); - }); - - describe('ShuffleDiscardPile', () => { - const discardPile = [1, 3, 2, 6, 4, 5]; - describe(`given dicard pile ${discardPile}`, () => { - it.each([ - [reverse, [5, 4, 6, 2, 3, 1]], - [sort, [1, 2, 3, 4, 5, 6]], - ])('should shuffle by %s to be %j', (shuffler, expected) => { - const cardDeck = newCardDeck([], discardPile); - Deck.ShuffleDiscardPile(cardDeck, shuffler); - expect(cardDeck.discardPile).toEqual(expected); - }); - }); - }); - - describe('PutDiscardPileToDrawPile', () => { - describe.each([ - [[], [4, 5, 6], [4, 5, 6]], - [[3, 2, 1], [], [3, 2, 1]], - [[3, 2, 1], [4, 5, 6], [3, 2, 1, 4, 5, 6]], - ])('given draw pile %j and discard pile %j', (drawPile, discardPile, expected) => { - const cardDeck = newCardDeck(drawPile, discardPile); - Deck.PutDiscardPileToDrawPile(cardDeck); - test(`draw pile should be ${expected}`, () => { - expect(cardDeck.drawPile).toEqual(expected); - }); - test('discard pile should be empty', () => { - expect(cardDeck.discardPile).toEqual([]); - }); - }); - }); - - describe('Draw', () => { - const drawPile = ['card 1', 'card 3', 'card 2']; - describe(`given draw pile ${drawPile}`, () => { - test.each([ - [2, ['card 1', 'card 3']], - [0, []], - [1, ['card 1']], - [5, ['card 1', 'card 3', 'card 2']], - ])('draw %i cards from deck should return %j', (n, expected) => { - const cardDeck = newCardDeck(drawPile); - const actual = Deck.Draw(cardDeck, n); - expect(actual).toEqual(expected); - }); - }); - }); - - describe('Discard', () => { - const discardPile = ['card 3', 'card 2']; - describe(`given discard pile ${discardPile}`, () => { - test.each([ - [['card 1'], ['card 3', 'card 2', 'card 1']], - [[], ['card 3', 'card 2']], - [['card 1', 'card 3', 'card 1'], ['card 3', 'card 2', 'card 1', 'card 3', 'card 1']], - ])('discard %j cards to deck discard pile should be %j', (discards, expected) => { - const cardDeck = newCardDeck([], discardPile); - Deck.Discard(cardDeck, discards); - expect(cardDeck.discardPile).toEqual(expected); - }); - }); - }); -}); diff --git a/packages/webapp/src/game/decks/deck.ts b/packages/webapp/src/game/decks/deck.ts deleted file mode 100644 index cc7bea12..00000000 --- a/packages/webapp/src/game/decks/deck.ts +++ /dev/null @@ -1,40 +0,0 @@ -export interface Deck { - drawPile: T[]; - discardPile: T[]; -} - -export function newCardDeck(drawPile: T[] = [], discardPile: T[] = []): Deck { - return { - drawPile, - discardPile, - }; -} - -export interface IDeck { - ShuffleDrawPile(deck: Deck, shuffler: (pile: T[]) => T[]): void; - ShuffleDiscardPile(deck: Deck, shuffler: (pile: T[]) => T[]): void; - PutDiscardPileToDrawPile(deck: Deck): void; - Draw(deck: Deck, n: number): T[]; - Discard(deck: Deck, ts: T[]): void; -} - -export const Deck: IDeck = { - ShuffleDrawPile(deck: Deck, shuffler: (pile: T[]) => T[]): void { - deck.drawPile = shuffler(deck.drawPile); - }, - ShuffleDiscardPile(deck: Deck, shuffler: (pile: T[]) => T[]): void { - deck.discardPile = shuffler(deck.discardPile); - }, - PutDiscardPileToDrawPile(deck: Deck): void { - deck.drawPile = [...deck.drawPile, ...deck.discardPile]; - deck.discardPile = []; - }, - Draw(deck: Deck, n: number): T[] { - const result = deck.drawPile.slice(0, n); - deck.drawPile = deck.drawPile.slice(n); - return result; - }, - Discard(deck: Deck, ts: T[]) { - deck.discardPile = [...deck.discardPile, ...ts]; - } -} diff --git a/packages/webapp/src/game/decks/decks.ts b/packages/webapp/src/game/decks/decks.ts deleted file mode 100644 index 74b150f9..00000000 --- a/packages/webapp/src/game/decks/decks.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EventCard, ForceCard, JobCard, ProjectCard } from "../cards/card"; -import { Deck, newCardDeck } from "./deck"; - -export type Decks = { - projects: Deck; - jobs: Deck; - forces: Deck; - events: Deck; -}; - -export const setupDecks = (projectCards: ProjectCard[], jobCards: JobCard[], forceCards: ForceCard[], eventCards: EventCard[]): Decks => { - return { - projects: newCardDeck(projectCards), - jobs: newCardDeck(jobCards), - forces: newCardDeck(forceCards), - events: newCardDeck(eventCards), - }; -} diff --git a/packages/webapp/src/game/game.ts b/packages/webapp/src/game/game.ts index 8df336e7..5f1dba79 100644 --- a/packages/webapp/src/game/game.ts +++ b/packages/webapp/src/game/game.ts @@ -1,46 +1,38 @@ import { Game, MoveFn, PlayerID } from 'boardgame.io'; import { INVALID_MOVE } from 'boardgame.io/core'; -import { Deck } from './decks/deck'; -import { Cards } from './cards/cards'; import projectCards from './data/card/projects.json'; import jobCards from './data/card/jobs.json'; import forceCards from './data/card/forces.json'; import eventCards from './data/card/events.json'; -import { ActiveProject, ActiveProjects } from './table/activeProjects'; -import { ActionMoves, Contribution, contributeJoinedProjects, contributeOwnedProjects, createProject, mirror, recruit, removeAndRefillJobs } from './moves/actionMoves'; -import { ProjectCard } from './cards/card'; -import { Decks, setupDecks } from './decks/decks'; -import { Table, setupTable } from './table/table'; -import { Player, Players, setupPlayers } from './players/players'; +import { ActionMoves } from './moves/actionMoves'; +import { recruit } from './moves/recruit'; +import { contributeOwnedProjects } from './moves/contributeOwnedProjects'; +import { removeAndRefillJobs } from './moves/removeAndRefillJobs'; +import { contributeJoinedProjects } from './moves/contributeJoinedProjects'; +import { mirror } from './moves/mirror'; +import { createProject } from './moves/createProject'; +import { ProjectCard } from './store/slice/card'; +import { Player, PlayersMutator } from './store/slice/players'; +import GameStore, { GameState } from './store/store'; +import { DeckMutator, DeckSelector } from './store/slice/deck'; +import { ActiveProjectsMutator, ActiveProjectsSelector } from './store/slice/activeProjects'; +import { selectors } from './store/slice/activeProject/activeProject.selectors'; +import { CardsMutator } from './store/slice/cards'; type PartialBy = Omit & Partial>; -export interface GameState { - rules: Rule; - decks: Decks; - table: Table; - players: Players; -} - -export interface Rule { -} - export const OpenStarTerVillage: Game = { setup: ({ ctx }) => { - const rules: Rule = {}; - - const players = setupPlayers(ctx.playOrder); + const G = GameStore.initialState(); - const decks = setupDecks(projectCards as unknown as ProjectCard[], jobCards, forceCards, eventCards); + DeckMutator.initialize(G.decks.projects, projectCards as unknown as ProjectCard[]); + DeckMutator.initialize(G.decks.jobs, jobCards); + DeckMutator.initialize(G.decks.forces, forceCards); + DeckMutator.initialize(G.decks.events, eventCards); - const table = setupTable(); + PlayersMutator.initialize(G.players, ctx.playOrder); - return { - rules, - decks, - table, - players, - }; + return G; }, moves: { @@ -51,29 +43,32 @@ export const OpenStarTerVillage: Game = { onBegin: ({ random, G }) => { // shuffle cards const shuffler = random.Shuffle; - Deck.ShuffleDrawPile(G.decks.events, shuffler); + DeckMutator.shuffleDrawPile(G.decks.events, shuffler); - Deck.ShuffleDrawPile(G.decks.projects, shuffler); + DeckMutator.shuffleDrawPile(G.decks.projects, shuffler); const maxProjectCards = 2; for (let playerId in G.players) { - const projectCards = Deck.Draw(G.decks.projects, maxProjectCards); - Cards.Add(G.players[playerId].hand.projects, projectCards); + const projectCards = DeckSelector.peek(G.decks.projects, maxProjectCards); + DeckMutator.draw(G.decks.projects, maxProjectCards); + CardsMutator.add(G.players[playerId].hand.projects, projectCards); } const isForceCardsEnabled = false; if (isForceCardsEnabled) { - Deck.ShuffleDrawPile(G.decks.forces, shuffler); + DeckMutator.shuffleDrawPile(G.decks.forces, shuffler); const maxForceCards = 2; for (let playerId in G.players) { - const forceCards = Deck.Draw(G.decks.forces, maxForceCards); - Cards.Add(G.players[playerId].hand.forces, forceCards); + const forceCards = DeckSelector.peek(G.decks.forces, maxForceCards); + DeckMutator.draw(G.decks.forces, maxForceCards); + CardsMutator.add(G.players[playerId].hand.forces, forceCards); } } - Deck.ShuffleDrawPile(G.decks.jobs, shuffler); + DeckMutator.shuffleDrawPile(G.decks.jobs, shuffler); const maxJobCards = 5; - const jobCards = Deck.Draw(G.decks.jobs, maxJobCards); - Cards.Add(G.table.activeJobs, jobCards); + const jobCards = DeckSelector.peek(G.decks.jobs, maxJobCards); + DeckMutator.draw(G.decks.jobs, maxJobCards); + CardsMutator.add(G.table.activeJobs, jobCards); for (let playerId in G.players) { G.players[playerId].token.workers = 10; @@ -137,28 +132,29 @@ export const OpenStarTerVillage: Game = { // client trigger settle project and move on to next stage move: (({ G }) => { const activeProjects = G.table.activeProjects; - const fulfilledProjects = ActiveProjects.FilterFulfilled(activeProjects); + const fulfilledProjects = ActiveProjectsSelector.filterFulfilled(activeProjects); if (fulfilledProjects.length === 0) { return; } fulfilledProjects.forEach(project => { // Update Score Object.keys(G.players).forEach(playerId => { - const victoryPoints = ActiveProject.GetPlayerContribution(project, playerId); + const victoryPoints = selectors.getPlayerContribution(project, playerId); G.players[playerId].victoryPoints += victoryPoints; }); // Return Tokens Object.keys(G.players).forEach(playerId => { - const workerTokens = ActiveProject.GetPlayerWorkerTokens(project, playerId); + const workerTokens = selectors.getPlayerWorkerTokens(project, playerId); G.players[playerId].token.workers += workerTokens; }); }); // Update OpenSourceTree // Remove from table - ActiveProjects.Remove(activeProjects, fulfilledProjects); + ActiveProjectsMutator.remove(activeProjects, fulfilledProjects); // Discard Project Card const projectCards = fulfilledProjects.map(project => project.card); - Deck.Discard(G.decks.projects, projectCards); + // Deck.Discard(G.decks.projects, projectCards); + DeckMutator.discard(G.decks.projects, projectCards); }), }, }, @@ -187,15 +183,17 @@ export const OpenStarTerVillage: Game = { const refillProject: MoveFn = (({G, ctx}) => { const maxProjectCards = 2; const refillCardNumber = maxProjectCards - G.players[ctx.currentPlayer].hand.projects.length; - const projectCards = Deck.Draw(G.decks.projects, refillCardNumber); - Cards.Add(G.players[ctx.currentPlayer].hand.projects, projectCards); + const projectCards = DeckSelector.peek(G.decks.projects, refillCardNumber); + DeckMutator.draw(G.decks.projects, refillCardNumber); + CardsMutator.add(G.players[ctx.currentPlayer].hand.projects, projectCards); }); const refillForce: MoveFn = (({G, ctx}) => { const maxForceCards = 2; const refillCardNumber = maxForceCards - G.players[ctx.currentPlayer].hand.forces.length; - const forceCards = Deck.Draw(G.decks.forces, refillCardNumber); - Cards.Add(G.players[ctx.currentPlayer].hand.forces, forceCards); + const forceCards = DeckSelector.peek(G.decks.forces, refillCardNumber); + DeckMutator.draw(G.decks.forces, refillCardNumber); + CardsMutator.add(G.players[ctx.currentPlayer].hand.forces, forceCards); }); // refill cards diff --git a/packages/webapp/src/game/index.ts b/packages/webapp/src/game/index.ts index 330bc296..fe6682b0 100644 --- a/packages/webapp/src/game/index.ts +++ b/packages/webapp/src/game/index.ts @@ -1 +1,2 @@ -export { OpenStarTerVillage } from './game'; +export type { GameState } from "./store/store"; +export { OpenStarTerVillage as default } from "./game"; diff --git a/packages/webapp/src/game/moves/actionMoves.ts b/packages/webapp/src/game/moves/actionMoves.ts index 245263c2..9344b63c 100644 --- a/packages/webapp/src/game/moves/actionMoves.ts +++ b/packages/webapp/src/game/moves/actionMoves.ts @@ -1,11 +1,13 @@ import { FnContext, PlayerID } from 'boardgame.io'; import { INVALID_MOVE } from 'boardgame.io/core'; -import { Deck } from '../decks/deck'; -import { Cards } from '../cards/cards'; -import { isInRange } from '../utils'; -import { ActiveProject, ActiveProjects } from '../table/activeProjects'; -import { JobName } from '../cards/card'; -import { GameState } from '../game'; +import { JobName } from '../store/slice/card'; +import { GameState } from '../store/store'; +import { CreateProject } from './createProject'; +import { Mirror } from './mirror'; +import { RemoveAndRefillJobs } from './removeAndRefillJobs'; +import { ContributeJoinedProjects } from './contributeJoinedProjects'; +import { ContributeOwnedProjects } from './contributeOwnedProjects'; +import { Recruit } from './recruit'; export type AllMoves = ActionMoves & StageMoves; @@ -28,12 +30,6 @@ export interface ContributionAction extends Contribution { activeProjectIndex: number; } -export type CreateProject = (projectCardIndex: number, jobCardIndex: number) => void; -export type Recruit = (resourceCardIndex: number, activeProjectIndex: number) => void; -export type ContributeOwnedProjects = (contributions: ContributionAction[]) => void; -export type ContributeJoinedProjects = (contributions: ContributionAction[]) => void; -export type RemoveAndRefillJobs = (jobCardIndices: number[]) => void; -export type Mirror = (actionName: keyof ActionMoves, ...params: any[]) => void; export type Settle = () => void; export type RefillAndEnd = () => void; export type RefillProject = () => void; @@ -41,280 +37,3 @@ export type RefillForce = () => void; // Define the type of a move to support type checking export type GameMove void> = (context: FnContext & { playerID: PlayerID }, ...args: Parameters) => void | GameState | typeof INVALID_MOVE; - -export const createProject: GameMove = ({ G, playerID }, projectCardIndex: number, jobCardIndex: number) => { - if (!G.table.activeActionMoves.createProject) { - return INVALID_MOVE; - } - - const currentPlayer = playerID!; - const currentPlayerToken = G.players[currentPlayer].token; - // TODO: replace hardcoded number with dynamic rules - const createProjectActionCosts = 2; - if (currentPlayerToken.actions < createProjectActionCosts) { - return INVALID_MOVE; - } - const createProjectWorkerCosts = 1; - if (currentPlayerToken.workers < createProjectWorkerCosts) { - return INVALID_MOVE; - } - - // check project card in in hand - const currentHandProjects = G.players[currentPlayer].hand.projects; - if (!isInRange(projectCardIndex, currentHandProjects.length)) { - return INVALID_MOVE; - } - - // check job card is on the table - const currentJobs = G.table.activeJobs; - if (!isInRange(jobCardIndex, currentJobs.length)) { - return INVALID_MOVE; - } - - // check job card is required in project - const projectCard = Cards.GetById(currentHandProjects, projectCardIndex); - const jobCard = Cards.GetById(currentJobs, jobCardIndex); - if (!Object.keys(projectCard.requirements).includes(jobCard.name)) { - return INVALID_MOVE; - } - - // reduce action tokens - currentPlayerToken.actions -= createProjectActionCosts; - Cards.RemoveOne(currentHandProjects, projectCard); - Cards.RemoveOne(currentJobs, jobCard); - - // initial active project - const activeProjectIndex = ActiveProjects.Add(G.table.activeProjects, projectCard, currentPlayer); - const activeProject = ActiveProjects.GetById(G.table.activeProjects, activeProjectIndex); - - // reduce worker token - currentPlayerToken.workers -= createProjectWorkerCosts; - // assign worker token - const jobInitPoints = 1; - ActiveProject.AssignWorker(activeProject, jobCard.name, currentPlayer, jobInitPoints); - // score victory points - const createProjectVictoryPoints = 1; - G.players[currentPlayer].victoryPoints += createProjectVictoryPoints; - - // discard job card - Deck.Discard(G.decks.jobs, [jobCard]); - - // Refill job card - const maxJobCards = 5; - const refillCardNumber = maxJobCards - currentJobs.length; - const jobCards = Deck.Draw(G.decks.jobs, refillCardNumber); - Cards.Add(currentJobs, jobCards); - - G.table.activeActionMoves.createProject = false; -} - -export const recruit: GameMove = ({ G, playerID }, jobCardIndex: number, activeProjectIndex: number) => { - if (!G.table.activeActionMoves.recruit) { - return INVALID_MOVE; - } - - const currentPlayer = playerID!; - const currentPlayerToken = G.players[currentPlayer].token; - const recruitActionCosts = 1; - if (currentPlayerToken.actions < recruitActionCosts) { - return INVALID_MOVE; - } - const recruitWorkerCosts = 1; - if (currentPlayerToken.workers < recruitWorkerCosts) { - return INVALID_MOVE; - } - - const currentJobs = G.table.activeJobs; - if (!isInRange(jobCardIndex, currentJobs.length)) { - return INVALID_MOVE; - } - - const activeProjects = G.table.activeProjects - if (!isInRange(activeProjectIndex, activeProjects.length)) { - return INVALID_MOVE; - } - const jobCard = Cards.GetById(currentJobs, jobCardIndex); - const activeProject = ActiveProjects.GetById(G.table.activeProjects, activeProjectIndex); - const jobContribution = ActiveProject.GetJobContribution(activeProject, jobCard.name); - // Check job requirment is not fulfilled yet - if (!(jobContribution < activeProject.card.requirements[jobCard.name])) { - return INVALID_MOVE; - } - // User cannot place more than one worker in same job - if (ActiveProject.HasWorker(activeProject, jobCard.name, currentPlayer)) { - return INVALID_MOVE; - } - - // reduce action - currentPlayerToken.actions -= recruitActionCosts; - Cards.RemoveOne(currentJobs, jobCard); - - // reduce worker tokens - currentPlayerToken.workers -= recruitWorkerCosts; - // assign worker token - const jobInitPoints = 1; - ActiveProject.AssignWorker(activeProject, jobCard.name, currentPlayer, jobInitPoints); - - // discard job card - Deck.Discard(G.decks.jobs, [jobCard]); - - // Refill job card - const maxJobCards = 5; - const refillCardNumber = maxJobCards - currentJobs.length; - const jobCards = Deck.Draw(G.decks.jobs, refillCardNumber); - Cards.Add(currentJobs, jobCards); - - G.table.activeActionMoves.recruit = false; -}; - -export const contributeOwnedProjects: GameMove = ({ G, playerID }, contributions: ContributionAction[]) => { - if (!G.table.activeActionMoves.contributeOwnedProjects) { - return INVALID_MOVE; - } - - const currentPlayer = playerID!; - const currentPlayerToken = G.players[currentPlayer].token; - const contributeActionCosts = 1; - if (currentPlayerToken.actions < contributeActionCosts) { - return INVALID_MOVE; - } - const activeProjects = G.table.activeProjects - const isInvalid = contributions.map(({ activeProjectIndex, jobName }) => { - if (!isInRange(activeProjectIndex, activeProjects.length)) { - return true; - } - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectIndex); - if (activeProject.owner !== currentPlayer) { - return true; - } - - if (!ActiveProject.HasWorker(activeProject, jobName, currentPlayer)) { - return true; - } - }).some(x => x); - if (isInvalid) { - return INVALID_MOVE; - } - const totalContributions = contributions.map(({ value }) => value).reduce((a, b) => a + b, 0); - const maxOwnedContributions = 4; - if (!(totalContributions <= maxOwnedContributions)) { - return INVALID_MOVE; - } - - // deduct action tokens - currentPlayerToken.actions -= contributeActionCosts; - contributions.forEach(({ activeProjectIndex, jobName, value }) => { - // update contributions to given contribution points - const activeProject = ActiveProjects.GetById(G.table.activeProjects, activeProjectIndex); - ActiveProject.PushWorker(activeProject, jobName, currentPlayer, value); - }); - - G.table.activeActionMoves.contributeOwnedProjects = false; -}; - -export const contributeJoinedProjects: GameMove = ({G, ctx, playerID}, contributions: ContributionAction[]) => { - if (!G.table.activeActionMoves.contributeJoinedProjects) { - return INVALID_MOVE; - } - - const currentPlayer = playerID!; - const currentPlayerToken = G.players[currentPlayer].token; - const contributeActionCosts = 1; - if (currentPlayerToken.actions < contributeActionCosts) { - return INVALID_MOVE; - } - const activeProjects = G.table.activeProjects - const isInvalid = contributions.map(({ activeProjectIndex, jobName }) => { - if (!isInRange(activeProjectIndex, activeProjects.length)) { - return true; - } - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectIndex); - if (activeProject.owner === currentPlayer) { - return true; - } - - if (!ActiveProject.HasWorker(activeProject, jobName, currentPlayer)) { - return true; - } - }).some(x => x); - if (isInvalid) { - return INVALID_MOVE; - } - const totalContributions = contributions.map(({ value }) => value).reduce((a, b) => a + b, 0); - const maxJoinedContributions = 3; - if (!(totalContributions <= maxJoinedContributions)) { - return INVALID_MOVE; - } - - // deduct action tokens - currentPlayerToken.actions -= contributeActionCosts; - contributions.forEach(({ activeProjectIndex, jobName, value }) => { - // update contributions to given contribution points - const activeProject = ActiveProjects.GetById(G.table.activeProjects, activeProjectIndex); - ActiveProject.PushWorker(activeProject, jobName, currentPlayer, value); - }); - - G.table.activeActionMoves.contributeJoinedProjects = false; -}; - -export const removeAndRefillJobs: GameMove = ({ G }, jobCardIndices: number[]) => { - if (!G.table.activeActionMoves.removeAndRefillJobs) { - return INVALID_MOVE; - } - - const currentJob = G.table.activeJobs; - const jobDeck = G.decks.jobs; - const isInvalid = jobCardIndices.map(index => !isInRange(index, currentJob.length)).some(x => x); - if (isInvalid) { - return INVALID_MOVE; - } - const removedJobCards = jobCardIndices.map(index => currentJob[index]); - Cards.Remove(currentJob, removedJobCards); - Deck.Discard(jobDeck, removedJobCards); - - const maxJobCards = 5; - const refillCardNumber = maxJobCards - currentJob.length; - const jobCards = Deck.Draw(jobDeck, refillCardNumber); - Cards.Add(currentJob, jobCards); - - G.table.activeActionMoves.removeAndRefillJobs = false; -}; - -export const mirror: GameMove = (context, actionName, ...params) => { - const { G } = context; - if (!G.table.activeActionMoves.mirror) { - return INVALID_MOVE; - } - - // TODO: add token to bypass the active moves check when its inactive - - let result = null; - switch (actionName) { - case 'createProject': - result = createProject(context, ...(params as Parameters)); - break; - case 'recruit': - result = recruit(context, ...(params as Parameters)); - break; - case 'contributeOwnedProjects': - result = contributeOwnedProjects(context, ...(params as Parameters)); - break; - case 'contributeJoinedProjects': - result = contributeJoinedProjects(context, ...(params as Parameters)); - break; - case 'removeAndRefillJobs': - result = removeAndRefillJobs(context, ...(params as Parameters)); - break; - default: - result = INVALID_MOVE; - break; - } - - // TODO: remove the token - - if (result === INVALID_MOVE) { - return INVALID_MOVE; - } - - G.table.activeActionMoves.mirror = false; -}; diff --git a/packages/webapp/src/game/moves/contributeJoinedProjects.ts b/packages/webapp/src/game/moves/contributeJoinedProjects.ts new file mode 100644 index 00000000..080ff659 --- /dev/null +++ b/packages/webapp/src/game/moves/contributeJoinedProjects.ts @@ -0,0 +1,51 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { isInRange } from '../utils'; +import { ActiveProjectsSelector } from '../store/slice/activeProjects'; +import { ActiveProjectMutator, ActiveProjectSelector } from '../store/slice/activeProject/activeProject'; +import { GameMove, ContributionAction } from './actionMoves'; + +export type ContributeJoinedProjects = (contributions: ContributionAction[]) => void; +export const contributeJoinedProjects: GameMove = ({ G, ctx, playerID }, contributions) => { + if (!G.table.activeActionMoves.contributeJoinedProjects) { + return INVALID_MOVE; + } + + const currentPlayer = playerID; + const currentPlayerToken = G.players[currentPlayer].token; + const contributeActionCosts = 1; + if (currentPlayerToken.actions < contributeActionCosts) { + return INVALID_MOVE; + } + const activeProjects = G.table.activeProjects; + const isInvalid = contributions.map(({ activeProjectIndex, jobName }) => { + if (!isInRange(activeProjectIndex, activeProjects.length)) { + return true; + } + const activeProject = ActiveProjectsSelector.getById(activeProjects, activeProjectIndex); + if (activeProject.owner === currentPlayer) { + return true; + } + + if (!ActiveProjectSelector.hasWorker(activeProject, jobName, currentPlayer)) { + return true; + } + }).some(x => x); + if (isInvalid) { + return INVALID_MOVE; + } + const totalContributions = contributions.map(({ value }) => value).reduce((a, b) => a + b, 0); + const maxJoinedContributions = 3; + if (totalContributions > maxJoinedContributions) { + return INVALID_MOVE; + } + + // deduct action tokens + currentPlayerToken.actions -= contributeActionCosts; + contributions.forEach(({ activeProjectIndex, jobName, value }) => { + // update contributions to given contribution points + const activeProject = ActiveProjectsSelector.getById(G.table.activeProjects, activeProjectIndex); + ActiveProjectMutator.pushWorker(activeProject, jobName, currentPlayer, value); + }); + + G.table.activeActionMoves.contributeJoinedProjects = false; +}; diff --git a/packages/webapp/src/game/moves/contributeOwnedProjects.ts b/packages/webapp/src/game/moves/contributeOwnedProjects.ts new file mode 100644 index 00000000..be16145d --- /dev/null +++ b/packages/webapp/src/game/moves/contributeOwnedProjects.ts @@ -0,0 +1,52 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { isInRange } from '../utils'; +import { ActiveProjectsSelector } from '../store/slice/activeProjects'; +import { ActiveProjectMutator, ActiveProjectSelector } from '../store/slice/activeProject/activeProject'; +import { GameMove, ContributionAction } from './actionMoves'; + +export type ContributeOwnedProjects = (contributions: ContributionAction[]) => void; + +export const contributeOwnedProjects: GameMove = ({ G, playerID }, contributions) => { + if (!G.table.activeActionMoves.contributeOwnedProjects) { + return INVALID_MOVE; + } + + const currentPlayer = playerID; + const currentPlayerToken = G.players[currentPlayer].token; + const contributeActionCosts = 1; + if (currentPlayerToken.actions < contributeActionCosts) { + return INVALID_MOVE; + } + const activeProjects = G.table.activeProjects; + const isInvalid = contributions.map(({ activeProjectIndex, jobName }) => { + if (!isInRange(activeProjectIndex, activeProjects.length)) { + return true; + } + const activeProject = ActiveProjectsSelector.getById(activeProjects, activeProjectIndex); + if (activeProject.owner !== currentPlayer) { + return true; + } + + if (!ActiveProjectSelector.hasWorker(activeProject, jobName, currentPlayer)) { + return true; + } + }).some(x => x); + if (isInvalid) { + return INVALID_MOVE; + } + const totalContributions = contributions.map(({ value }) => value).reduce((a, b) => a + b, 0); + const maxOwnedContributions = 4; + if (totalContributions > maxOwnedContributions) { + return INVALID_MOVE; + } + + // deduct action tokens + currentPlayerToken.actions -= contributeActionCosts; + contributions.forEach(({ activeProjectIndex, jobName, value }) => { + // update contributions to given contribution points + const activeProject = ActiveProjectsSelector.getById(G.table.activeProjects, activeProjectIndex); + ActiveProjectMutator.pushWorker(activeProject, jobName, currentPlayer, value); + }); + + G.table.activeActionMoves.contributeOwnedProjects = false; +}; diff --git a/packages/webapp/src/game/moves/createProject.ts b/packages/webapp/src/game/moves/createProject.ts new file mode 100644 index 00000000..18b2c7d7 --- /dev/null +++ b/packages/webapp/src/game/moves/createProject.ts @@ -0,0 +1,75 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { isInRange } from '../utils'; +import { ActiveProjectsMutator, ActiveProjectsSelector } from '../store/slice/activeProjects'; +import { DeckMutator, DeckSelector } from '../store/slice/deck'; +import { ActiveProjectMutator } from '../store/slice/activeProject/activeProject'; +import { GameMove } from './actionMoves'; +import { CardsMutator, CardsSelector } from '../store/slice/cards'; + +export type CreateProject = (projectCardIndex: number, jobCardIndex: number) => void; +export const createProject: GameMove = ({ G, playerID }, projectCardIndex, jobCardIndex) => { + if (!G.table.activeActionMoves.createProject) { + return INVALID_MOVE; + } + + const currentPlayer = playerID; + const currentPlayerToken = G.players[currentPlayer].token; + // TODO: replace hardcoded number with dynamic rules + const createProjectActionCosts = 2; + if (currentPlayerToken.actions < createProjectActionCosts) { + return INVALID_MOVE; + } + const createProjectWorkerCosts = 1; + if (currentPlayerToken.workers < createProjectWorkerCosts) { + return INVALID_MOVE; + } + + // check project card in in hand + const currentHandProjects = G.players[currentPlayer].hand.projects; + if (!isInRange(projectCardIndex, currentHandProjects.length)) { + return INVALID_MOVE; + } + + // check job card is on the table + const currentJobs = G.table.activeJobs; + if (!isInRange(jobCardIndex, currentJobs.length)) { + return INVALID_MOVE; + } + + // check job card is required in project + const projectCard = CardsSelector.getById(currentHandProjects, projectCardIndex); + const jobCard = CardsSelector.getById(currentJobs, jobCardIndex); + if (!Object.keys(projectCard.requirements).includes(jobCard.name)) { + return INVALID_MOVE; + } + + // reduce action tokens + currentPlayerToken.actions -= createProjectActionCosts; + CardsMutator.removeOne(currentHandProjects, projectCard); + CardsMutator.removeOne(currentJobs, jobCard); + + // initial active project + ActiveProjectsMutator.add(G.table.activeProjects, projectCard, currentPlayer); + const activeProject = ActiveProjectsSelector.getLast(G.table.activeProjects); + + // reduce worker token + currentPlayerToken.workers -= createProjectWorkerCosts; + // assign worker token + const jobInitPoints = 1; + ActiveProjectMutator.assignWorker(activeProject, jobCard.name, currentPlayer, jobInitPoints); + // score victory points + const createProjectVictoryPoints = 1; + G.players[currentPlayer].victoryPoints += createProjectVictoryPoints; + + // discard job card + DeckMutator.discard(G.decks.jobs, [jobCard]); + + // Refill job card + const maxJobCards = 5; + const refillCardNumber = maxJobCards - currentJobs.length; + const jobCards = DeckSelector.peek(G.decks.jobs, refillCardNumber); + DeckMutator.draw(G.decks.jobs, refillCardNumber); + CardsMutator.add(currentJobs, jobCards); + + G.table.activeActionMoves.createProject = false; +}; diff --git a/packages/webapp/src/game/moves/mirror.ts b/packages/webapp/src/game/moves/mirror.ts new file mode 100644 index 00000000..12fa3fee --- /dev/null +++ b/packages/webapp/src/game/moves/mirror.ts @@ -0,0 +1,45 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { CreateProject, createProject } from './createProject'; +import { GameMove, ActionMoves } from './actionMoves'; +import { Recruit, recruit } from './recruit'; +import { ContributeOwnedProjects, contributeOwnedProjects } from './contributeOwnedProjects'; +import { RemoveAndRefillJobs, removeAndRefillJobs } from './removeAndRefillJobs'; +import { ContributeJoinedProjects, contributeJoinedProjects } from './contributeJoinedProjects'; + +export type Mirror = (actionName: keyof ActionMoves, ...params: any[]) => void; +export const mirror: GameMove = (context, actionName, ...params) => { + const { G } = context; + if (!G.table.activeActionMoves.mirror) { + return INVALID_MOVE; + } + + // TODO: add token to bypass the active moves check when its inactive + let result = null; + switch (actionName) { + case 'createProject': + result = createProject(context, ...(params as Parameters)); + break; + case 'recruit': + result = recruit(context, ...(params as Parameters)); + break; + case 'contributeOwnedProjects': + result = contributeOwnedProjects(context, ...(params as Parameters)); + break; + case 'contributeJoinedProjects': + result = contributeJoinedProjects(context, ...(params as Parameters)); + break; + case 'removeAndRefillJobs': + result = removeAndRefillJobs(context, ...(params as Parameters)); + break; + default: + result = INVALID_MOVE; + break; + } + + // TODO: remove the token + if (result === INVALID_MOVE) { + return INVALID_MOVE; + } + + G.table.activeActionMoves.mirror = false; +}; diff --git a/packages/webapp/src/game/moves/recruit.ts b/packages/webapp/src/game/moves/recruit.ts new file mode 100644 index 00000000..87b9d5f1 --- /dev/null +++ b/packages/webapp/src/game/moves/recruit.ts @@ -0,0 +1,69 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { isInRange } from '../utils'; +import { ActiveProjectsSelector } from '../store/slice/activeProjects'; +import { DeckMutator, DeckSelector } from '../store/slice/deck'; +import { ActiveProjectMutator, ActiveProjectSelector } from '../store/slice/activeProject/activeProject'; +import { GameMove } from './actionMoves'; +import { CardsMutator, CardsSelector } from '../store/slice/cards'; + +export type Recruit = (resourceCardIndex: number, activeProjectIndex: number) => void; + +export const recruit: GameMove = ({ G, playerID }, jobCardIndex, activeProjectIndex) => { + if (!G.table.activeActionMoves.recruit) { + return INVALID_MOVE; + } + + const currentPlayer = playerID; + const currentPlayerToken = G.players[currentPlayer].token; + const recruitActionCosts = 1; + if (currentPlayerToken.actions < recruitActionCosts) { + return INVALID_MOVE; + } + const recruitWorkerCosts = 1; + if (currentPlayerToken.workers < recruitWorkerCosts) { + return INVALID_MOVE; + } + + const currentJobs = G.table.activeJobs; + if (!isInRange(jobCardIndex, currentJobs.length)) { + return INVALID_MOVE; + } + + const activeProjects = G.table.activeProjects; + if (!isInRange(activeProjectIndex, activeProjects.length)) { + return INVALID_MOVE; + } + const jobCard = CardsSelector.getById(currentJobs, jobCardIndex); + const activeProject = ActiveProjectsSelector.getById(G.table.activeProjects, activeProjectIndex); + const jobContribution = ActiveProjectSelector.getJobContribution(activeProject, jobCard.name); + // Check job requirment is not fulfilled yet + if (jobContribution >= activeProject.card.requirements[jobCard.name]) { + return INVALID_MOVE; + } + // User cannot place more than one worker in same job + if (ActiveProjectSelector.hasWorker(activeProject, jobCard.name, currentPlayer)) { + return INVALID_MOVE; + } + + // reduce action + currentPlayerToken.actions -= recruitActionCosts; + CardsMutator.removeOne(currentJobs, jobCard); + + // reduce worker tokens + currentPlayerToken.workers -= recruitWorkerCosts; + // assign worker token + const jobInitPoints = 1; + ActiveProjectMutator.assignWorker(activeProject, jobCard.name, currentPlayer, jobInitPoints); + + // discard job card + DeckMutator.discard(G.decks.jobs, [jobCard]); + + // Refill job card + const maxJobCards = 5; + const refillCardNumber = maxJobCards - currentJobs.length; + const jobCards = DeckSelector.peek(G.decks.jobs, refillCardNumber); + DeckMutator.draw(G.decks.jobs, refillCardNumber); + CardsMutator.add(currentJobs, jobCards); + + G.table.activeActionMoves.recruit = false; +}; diff --git a/packages/webapp/src/game/moves/removeAndRefillJobs.ts b/packages/webapp/src/game/moves/removeAndRefillJobs.ts new file mode 100644 index 00000000..4fd52aa9 --- /dev/null +++ b/packages/webapp/src/game/moves/removeAndRefillJobs.ts @@ -0,0 +1,30 @@ +import { INVALID_MOVE } from 'boardgame.io/core'; +import { isInRange } from '../utils'; +import { DeckMutator, DeckSelector } from '../store/slice/deck'; +import { GameMove } from './actionMoves'; +import { CardsMutator } from '../store/slice/cards'; + +export type RemoveAndRefillJobs = (jobCardIndices: number[]) => void; +export const removeAndRefillJobs: GameMove = ({ G }, jobCardIndices) => { + if (!G.table.activeActionMoves.removeAndRefillJobs) { + return INVALID_MOVE; + } + + const currentJob = G.table.activeJobs; + const jobDeck = G.decks.jobs; + const isInvalid = jobCardIndices.map(index => !isInRange(index, currentJob.length)).some(x => x); + if (isInvalid) { + return INVALID_MOVE; + } + const removedJobCards = jobCardIndices.map(index => currentJob[index]); + CardsMutator.remove(currentJob, removedJobCards); + DeckMutator.discard(jobDeck, removedJobCards); + + const maxJobCards = 5; + const refillCardNumber = maxJobCards - currentJob.length; + const jobCards = DeckSelector.peek(jobDeck, refillCardNumber); + DeckMutator.draw(jobDeck, refillCardNumber); + CardsMutator.add(currentJob, jobCards); + + G.table.activeActionMoves.removeAndRefillJobs = false; +}; diff --git a/packages/webapp/src/game/players/players.ts b/packages/webapp/src/game/players/players.ts deleted file mode 100644 index 77766e9c..00000000 --- a/packages/webapp/src/game/players/players.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PlayerID } from "boardgame.io"; -import { ForceCard, ProjectCard } from "../cards/card"; - -export interface Hand { - projects: ProjectCard[]; - forces: ForceCard[]; -} - -export interface Player { - hand: Hand; - token: { - workers: number; - actions: number; - }; - completed: { - projects: ProjectCard[]; - }; - victoryPoints: number; -} - -export type Players = Record; - -export const setupPlayers = (playerNames: string[]): Players => { - const players: Players = {}; - playerNames.forEach(player => { - players[player] = { - hand: { projects: [], forces: [] }, - token: { workers: 0, actions: 0 }, - completed: { projects: [] }, - victoryPoints: 0, - } - }); - return players; -} diff --git a/packages/webapp/src/game/store/slice/activeProject/activeProject.mutators.ts b/packages/webapp/src/game/store/slice/activeProject/activeProject.mutators.ts new file mode 100644 index 00000000..eaa15767 --- /dev/null +++ b/packages/webapp/src/game/store/slice/activeProject/activeProject.mutators.ts @@ -0,0 +1,22 @@ +import { PlayerID } from "boardgame.io"; +import { JobName } from "../card"; +import { findContribution } from "./activeProject.utils"; +import { ActiveProject, ProjectContribution } from "./activeProject"; + +const assignWorker = (state: ActiveProject, jobName: JobName, playerId: PlayerID, points: number): void => { + const contribution: ProjectContribution = { jobName, worker: playerId, value: points }; + state.contributions.push(contribution); +}; + +const pushWorker = (state: ActiveProject, jobName: JobName, playerId: PlayerID, points: number): void => { + const contribution = findContribution(state.contributions, jobName, playerId); + if (!contribution) { + throw new Error(`${jobName} work played by ${playerId} not found in ${state.card.name}`); + } + contribution.value += points; +}; + +export const mutators = { + assignWorker, + pushWorker, +}; diff --git a/packages/webapp/src/game/store/slice/activeProject/activeProject.selectors.ts b/packages/webapp/src/game/store/slice/activeProject/activeProject.selectors.ts new file mode 100644 index 00000000..04a0e101 --- /dev/null +++ b/packages/webapp/src/game/store/slice/activeProject/activeProject.selectors.ts @@ -0,0 +1,46 @@ +import { PlayerID } from "boardgame.io"; +import { findContribution } from "./activeProject.utils"; +import { ActiveProject } from "./activeProject"; +import { JobName } from "@/game/store/slice/card"; + +const hasWorker = (state: ActiveProject, jobName: JobName, playerId: PlayerID): boolean => { + const contribution = findContribution(state.contributions, jobName, playerId); + return contribution !== undefined; +} + +const getWorkerContribution = (state: ActiveProject, jobName: JobName, playerId: PlayerID): number => { + const contribution = findContribution(state.contributions, jobName, playerId); + return contribution?.value ?? 0; +} + +const getJobContribution = (state: ActiveProject, jobName: JobName): number => { + const jobContribution = state.contributions + .filter(contribution => contribution.jobName === jobName) + .map(contribution => contribution.value) + .reduce((a, b) => a + b, 0); + return jobContribution; +} + +const getPlayerContribution = (state: ActiveProject, playerId: PlayerID): number => { + const playerContribution = state.contributions + .filter(contribution => contribution.worker === playerId) + .map(contribution => contribution.value) + .reduce((a, b) => a + b, 0); + return playerContribution; +} + +const getPlayerWorkerTokens = (state: ActiveProject, playerId: PlayerID): number => { + const ownerToken = state.owner === playerId ? 1 : 0; + const jobTokens = state.contributions + .filter(contribution => contribution.worker === playerId) + .length; + return ownerToken + jobTokens; +} + +export const selectors = { + hasWorker, + getWorkerContribution, + getJobContribution, + getPlayerContribution, + getPlayerWorkerTokens, +}; diff --git a/packages/webapp/src/game/store/slice/activeProject/activeProject.ts b/packages/webapp/src/game/store/slice/activeProject/activeProject.ts new file mode 100644 index 00000000..01836504 --- /dev/null +++ b/packages/webapp/src/game/store/slice/activeProject/activeProject.ts @@ -0,0 +1,31 @@ +import { PlayerID } from "boardgame.io"; +import { mutators } from "./activeProject.mutators"; +import { selectors } from "./activeProject.selectors"; +import { Contribution } from "../../../moves/actionMoves"; +import { ProjectCard } from "../card"; + +export interface ProjectContribution extends Contribution { + worker: PlayerID; +} + +export interface ActiveProject { + card: ProjectCard; + owner: PlayerID; + contributions: ProjectContribution[]; +} + +const initialState = (): ActiveProject => ({ + card: { name: '', requirements: {} }, + owner: '', + contributions: [], +}) + +const ActiveProjectSlice = { + initialState, + mutators, + selectors, +}; + +export const ActiveProjectMutator = ActiveProjectSlice.mutators; +export const ActiveProjectSelector = ActiveProjectSlice.selectors; +export default ActiveProjectSlice; diff --git a/packages/webapp/src/game/store/slice/activeProject/activeProject.utils.ts b/packages/webapp/src/game/store/slice/activeProject/activeProject.utils.ts new file mode 100644 index 00000000..9481ba1b --- /dev/null +++ b/packages/webapp/src/game/store/slice/activeProject/activeProject.utils.ts @@ -0,0 +1,5 @@ +import { PlayerID } from "boardgame.io"; +import { JobName } from "../card"; +import { ProjectContribution } from "./activeProject"; + +export const findContribution = (contributions: ProjectContribution[], jobName: JobName, playerId: PlayerID) => contributions.find(contribution => contribution.jobName === jobName && contribution.worker === playerId); diff --git a/packages/webapp/src/game/store/slice/activeProjects.ts b/packages/webapp/src/game/store/slice/activeProjects.ts new file mode 100644 index 00000000..02d693e2 --- /dev/null +++ b/packages/webapp/src/game/store/slice/activeProjects.ts @@ -0,0 +1,52 @@ +import { PlayerID } from 'boardgame.io'; +import { filterInplace } from '../../utils'; +import { ProjectCard } from './card'; +import { ActiveProject, ActiveProjectSelector } from './activeProject/activeProject'; + +const initialState = (): ActiveProject[] => []; + +const add = (state: ActiveProject[], card: ProjectCard, owner: PlayerID): void => { + const activeProject: ActiveProject = { + card, + owner, + contributions: [], + }; + state.push(activeProject); +} + +const remove = (state: ActiveProject[], removedProjects: ActiveProject[]): void => { + filterInplace(state, project => !removedProjects.includes(project)); +} + +const getById = (state: ActiveProject[], index: number): ActiveProject => { + return state[index]; +} + +const getLast = (state: ActiveProject[]): ActiveProject => { + return state[state.length - 1]; +} + +const filterFulfilled = (state: ActiveProject[]): ActiveProject[] => { + return state.filter(project => { + const fulfilledThresholds = Object.keys(project.card.requirements) + .map(jobName => ActiveProjectSelector.getJobContribution(project, jobName) >= project.card.requirements[jobName]); + return fulfilledThresholds.every(x => x); + }); +} + +const ActiveProjectsSlice = { + initialState, + mutators: { + add, + remove, + }, + selectors: { + getById, + getLast, + filterFulfilled, + }, +}; + +export const ActiveProjectsMutator = ActiveProjectsSlice.mutators; +export const ActiveProjectsSelector = ActiveProjectsSlice.selectors; +export default ActiveProjectsSlice; diff --git a/packages/webapp/src/game/cards/card.d.ts b/packages/webapp/src/game/store/slice/card.d.ts similarity index 100% rename from packages/webapp/src/game/cards/card.d.ts rename to packages/webapp/src/game/store/slice/card.d.ts diff --git a/packages/webapp/src/game/store/slice/cards.ts b/packages/webapp/src/game/store/slice/cards.ts new file mode 100644 index 00000000..5d0143a4 --- /dev/null +++ b/packages/webapp/src/game/store/slice/cards.ts @@ -0,0 +1,49 @@ +import { filterInplace } from '../../utils'; + +const getById = (cards: T[], index: number): T => { + return cards[index]; +}; +const add = (cards: T[], newCards: T[]): void => { + cards.push(...newCards); +}; +const addOne = (cards: T[], newCard: T): void => { + cards.push(newCard); +}; +const remove = (cards: T[], discardCards: T[]): void => { + // O(M+N) + // convert discard cards to card => number map + const discardCardsMap = new Map(); + for (let discardCard of discardCards) { + discardCardsMap.set(discardCard, (discardCardsMap.get(discardCard) ?? 0) + 1); + } + // remove discard card as empty in cards + filterInplace(cards, (card) => { + if ((discardCardsMap.get(card) ?? 0) > 0) { + discardCardsMap.set(card, discardCardsMap.get(card)! - 1); + return false; + } + return true; + }); +} +const removeOne = (cards: T[], discardCard: T): void => { + return remove(cards, [discardCard]); +}; + +const initialState = (): T[] => []; + +const CardsSlice = { + initialState, + mutators: { + add, + addOne, + remove, + removeOne, + }, + selectors: { + getById, + }, +}; + +export const CardsMutator = CardsSlice.mutators; +export const CardsSelector = CardsSlice.selectors; +export default CardsSlice; diff --git a/packages/webapp/src/game/store/slice/deck.ts b/packages/webapp/src/game/store/slice/deck.ts new file mode 100644 index 00000000..1889566b --- /dev/null +++ b/packages/webapp/src/game/store/slice/deck.ts @@ -0,0 +1,60 @@ +export interface Deck { + drawPile: T[]; + discardPile: T[]; +} + +const initialState = (): Deck => ({ + drawPile: [], + discardPile: [], +}); + +type Shuffler = (pile: T[]) => T[]; + +const initialize = (state: Deck, cards: T[]): void => { + state.drawPile = cards; + state.discardPile = []; +} + +const shuffleDrawPile = (state: Deck, shuffler: Shuffler): void => { + state.drawPile = shuffler(state.drawPile); +} + +const shuffleDiscardPile = (state: Deck, shuffler: Shuffler): void => { + state.discardPile = shuffler(state.discardPile); +} + +const moveDiscardPileUnderDrawPile = (state: Deck): void => { + state.drawPile.push(...state.discardPile); + state.discardPile = []; +} + +const draw = (state: Deck, n: number): void => { + state.drawPile = state.drawPile.slice(n); +} + +const discard = (state: Deck, discards: T[]): void => { + state.discardPile.push(...discards); +} + +const peek = (state: Deck, n: number): T[] => { + return state.drawPile.slice(0, n); +} + +const DeckSlice = { + initialState, + mutators: { + initialize, + shuffleDrawPile, + shuffleDiscardPile, + moveDiscardPileUnderDrawPile, + draw, + discard, + }, + selectors: { + peek, + }, +}; + +export const DeckMutator = DeckSlice.mutators; +export const DeckSelector = DeckSlice.selectors; +export default DeckSlice; diff --git a/packages/webapp/src/game/store/slice/decks.ts b/packages/webapp/src/game/store/slice/decks.ts new file mode 100644 index 00000000..315e8031 --- /dev/null +++ b/packages/webapp/src/game/store/slice/decks.ts @@ -0,0 +1,22 @@ +import { EventCard, ForceCard, JobCard, ProjectCard } from "./card"; +import DeckSlice, { Deck } from "./deck"; + +export type Decks = { + projects: Deck; + jobs: Deck; + forces: Deck; + events: Deck; +}; + +const initialState = (): Decks => ({ + projects: DeckSlice.initialState(), + jobs: DeckSlice.initialState(), + forces: DeckSlice.initialState(), + events: DeckSlice.initialState(), +}); + +const DecksSlice = { + initialState, +}; + +export default DecksSlice; diff --git a/packages/webapp/src/game/store/slice/players.ts b/packages/webapp/src/game/store/slice/players.ts new file mode 100644 index 00000000..2b7a9f68 --- /dev/null +++ b/packages/webapp/src/game/store/slice/players.ts @@ -0,0 +1,46 @@ +import { PlayerID } from "boardgame.io"; +import { ForceCard, ProjectCard } from "./card"; + +export interface Hand { + projects: ProjectCard[]; + forces: ForceCard[]; +} + +export interface Player { + hand: Hand; + token: { + workers: number; + actions: number; + }; + completed: { + projects: ProjectCard[]; + }; + victoryPoints: number; +} + +const playerInitialState = (): Player => ({ + hand: { projects: [], forces: [] }, + token: { workers: 0, actions: 0 }, + completed: { projects: [] }, + victoryPoints: 0, +}); + +export type Players = Record; + +const initialState = (): Players => ({}); + +export const initialize = (state: Players, playerNames: string[]): void => { + playerNames.forEach(player => { + state[player] = playerInitialState(); + }); +} + +const PlayersSlice = { + initialState, + mutators: { + initialize, + }, +}; + +export const PlayersMutator = PlayersSlice.mutators; +export default PlayersSlice; diff --git a/packages/webapp/src/game/store/slice/table.ts b/packages/webapp/src/game/store/slice/table.ts new file mode 100644 index 00000000..bb3bb65e --- /dev/null +++ b/packages/webapp/src/game/store/slice/table.ts @@ -0,0 +1,33 @@ +import { EventCard, JobCard } from "./card"; +import { ActionMoves } from "../../moves/actionMoves"; +import ActiveProjectsSlice from "./activeProjects"; +import { ActiveProject } from "./activeProject/activeProject"; + +export type ActiveActionMoves = Record; + +export interface Table { + activeEvent: EventCard | null; + activeProjects: ActiveProject[]; + activeJobs: JobCard[]; + activeActionMoves: ActiveActionMoves; +} + +const initialState = (): Table => ({ + activeEvent: null, + activeProjects: ActiveProjectsSlice.initialState(), + activeJobs: [], + activeActionMoves: { + contributeJoinedProjects: true, + contributeOwnedProjects: true, + createProject: true, + recruit: true, + removeAndRefillJobs: true, + mirror: true, + }, +}); + +const TableSlice = { + initialState, +}; + +export default TableSlice; diff --git a/packages/webapp/src/game/store/store.ts b/packages/webapp/src/game/store/store.ts new file mode 100644 index 00000000..a04e3cfd --- /dev/null +++ b/packages/webapp/src/game/store/store.ts @@ -0,0 +1,26 @@ +import PlayersSlice, { Players } from "./slice/players"; +import TableSlice, { Table } from "./slice/table"; +import DecksSlice, { Decks } from "./slice/decks"; + +export interface Rule { +} + +export interface GameState { + rules: Rule; + decks: Decks; + table: Table; + players: Players; +} + +const initialState = (): GameState => ({ + rules: {}, + decks: DecksSlice.initialState(), + table: TableSlice.initialState(), + players: PlayersSlice.initialState(), +}); + +const GameStore = { + initialState, +}; + +export default GameStore; diff --git a/packages/webapp/src/game/table/activeProjects.test.ts b/packages/webapp/src/game/table/activeProjects.test.ts deleted file mode 100644 index c80b1a34..00000000 --- a/packages/webapp/src/game/table/activeProjects.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { ActiveProject, ActiveProjects } from './activeProjects'; -import { ProjectCard } from '../cards/card'; -import { Project } from './table' - -const mockJob1 = 'a'; -const mockJob2 = 'b'; -const mockJob3 = 'c'; -const mockCard: ProjectCard = { - name: 'test 1', - requirements: { [mockJob1]: 5, [mockJob2]: 4, [mockJob3]: 8 }, -}; -const mockPlayer1 = 'test player 1'; -const mockPlayer2 = 'test player 2'; - -describe('ActiveProjects', () => { - describe('Add', () => { - it('should append a new project', () => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - expect(activeProjectId).toEqual(0); - }); - }); - - describe('GetById', () => { - it('should return active project', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - - expect(activeProject.card).toEqual(mockCard); - }); - }); - - describe('FilterFulfilled', () => { - it('should return fulfilled active projects', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - const initPoints = 1; - // job 1: 1 / 5 - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - // job 2: 1 / 4 - ActiveProject.AssignWorker(activeProject, mockJob2, mockPlayer2, initPoints); - // job 3: 1 / 8 - ActiveProject.AssignWorker(activeProject, mockJob3, mockPlayer1, initPoints); - // job 3: 2 / 8 - ActiveProject.AssignWorker(activeProject, mockJob3, mockPlayer2, initPoints); - // job 1: 5 / 5 - ActiveProject.PushWorker(activeProject, mockJob1, mockPlayer1, 4); - // job 2: 4 / 4 - ActiveProject.PushWorker(activeProject, mockJob2, mockPlayer2, 3); - // job 3: 6 / 8 - ActiveProject.PushWorker(activeProject, mockJob3, mockPlayer2, 4); - // job 3: 8 / 8 - ActiveProject.PushWorker(activeProject, mockJob3, mockPlayer1, 2); - - const fulfilledProjects = ActiveProjects.FilterFulfilled(activeProjects); - - expect(fulfilledProjects).toHaveLength(1); - expect(fulfilledProjects[0].card).toEqual(mockCard); - }); - }); - - describe('Remove', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - // job 1: 5 - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, 5); - // job 2: 4 - ActiveProject.AssignWorker(activeProject, mockJob2, mockPlayer2, 4); - // job 3: 8 - ActiveProject.AssignWorker(activeProject, mockJob3, mockPlayer1, 8); - const fulfilledProjects = ActiveProjects.FilterFulfilled(activeProjects); - - ActiveProjects.Remove(activeProjects, fulfilledProjects); - - expect(activeProjects).toHaveLength(0); - }); -}); - -describe('ActiveProject', () => { - describe('New Active Project', () => { - test.each([ - [mockJob1, mockPlayer1], - [mockJob1, mockPlayer2], - [mockJob2, mockPlayer1], - [mockJob2, mockPlayer2], - [mockJob3, mockPlayer1], - [mockJob3, mockPlayer2], - ])('job %s x player %s should has no worker', (mockJob, mockPlayer) => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - expect(ActiveProject.HasWorker(activeProject, mockJob, mockPlayer)).toBeFalsy(); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob, mockPlayer)).toBe(0); - }); - - test.each([ - [mockJob1], - [mockJob2], - [mockJob3], - ])('job %s should has no contribution', (mockJob) => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - expect(ActiveProject.GetJobContribution(activeProject, mockJob)).toBe(0); - }); - - test.each([ - [mockPlayer1], - [mockPlayer2], - ])('player %s should have no contribution', (mockPlayer) => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer)).toBe(0); - }); - - test('owner should have one worker token', () => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - expect(ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer1)).toBe(1); - }); - - test('the other players should have NO worker token', () => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - expect(ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer2)).toBe(0); - }); - }); - - describe('AssignWorker', () => { - it('should store inital points', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - - expect(ActiveProject.HasWorker(activeProject, mockJob1, mockPlayer1)).toBeTruthy(); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob1, mockPlayer1)).toBe(initPoints); - expect(ActiveProject.GetJobContribution(activeProject, mockJob1)).toBe(initPoints); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer1)).toBe(initPoints); - }); - - it('should store each initial points on same job from different players', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer2, initPoints); - - expect(ActiveProject.HasWorker(activeProject, mockJob1, mockPlayer1)).toBeTruthy(); - expect(ActiveProject.HasWorker(activeProject, mockJob1, mockPlayer2)).toBeTruthy(); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob1, mockPlayer1)).toBe(initPoints); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob1, mockPlayer2)).toBe(initPoints); - expect(ActiveProject.GetJobContribution(activeProject, mockJob1)).toBe(initPoints + initPoints); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer1)).toBe(initPoints); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer2)).toBe(initPoints); - }); - - it('should store each initial points on different job from the same player', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - ActiveProject.AssignWorker(activeProject, mockJob2, mockPlayer1, initPoints); - - expect(ActiveProject.HasWorker(activeProject, mockJob1, mockPlayer1)).toBeTruthy(); - expect(ActiveProject.HasWorker(activeProject, mockJob2, mockPlayer1)).toBeTruthy(); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob1, mockPlayer1)).toBe(initPoints); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob2, mockPlayer1)).toBe(initPoints); - expect(ActiveProject.GetJobContribution(activeProject, mockJob1)).toBe(initPoints); - expect(ActiveProject.GetJobContribution(activeProject, mockJob2)).toBe(initPoints); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer1)).toBe(initPoints + initPoints); - }); - - it('should use one job token', () => { - const activeProjects: Project[] = []; - - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer2, initPoints); - - const ownerToken = 1; - const jobToken = 1; - expect(ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer1)).toBe(ownerToken + jobToken); - expect(ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer2)).toBe(jobToken); - }); - }); - - describe('PushWorker', () => { - it('Worker should has correct points after pushed', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - - const pushedPoints = 3; - ActiveProject.PushWorker(activeProject, mockJob1, mockPlayer1, pushedPoints); - - expect(ActiveProject.HasWorker(activeProject, mockJob1, mockPlayer1)).toBeTruthy(); - expect(ActiveProject.GetWorkerContribution(activeProject, mockJob1, mockPlayer1)).toBe(initPoints + pushedPoints); - expect(ActiveProject.GetJobContribution(activeProject, mockJob1)).toBe(initPoints + pushedPoints); - expect(ActiveProject.GetPlayerContribution(activeProject, mockPlayer1)).toBe(initPoints + pushedPoints); - }); - - it('should NOT increase job token usage', () => { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - const workerTokensBeforeAct = ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer1); - - const pushedPoints = 3; - ActiveProject.PushWorker(activeProject, mockJob1, mockPlayer1, pushedPoints); - - const workerTokensAfterAct = ActiveProject.GetPlayerWorkerTokens(activeProject, mockPlayer1); - expect(workerTokensAfterAct).toBe(workerTokensBeforeAct); - }); - - it('should throw error when push a non assigned worker', () => { - try { - const activeProjects: Project[] = []; - const activeProjectId = ActiveProjects.Add(activeProjects, mockCard, mockPlayer1); - const activeProject = ActiveProjects.GetById(activeProjects, activeProjectId); - const initPoints = 1; - ActiveProject.AssignWorker(activeProject, mockJob1, mockPlayer1, initPoints); - - const pushedPoints = 3; - ActiveProject.PushWorker(activeProject, mockJob1, mockPlayer2, pushedPoints); - // expect raise an error - expect(true).toBe(false); - } catch (err) { - expect(err).toBeTruthy(); - } - }); - }); -}); diff --git a/packages/webapp/src/game/table/activeProjects.ts b/packages/webapp/src/game/table/activeProjects.ts deleted file mode 100644 index 16f97f6f..00000000 --- a/packages/webapp/src/game/table/activeProjects.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { PlayerID } from 'boardgame.io'; -import { filterInplace } from '../utils'; -import { JobName, ProjectCard } from '../cards/card'; -import { Project, ProjectContribution } from './table'; - -export interface IActiveProjects { - // add a project card in active projects pool and assign it to the owner. Return the active project index - Add(activeProjects: Project[], card: ProjectCard, owner: PlayerID): number; - GetById(activeProjects: Project[], index: number): Project; - FilterFulfilled(activeProjects: Project[]): Project[]; - Remove(activeProjects: Project[], removedProjects: Project[]): void; -} - -export const ActiveProjects: IActiveProjects = { - Add(activeProjects, card, owner) { - const activeProject: Project = { - card, - owner, - contributions: [], - }; - activeProjects.push(activeProject); - // return the active project index - return activeProjects.length - 1; - }, - GetById(activeProjects, index) { - return activeProjects[index]; - }, - FilterFulfilled(activeProjects) { - return activeProjects.filter(project => { - const fulfilledThresholds = Object.keys(project.card.requirements) - .map(jobName => ActiveProject.GetJobContribution(project, jobName) >= project.card.requirements[jobName]); - return fulfilledThresholds.every(x => x); - }); - }, - Remove(activeProjects, removedProjects) { - filterInplace(activeProjects, project => !removedProjects.includes(project)); - }, -}; - -export interface IActiveProject { - HasWorker(activeProject: Project, jobName: JobName, playerId: PlayerID): boolean; - GetWorkerContribution(activeProject: Project, jobName: JobName, playerId: PlayerID): number; - GetJobContribution(activeProject: Project, jobName: JobName): number; - GetPlayerContribution(activeProject: Project, playerId: PlayerID): number; - GetPlayerWorkerTokens(activeProject: Project, playerId: PlayerID): number; - AssignWorker(activeProject: Project, jobName: JobName, playerId: PlayerID, points: number): void; - PushWorker(activeProject: Project, jobName: JobName, playerId: PlayerID, points: number): void; -} - -const findContribution = (contributions: ProjectContribution[], jobName: JobName, playerId: PlayerID) => - contributions.find(contribution => contribution.jobName === jobName && contribution.worker === playerId); - -export const ActiveProject: IActiveProject = { - HasWorker(activeProject, jobName, playerId) { - const contribution = findContribution(activeProject.contributions, jobName, playerId); - return contribution !== undefined; - }, - GetWorkerContribution(activeProject, jobName, playerId) { - const contribution = findContribution(activeProject.contributions, jobName, playerId); - return contribution?.value ?? 0; - }, - GetJobContribution(activeProject, jobName) { - const jobContribution = activeProject.contributions - .filter(contribution => contribution.jobName === jobName) - .map(contribution => contribution.value) - .reduce((a, b) => a + b, 0); - return jobContribution; - }, - GetPlayerContribution(activeProject, playerId) { - const playerContribution = activeProject.contributions - .filter(contribution => contribution.worker === playerId) - .map(contribution => contribution.value) - .reduce((a, b) => a + b, 0); - return playerContribution; - }, - GetPlayerWorkerTokens(activeProject, playerId) { - const ownerToken = activeProject.owner === playerId ? 1 : 0; - const jobTokens = activeProject.contributions - .filter(contribution => contribution.worker === playerId) - .length; - return ownerToken + jobTokens; - }, - AssignWorker(activeProject, jobName, playerId, points) { - const contribution: ProjectContribution = { jobName, worker: playerId, value: points }; - activeProject.contributions.push(contribution); - }, - PushWorker(activeProject, jobName, playerId, points) { - const contribution = findContribution(activeProject.contributions, jobName, playerId); - if (!contribution) { - throw new Error(`${jobName} work played by ${playerId} not found in ${activeProject.card.name}`); - } - contribution.value += points; - }, -}; diff --git a/packages/webapp/src/game/table/table.ts b/packages/webapp/src/game/table/table.ts deleted file mode 100644 index 09a7c69d..00000000 --- a/packages/webapp/src/game/table/table.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PlayerID } from "boardgame.io"; -import { EventCard, JobCard, ProjectCard } from "../cards/card"; -import { ActionMoves, Contribution } from "../moves/actionMoves"; - -export interface ProjectContribution extends Contribution { - worker: PlayerID; -} - -export interface Project { - card: ProjectCard; - owner: PlayerID; - contributions: ProjectContribution[]; -} - -export type ActiveActionMoves = Record; - -export interface Table { - activeEvent: EventCard | null; - activeProjects: Project[]; - activeJobs: JobCard[]; - activeActionMoves: ActiveActionMoves; -} - -export const setupTable = (): Table => { - return { - activeEvent: null, - activeProjects: [], - activeJobs: [], - activeActionMoves: { - contributeJoinedProjects: true, - contributeOwnedProjects: true, - createProject: true, - recruit: true, - removeAndRefillJobs: true, - mirror: true, - }, - }; -}