Skip to content

Commit

Permalink
Merge d9b6e64 into bfc8a1f
Browse files Browse the repository at this point in the history
  • Loading branch information
cristiands7 committed Jul 19, 2021
2 parents bfc8a1f + d9b6e64 commit 02c6914
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 14 deletions.
68 changes: 68 additions & 0 deletions src/core/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,21 @@ describe('infinite loops', () => {
client.moves.endTurn();
expect(client.getState().ctx.currentPlayer).toBe('1');
});

test('loop 5', () => {
const onBegin = (G, ctx) => ctx.events.endPhase();
const game = {
phases: {
A: { onBegin, next: 'B', start: true },
B: { onBegin, next: 'C' },
C: { onBegin, next: 'A' },
},
};

const client = Client({ game, numPlayers: 3 });

expect(client.getState().ctx.phase).toBe(null);
});
});

describe('activePlayers', () => {
Expand Down Expand Up @@ -1084,3 +1099,56 @@ test('events in hooks triggered by moves should be processed', () => {
'1': 'A',
});
});

test('stage events should not be processed out of turn', () => {
const game = {
phases: {
A: {
start: true,
turn: {
activePlayers: {
all: 'A1',
},
stages: {
A1: {
moves: {
endStage: (G, ctx) => {
G.endStage = true;
ctx.events.endStage();
},
},
},
},
},
endIf: (G) => G.endStage,
next: 'B',
},
B: {
turn: {
activePlayers: {
all: 'B1',
},
stages: {
B1: {},
},
},
},
},
};

const client = Client({ game, numPlayers: 3 });

expect(client.getState().ctx.activePlayers).toEqual({
'0': 'A1',
'1': 'A1',
'2': 'A1',
});

client.moves.endStage();

expect(client.getState().ctx.activePlayers).toEqual({
'0': 'B1',
'1': 'B1',
'2': 'B1',
});
});
9 changes: 6 additions & 3 deletions src/plugins/events/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import type { Game, Ctx } from '../../types';
test('constructor', () => {
const flow = {} as Game['flow'];
const playerID = '0';
const e = new Events(flow, playerID);
const e = new Events(flow, { phase: '', turn: 0 } as Ctx, playerID);
expect(e.flow).toBe(flow);
expect(e.playerID).toBe(playerID);
expect(e.dispatch).toEqual([]);
expect(e.initialTurn).toEqual(0);
expect(e.currentPhase).toEqual('');
expect(e.currentTurn).toEqual(0);
});

test('dispatch', () => {
const flow = { eventNames: ['A', 'B'] } as Game['flow'];
const e = new Events(flow);
const events = e.api({ phase: '', turn: 0 } as Ctx);
const e = new Events(flow, { phase: '', turn: 0 } as Ctx);
const events = e.api();

expect(e.dispatch).toEqual([]);
((events as unknown) as { A(): void }).A();
Expand Down
50 changes: 41 additions & 9 deletions src/plugins/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface EventsAPI {
export interface PrivateEventsAPI {
_obj: {
isUsed(): boolean;
updateTurnContext(ctx: Ctx): void;
update(state: State): State;
};
}
Expand All @@ -39,26 +40,30 @@ export class Events {
phase: string;
turn: number;
}>;
initialTurn: number;
currentPhase: string;
currentTurn: number;

constructor(flow: Game['flow'], playerID?: PlayerID) {
constructor(flow: Game['flow'], ctx: Ctx, playerID?: PlayerID) {
this.flow = flow;
this.playerID = playerID;
this.dispatch = [];
this.initialTurn = ctx.turn;
this.updateTurnContext(ctx);
}

/**
* Attaches the Events API to ctx.
* @param {object} ctx - The ctx object to attach to.
*/
api(ctx: Ctx) {
api() {
const events: EventsAPI & PrivateEventsAPI = {
_obj: this,
};
const { phase, turn } = ctx;

for (const key of this.flow.eventNames) {
events[key] = (...args: any[]) => {
this.dispatch.push({ key, args, phase, turn });
this.dispatch.push({
key,
args,
phase: this.currentPhase,
turn: this.currentTurn,
});
};
}

Expand All @@ -69,14 +74,41 @@ export class Events {
return this.dispatch.length > 0;
}

updateTurnContext(ctx: Ctx) {
this.currentPhase = ctx.phase;
this.currentTurn = ctx.turn;
}

/**
* Updates ctx with the triggered events.
* @param {object} state - The state object { G, ctx }.
*/
update(state: State) {
for (let i = 0; i < this.dispatch.length; i++) {
const endedTurns = this.currentTurn - this.initialTurn;
// This is an arbitrarily large number.
const threshold = state.ctx.numPlayers * 10;

// This protects against potential infinite loops if specific events are called on hooks.
// The moment we exceed the defined threshold, we just bail out of all phases.
if (endedTurns > threshold) {
state = { ...state, ctx: { ...state.ctx, phase: null } };
break;
}

const item = this.dispatch[i];

// If the turn already ended,
// don't try to process stage events.
if (
(item.key === 'endStage' ||
item.key === 'setStage' ||
item.key === 'setActivePlayers') &&
item.turn !== state.ctx.turn
) {
continue;
}

// If the turn already ended some other way,
// don't try to end the turn again.
if (item.key === 'endTurn' && item.turn !== state.ctx.turn) {
Expand Down
16 changes: 14 additions & 2 deletions src/plugins/plugin-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ const EventsPlugin: Plugin<EventsAPI & PrivateEventsAPI> = {
return api._obj.isUsed();
},

fnWrap: (fn) => (G, ctx, ...args) => {
const api = ctx.events as PrivateEventsAPI;

if (api) {
api._obj.updateTurnContext(ctx);
}

G = fn(G, ctx, ...args);

return G;
},

dangerouslyFlushRawState: ({ state, api }) => {
return api._obj.update(state);
},

api: ({ game, playerID, ctx }) => {
return new Events(game.flow, playerID).api(ctx);
api: ({ game, ctx, playerID }) => {
return new Events(game.flow, ctx, playerID).api();
},
};

Expand Down

0 comments on commit 02c6914

Please sign in to comment.