Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/documentation/stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ the global `moves` section for players in that stage (players are not allowed to
any moves from the global `moves` section while they are in that stage). However, if
a stage does not contain a `moves` section, then players can make moves from the global `moves`.

A stage can also set a default move limit. The player automatically exits the stage
after making that number of moves:

```js
stages: {
draw: {
moves: { DrawCard },
maxMoves: 3,
},
}
```

An explicit `maxMoves` passed to `setStage` or `setActivePlayers` overrides the
default from the stage configuration.

!> A move defined in a stage can have the same name as a global move, but it isn't related to the global equivalent in any way.

### Entering Stages
Expand Down
90 changes: 89 additions & 1 deletion src/core/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,47 @@ describe('stages', () => {
state = client.getState();
expect(state.ctx.activePlayers).toEqual({ '1': 'A' });
});

test('stage config provides a default move limit', () => {
const game: Game = {
turn: {
activePlayers: { currentPlayer: 'A' },
stages: {
A: {
maxMoves: 1,
moves: { A: () => {} },
},
},
},
};
const client = Client({ game });

expect(client.getState().ctx._activePlayersMaxMoves).toEqual({ '0': 1 });
client.moves.A();
expect(client.getState().ctx.activePlayers).toBeNull();
});

test('active player config overrides the stage move limit', () => {
const game: Game = {
turn: {
activePlayers: {
currentPlayer: { stage: 'A', maxMoves: 2 },
},
stages: {
A: {
maxMoves: 1,
moves: { A: () => {} },
},
},
},
};
const client = Client({ game });

client.moves.A();
expect(client.getState().ctx.activePlayers).toEqual({ '0': 'A' });
client.moves.A();
expect(client.getState().ctx.activePlayers).toBeNull();
});
});

