Skip to content

Commit

Permalink
feat(webapp): add create project action
Browse files Browse the repository at this point in the history
  • Loading branch information
ben196888 committed Jul 21, 2024
1 parent 3a0b5a7 commit 86dbed7
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 41 deletions.
17 changes: 15 additions & 2 deletions packages/webapp/src/components/ActionBoard/ActionBar.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionMoveName } from '@/game/core/stage/action/move/type';
import { ActionMoveName, ActionMoves } from '@/game/core/stage/action/move/type';
import { GameContext } from '../GameContextHelpers';
import { GameState } from '@/game/store/store';
import { RuleSelector } from '@/game/store/slice/rule';
Expand Down Expand Up @@ -58,7 +58,9 @@ const getActionMoveState = (state: GameState, actionMove: ActionMoveName): Actio
};

export const mapGameContextToProps = (gameContext: GameContext, stateProps: StateProps) => {
const { G, events } = gameContext;
// cast moves to ActionMoves
const { G, events, moves } = gameContext as GameContext & { moves: ActionMoves };
const { selectedHandProjectCards, selectedJobSlots } = stateProps;

const actionsState: Record<UserActionMoves, ActionMoveState> = {
[UserActionMoves.CreateProject]: getActionMoveState(G, UserActionMoves.CreateProject),
Expand All @@ -70,12 +72,23 @@ export const mapGameContextToProps = (gameContext: GameContext, stateProps: Stat
[UserActionMoves.EndActionTurn]: ActionMoveState.Available,
};

const onCreateProject = () => {
if (selectedHandProjectCards.length === 1 && selectedJobSlots.length === 1) {
moves.createProject(selectedHandProjectCards[0], selectedJobSlots[0]);
} else {
// show error message
}
}

const onEndActionTurn = () => {
events.endTurn!();
};

const onActionClick = (action: UserActionMoves) => {
switch (action) {
case UserActionMoves.CreateProject:
onCreateProject();
break;
case UserActionMoves.EndActionTurn:
onEndActionTurn();
break;
Expand Down
22 changes: 16 additions & 6 deletions packages/webapp/src/game/core/stage/action/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,43 @@ import { contributeJoinedProjects } from "./move/contributeJoinedProjects";
import { removeAndRefillJobs } from "./move/removeAndRefillJobs";
import { mirror } from "./move/mirror";
import { GameStageConfig } from "@/game/core/type";
import { INVALID_MOVE } from 'boardgame.io/core';

const withErrorBoundary = (moveFn: (...args: any[]) => any, fallback: any) => (...args: any[]) => {
try {
return moveFn(...args);
} catch (e) {
console.error(e);
}
return fallback;
};

export const action: GameStageConfig = {
moves: {
createProject: {
client: false,
move: createProject,
move: withErrorBoundary(createProject, INVALID_MOVE),
},
recruit: {
// client cannot see decks, discard job card should evaluated on server side
client: false,
move: recruit,
move: withErrorBoundary(recruit, INVALID_MOVE),
},
contributeOwnedProjects: {
client: false,
move: contributeOwnedProjects,
move: withErrorBoundary(contributeOwnedProjects, INVALID_MOVE),
},
contributeJoinedProjects: {
client: false,
move: contributeJoinedProjects,
move: withErrorBoundary(contributeJoinedProjects, INVALID_MOVE),
},
removeAndRefillJobs: {
client: false,
move: removeAndRefillJobs,
move: withErrorBoundary(removeAndRefillJobs, INVALID_MOVE),
},
mirror: {
client: false,
move: mirror,
move: withErrorBoundary(mirror, INVALID_MOVE),
},
},
};
49 changes: 23 additions & 26 deletions packages/webapp/src/game/core/stage/action/move/createProject.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,45 @@
import { INVALID_MOVE } from 'boardgame.io/core';
import { isInRange } from '@/game/utils';
import { ProjectBoardMutator, ProjectBoardSelector } from '@/game/store/slice/projectBoard';
import { DeckMutator, DeckSelector } from '@/game/store/slice/deck';
import { ProjectSlotMutator } from '@/game/store/slice/projectSlot/projectSlot';
import { GameMove } from '@/game/core/type';
import { CardsMutator, CardsSelector } from '@/game/store/slice/cards';
import { ActionSlotMutator, ActionSlotSelector } from '@/game/store/slice/actionSlot';
import { ScoreBoardMutator } from '@/game/store/slice/scoreBoard';
import { PlayersMutator, PlayersSelector } from '@/game/store/slice/players';
import { JobSlotsMutator } from '@/game/store/slice/jobSlots';
import { JobSlotsMutator, JobSlotsSelector } from '@/game/store/slice/jobSlots';
import { RuleSelector } from '@/game/store/slice/rule';

export type CreateProject = (projectCardIndex: number, jobCardIndex: number) => void;
export type CreateProject = (projectCardId: string, jobCardId: string) => void;

export const createProject: GameMove<CreateProject> = ({ G, playerID, events }, projectCardIndex, jobCardIndex) => {
export const createProject: GameMove<CreateProject> = ({ G, playerID }, projectCardId, jobCardId) => {
if (!RuleSelector.isActionSlotAvailable(G.rules, 'createProject')) {
return INVALID_MOVE;
throw new Error('Action slot not available');
}
if (!ActionSlotSelector.isAvailable(G.table.actionSlots.createProject)) {
return INVALID_MOVE;
if (ActionSlotSelector.isOccupied(G.table.actionSlots.createProject)) {
throw new Error('Action slot is occupied');
}

console.log('use action tokens')
// TODO: replace hardcoded number with dynamic rules
const actionTokenCosts = RuleSelector.getActionTokenCost(G.rules, 'createProject');
PlayersMutator.useActionTokens(G.players, playerID, actionTokenCosts);
if (PlayersSelector.getNumActionTokens(G.players, playerID) < 0) {
return INVALID_MOVE;
throw new Error('Not enough action tokens');
}
ActionSlotMutator.occupy(G.table.actionSlots.createProject);

console.log('use worker tokens')
const projectOwnerWorkerTokenCosts = RuleSelector.getProjectOwnerWorkerTokenCost(G.rules, 'createProject');
PlayersMutator.useWorkerTokens(G.players, playerID, projectOwnerWorkerTokenCosts);
if (PlayersSelector.getNumWorkerTokens(G.players, playerID) < 0) {
return INVALID_MOVE;
throw new Error('Not enough worker tokens');
}

console.log('use project card')
// check project card in in hand
const currentHandProjects = G.players[playerID].hand.projects;
if (!isInRange(projectCardIndex, currentHandProjects.length)) {
return INVALID_MOVE;
const projectCard = PlayersSelector.getProjectCardById(G.players, playerID, projectCardId);
if (!projectCard) {
throw new Error('Project card not found');
}
const projectCard = CardsSelector.getById(currentHandProjects, projectCardIndex);
PlayersMutator.useProject(G.players, playerID, projectCard);
ProjectBoardMutator.add(G.table.projectBoard, projectCard);

Expand All @@ -53,35 +49,36 @@ export const createProject: GameMove<CreateProject> = ({ G, playerID, events },

console.log('use job card')
// check job card is on the table
if (!isInRange(jobCardIndex, G.table.jobSlots.length)) {
return INVALID_MOVE;
const jobCard = JobSlotsSelector.getJobCardById(G.table.jobSlots, jobCardId);
if (!jobCard) {
throw new Error('Job card not found');
}

// remove and discard job card
JobSlotsMutator.removeJobCard(G.table.jobSlots, jobCard);
DeckMutator.discard(G.decks.jobs, [jobCard]);

// check job card is required in project
const jobCard = CardsSelector.getById(G.table.jobSlots, jobCardIndex);
if (!Object.keys(projectCard.requirements).includes(jobCard.name)) {
return INVALID_MOVE;
throw new Error('Job card is not required in project');
}
JobSlotsMutator.removeJobCard(G.table.jobSlots, jobCard);

// assign worker token to job slot
const assignWorkerTokenCosts = RuleSelector.getAssignWorkerTokenCost(G.rules, 'createProject');
PlayersMutator.useWorkerTokens(G.players, playerID, assignWorkerTokenCosts);
if (PlayersSelector.getNumWorkerTokens(G.players, playerID) < 0) {
return INVALID_MOVE;
throw new Error('Not enough worker tokens');
}
const initialContributionValue = RuleSelector.getAssignWorkerInitialContributionValue(G.rules, 'createProject');
ProjectSlotMutator.assignWorker(projectSlot, jobCard.name, playerID, initialContributionValue);

console.log('discard and refill job card')
// discard job card
DeckMutator.discard(G.decks.jobs, [jobCard]);

console.log('refill job card')
// Refill job card
const maxJobSlots = RuleSelector.getTableMaxJobSlots(G.rules);
const refillCardNumber = maxJobSlots - G.table.jobSlots.length;
const jobCards = DeckSelector.peek(G.decks.jobs, refillCardNumber);
DeckMutator.draw(G.decks.jobs, refillCardNumber);
CardsMutator.add(G.table.jobSlots, jobCards);
JobSlotsMutator.addJobCards(G.table.jobSlots, jobCards);

console.log('score victory points')
const victoryPoints = RuleSelector.getActionVictoryPoints(G.rules, 'createProject');
Expand Down
14 changes: 9 additions & 5 deletions packages/webapp/src/game/store/slice/jobSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ export type JobSlots = JobCard[];

const initialState = (): JobSlots => [];

const addJobCard = (state: JobSlots, jobCard: JobCard): void => {
state.push(jobCard);
}
const removeJobCard = (state: JobSlots, jobCard: JobCard): void => {
const index = state.findIndex((card) => card.name === jobCard.name);
const index = state.findIndex((card) => card.id === jobCard.id);
if (index !== -1) {
state.splice(index, 1);
}
Expand All @@ -17,14 +14,21 @@ const addJobCards = (state: JobSlots, jobCards: JobCard[]): void => {
state.push(...jobCards);
};

const selectors = {
getJobCardById: (state: JobSlots, id: string): JobCard | undefined => {
return state.find((card) => card.id === id);
},
};

const JobSlotsSlice = {
initialState,
mutators: {
addJobCard,
removeJobCard,
addJobCards,
},
selectors,
};

export const JobSlotsMutator = JobSlotsSlice.mutators;
export const JobSlotsSelector = JobSlotsSlice.selectors;
export default JobSlotsSlice;
11 changes: 9 additions & 2 deletions packages/webapp/src/game/store/slice/players.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ const getProjectCards = (state: Players, playerId: PlayerID): ProjectCard[] => {
return state[playerId].hand.projects;
}

const getProjectCardById = (state: Players, playerId: PlayerID, projectId: string): ProjectCard | undefined => {
return state[playerId].hand.projects.find(p => p.id === projectId);
}

const addProjects = (state: Players, playerId: PlayerID, projects: ProjectCard[]): void => {
state[playerId].hand.projects.push(...projects);
};

const useProject = (state: Players, playerId: PlayerID, project: ProjectCard): void => {
// remove the first project that matches the project card
const index = state[playerId].hand.projects.findIndex(p => p.name === project.name);
state[playerId].hand.projects.splice(index, 1);
const index = state[playerId].hand.projects.findIndex(p => p.id === project.id);
if (index !== -1) {
state[playerId].hand.projects.splice(index, 1);
}
};

const addWorkerTokens = (state: Players, playerId: PlayerID, numWorkers: number): void => {
Expand Down Expand Up @@ -91,6 +97,7 @@ const PlayersSlice = {
getNumActionTokens,
getNumProjects,
getProjectCards,
getProjectCardById,
},
};

Expand Down

0 comments on commit 86dbed7

Please sign in to comment.