UNPKG

boardgame.io

Version:
354 lines (316 loc) 9.25 kB
/* * Copyright 2018 The boardgame.io Authors * * Use of this source code is governed by a MIT-style * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. */ import { LocalTransport, LocalMaster, Local, GetBotPlayer } from './local'; import { gameEvent } from '../../core/action-creators'; import { ProcessGameConfig } from '../../core/game'; import { Client } from '../client'; import { RandomBot } from '../../ai/random-bot'; import { Stage } from '../../core/turn-order'; import type { Game, State } from '../../types'; const sleep = (ms = 500) => new Promise((resolve) => setTimeout(resolve, ms)); describe('bots', () => { const game: Game = { moves: { A: ({ events }) => { events.endTurn(); }, }, ai: { enumerate: () => [{ move: 'A' }], }, }; test('make bot move', async () => { const client = Client({ game: { ...game }, playerID: '0', multiplayer: Local({ bots: { '1': RandomBot } }), }); client.start(); expect(client.getState().ctx.turn).toBe(1); // Make it Player 1's turn and trigger the bot move. client.events.endTurn(); expect(client.getState().ctx.turn).toBe(2); // Wait until the bot has hopefully completed its move. await sleep(); expect(client.getState().ctx.turn).toBe(3); }); test('no bot move', async () => { const client = Client({ numPlayers: 3, game: { ...game }, playerID: '0', multiplayer: Local({ bots: { '2': RandomBot } }), }); client.start(); expect(client.getState().ctx.turn).toBe(1); // Make it Player 1's turn. No bot move. client.events.endTurn(); expect(client.getState().ctx.currentPlayer).toBe('1'); // Wait until the bot has hopefully completed its move. await sleep(); expect(client.getState().ctx.currentPlayer).toBe('1'); expect(client.getState().ctx.numMoves).toBe(0); }); }); describe('GetBotPlayer', () => { test('stages', () => { const result = GetBotPlayer( { ctx: { activePlayers: { '1': Stage.NULL, }, }, } as unknown as State, { '0': {}, '1': {}, } ); expect(result).toEqual('1'); }); test('no stages', () => { const result = GetBotPlayer( { ctx: { currentPlayer: '0', }, } as unknown as State, { '0': {} } ); expect(result).toEqual('0'); }); test('null', () => { const result = GetBotPlayer( { ctx: { currentPlayer: '1', }, } as unknown as State, { '0': {} } ); expect(result).toEqual(null); }); test('gameover', () => { const result = GetBotPlayer( { ctx: { currentPlayer: '0', gameover: true, }, } as unknown as State, { '0': {} } ); expect(result).toEqual(null); }); }); describe('Local', () => { test('transports for same game use shared master', () => { const gameKey = {}; const game = ProcessGameConfig(gameKey); const transport1 = Local()({ game, gameKey, transportDataCallback: () => {}, }); const transport2 = Local()({ game, gameKey, transportDataCallback: () => {}, }); expect(transport1.master).toBe(transport2.master); }); test('transports use shared master with bots', () => { const gameKey = {}; const game = ProcessGameConfig(gameKey); const bots = {}; const transport1 = Local({ bots })({ game, gameKey, transportDataCallback: () => {}, }); const transport2 = Local({ bots })({ game, gameKey, transportDataCallback: () => {}, }); expect(transport1.master).toBe(transport2.master); }); test('transports use different master for different bots', () => { const gameKey = {}; const game = ProcessGameConfig(gameKey); const transport1 = Local({ bots: {} })({ game, gameKey, transportDataCallback: () => {}, }); const transport2 = Local({ bots: {} })({ game, gameKey, transportDataCallback: () => {}, }); expect(transport1.master).not.toBe(transport2.master); }); describe('with localStorage persistence', () => { const game: Game = { setup: () => ({ count: 0 }), moves: { A: ({ G }) => { G.count++; }, }, }; afterEach(() => { localStorage.clear(); }); test('writes to localStorage', () => { const matchID = 'persists-to-ls'; const multiplayer = Local({ persist: true }); const client = Client({ playerID: '0', matchID, game, multiplayer }); client.start(); expect(client.getState().G).toEqual({ count: 0 }); client.moves.A(); expect(client.getState().G).toEqual({ count: 1 }); client.stop(); const stored = JSON.parse(localStorage.getItem('bgio_state')); const [id, state] = stored.find(([id]) => id === matchID); expect(id).toBe(matchID); expect(state.G).toEqual({ count: 1 }); }); test('reads from localStorage', () => { const matchID = 'reads-from-ls'; const storageKey = 'rfls'; const stateMap = { [matchID]: { G: { count: 'foo' }, ctx: {}, }, }; const entriesString = JSON.stringify(Object.entries(stateMap)); localStorage.setItem(`${storageKey}_state`, entriesString); const multiplayer = Local({ persist: true, storageKey }); const client = Client({ playerID: '0', matchID, game, multiplayer }); client.start(); expect(client.getState().G).toEqual({ count: 'foo' }); client.stop(); }); }); }); describe('LocalMaster', () => { let master: LocalMaster; let player0Callback: jest.Mock; let player1Callback: jest.Mock; beforeEach(() => { master = new LocalMaster({ game: {} }); player0Callback = jest.fn(); player1Callback = jest.fn(); master.connect('0', player0Callback); master.connect('1', player1Callback); master.onSync('matchID', '0'); master.onSync('matchID', '1'); }); test('sync', () => { expect(player0Callback).toHaveBeenCalledWith( expect.objectContaining({ type: 'sync' }) ); expect(player1Callback).toHaveBeenCalledWith( expect.objectContaining({ type: 'sync' }) ); }); test('update', () => { master.onUpdate(gameEvent('endTurn'), 0, 'matchID', '0'); expect(player0Callback).toBeCalledWith( expect.objectContaining({ type: 'update' }) ); expect(player1Callback).toBeCalledWith( expect.objectContaining({ type: 'update' }) ); }); test('connect without callback', () => { expect(() => { master.connect('0', undefined); master.onSync('matchID', '0'); }).not.toThrow(); }); }); describe('LocalTransport', () => { describe('update matchID / playerID', () => { const master = { connect: jest.fn(), onSync: jest.fn(), } as unknown as LocalMaster; class WrappedLocalTransport extends LocalTransport { getMatchID() { return this.matchID; } getPlayerID() { return this.playerID; } } const transport = new WrappedLocalTransport({ master, transportDataCallback: () => {}, game: ProcessGameConfig({}), gameKey: {}, }); jest.spyOn(transport, 'requestSync'); beforeEach(() => { jest.resetAllMocks(); }); test('matchID', () => { transport.updateMatchID('test'); expect(transport.getMatchID()).toBe('test'); expect(transport.requestSync).toBeCalled(); }); test('playerID', () => { transport.updatePlayerID('player'); expect(transport.getPlayerID()).toBe('player'); expect(master.connect).toBeCalled(); }); }); describe('multiplayer', () => { const game: Game = { setup: () => ({ initial: true }), moves: { A: () => ({ A: true }), }, }; const multiplayer = Local(); const matchID = 'local-multiplayer'; const client1 = Client({ game, multiplayer, matchID, playerID: '0' }); const client2 = Client({ game, multiplayer, matchID, playerID: '1' }); beforeAll(() => { client1.start(); client2.start(); }); afterAll(() => { client1.stop(); client2.stop(); }); test('send/receive update', () => { expect(client1.getState().G).toStrictEqual({ initial: true }); expect(client2.getState().G).toStrictEqual({ initial: true }); client1.moves.A(); expect(client1.getState().G).toStrictEqual({ A: true }); expect(client2.getState().G).toStrictEqual({ A: true }); }); test('receive sync', () => { const newClient = Client({ game, multiplayer, matchID }); newClient.start(); expect(newClient.getState().G).toStrictEqual({ A: true }); newClient.stop(); }); test('send chat-message', () => { const payload = { message: 'foo' }; client1.sendChatMessage(payload); expect(client2.chatMessages).toStrictEqual([ { id: expect.any(String), sender: '0', payload }, ]); }); }); });