describe('stage events', () => {
Expand Down Expand Up @@ -808,7 +849,7 @@ describe('stage events', () => {
});

test('with max moves', () => {
const flow = Flow({});
const flow = Flow({ turn: { stages: { A: { maxMoves: 2 } } } });
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.init(state);

Expand All @@ -822,6 +863,33 @@ describe('stage events', () => {
expect(state.ctx._activePlayersMaxMoves).toEqual({ '0': 1 });
});

test('uses max moves from the stage config by default', () => {
const flow = Flow({
turn: {
stages: { A: { maxMoves: 2 } },
},
});
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.init(state);

state = flow.processEvent(state, gameEvent('setStage', 'A'));
expect(state.ctx._activePlayersMaxMoves).toEqual({ '0': 2 });
});

test('stage config replaces a previous move limit', () => {
const flow = Flow({
turn: {
activePlayers: { all: 'B', maxMoves: 5 },
stages: { A: { maxMoves: 2 } },
},
});
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.init(state);

state = flow.processEvent(state, gameEvent('setStage', 'A'));
expect(state.ctx._activePlayersMaxMoves).toEqual({ '0': 2, '1': 5 });
});

test('empty argument ends stage', () => {
const flow = Flow({ turn: { activePlayers: { currentPlayer: 'A' } } });
let state = { G: {}, ctx: flow.ctx(2) } as State;
Expand Down Expand Up @@ -2235,6 +2303,26 @@ describe('backwards compatibility for moveLimit', () => {
expect(state.ctx._activePlayersMinMoves).toBeNull();
expect(state.ctx._activePlayersMaxMoves).toEqual({ '0': 2 });
});

test('stage config maps moveLimit to maxMoves only', () => {
const flow = Flow({
turn: {
activePlayers: { currentPlayer: 'A' },
stages: { A: { moveLimit: 2 } },
},
});
const state = flow.init({ ctx: flow.ctx(2) } as State);

expect(state.ctx._activePlayersMinMoves).toBeNull();
expect(state.ctx._activePlayersMaxMoves).toEqual({ '0': 2 });

const eventFlow = Flow({
turn: { stages: { A: { moveLimit: 3 } } },
});
let eventState = eventFlow.init({ ctx: eventFlow.ctx(2) } as State);
eventState = eventFlow.processEvent(eventState, gameEvent('setStage', 'A'));
expect(eventState.ctx._activePlayersMaxMoves).toEqual({ '0': 3 });
});
});

// These tests serve to document the order in which the various game hooks
Expand Down
33 changes: 25 additions & 8 deletions src/core/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,11 @@ export function Flow({
if (currentPlayer) {
ctx = { ...ctx, currentPlayer };
if (phaseConfig.turn.activePlayers) {
ctx = SetActivePlayers(ctx, phaseConfig.turn.activePlayers);
ctx = SetActivePlayers(
ctx,
phaseConfig.turn.activePlayers,
phaseConfig.turn.stages,
);
}
} else {
// This is only called at the beginning of the phase
Expand Down Expand Up @@ -430,6 +434,15 @@ export function Flow({
_activePlayersMaxMoves = {};
}
_activePlayersMaxMoves[playerID] = arg.maxMoves;
} else {
const stage = GetPhase(ctx).turn.stages[arg.stage];
const maxMoves = stage && (stage.maxMoves || stage.moveLimit);
if (maxMoves) {
if (_activePlayersMaxMoves === null) {
_activePlayersMaxMoves = {};
}
_activePlayersMaxMoves[playerID] = maxMoves;
}
}
}

Expand All @@ -445,7 +458,8 @@ export function Flow({
}

function UpdateActivePlayers(state: State, { arg }): State {
return { ...state, ctx: SetActivePlayers(state.ctx, arg) };
const stages = GetPhase(state.ctx).turn.stages;
return { ...state, ctx: SetActivePlayers(state.ctx, arg, stages) };
}

///////////////
Expand Down Expand Up @@ -659,12 +673,15 @@ export function Flow({
delete _activePlayersMaxMoves[playerID];
}

ctx = UpdateActivePlayersOnceEmpty({
...ctx,
activePlayers,
_activePlayersMinMoves,
_activePlayersMaxMoves,
});
ctx = UpdateActivePlayersOnceEmpty(
{
...ctx,
activePlayers,
_activePlayersMinMoves,
_activePlayersMaxMoves,
},
phaseConfig.turn.stages,
);

// Create log entry.
const action = gameEvent('endStage', arg);
Expand Down
21 changes: 17 additions & 4 deletions src/core/turn-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
ActivePlayersArg,
PlayerID,
State,
StageMap,
TurnConfig,
FnContext,
} from '../types';
Expand Down Expand Up @@ -126,7 +127,11 @@ export function RemovePlayer(ctx: Ctx, playerID: PlayerID): Ctx {
};
}

export function SetActivePlayers(ctx: Ctx, arg: ActivePlayersArg): Ctx {
export function SetActivePlayers(
ctx: Ctx,
arg: ActivePlayersArg,
stages: StageMap = {},
): Ctx {
let activePlayers: typeof ctx.activePlayers = {};
let _prevActivePlayers: typeof ctx._prevActivePlayers = [];
let _nextActivePlayers: ActivePlayersArg | null = null;
Expand Down Expand Up @@ -233,6 +238,14 @@ export function SetActivePlayers(ctx: Ctx, arg: ActivePlayersArg): Ctx {
}
}
}

for (const id in activePlayers) {
const stage = stages[activePlayers[id]];
const maxMoves = stage && (stage.maxMoves || stage.moveLimit);
if (_activePlayersMaxMoves[id] === undefined && maxMoves) {
_activePlayersMaxMoves[id] = maxMoves;
}
}
}

if (Object.keys(activePlayers).length === 0) {
Expand Down Expand Up @@ -268,7 +281,7 @@ export function SetActivePlayers(ctx: Ctx, arg: ActivePlayersArg): Ctx {
* when it becomes empty.
* @param ctx
*/
export function UpdateActivePlayersOnceEmpty(ctx: Ctx) {
export function UpdateActivePlayersOnceEmpty(ctx: Ctx, stages?: StageMap) {
let {
activePlayers,
_activePlayersMinMoves,
Expand All @@ -280,7 +293,7 @@ export function UpdateActivePlayersOnceEmpty(ctx: Ctx) {

if (activePlayers && Object.keys(activePlayers).length === 0) {
if (_nextActivePlayers) {
ctx = SetActivePlayers(ctx, _nextActivePlayers);
ctx = SetActivePlayers(ctx, _nextActivePlayers, stages);
({
activePlayers,
_activePlayersMinMoves,
Expand Down Expand Up @@ -393,7 +406,7 @@ export function InitTurnOrderState(state: State, turn: TurnConfig) {
playOrder.length > 0 ? getCurrentPlayer(playOrder, playOrderPos) : '';

ctx = { ...ctx, currentPlayer, playOrderPos, playOrder };
ctx = SetActivePlayers(ctx, turn.activePlayers || {});
ctx = SetActivePlayers(ctx, turn.activePlayers || {}, turn.stages);

return ctx;
}
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ export interface StageConfig<
> {
moves?: MoveMap<G, PluginAPIs>;
next?: string;
/** Ends the stage automatically after this number of moves. */
maxMoves?: number;
/** @deprecated Use `maxMoves` instead. */
moveLimit?: number;
}

export interface StageMap<
Expand Down