Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"presets": [
["es2015", { "modules": false }],
"react",
"stage-0"
],
"env": {
"targets": {
Expand All @@ -21,5 +20,7 @@
"boardgame.io": "./packages",
}
}],
"transform-object-rest-spread",
"transform-class-properties"
]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@
"babel-loader": "^7.1.2",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-module-resolver": "^3.0.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-watch": "^2.0.7",
"chess.js": "^0.10.2",
"coveralls": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion rollup.npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default [
output: { file: 'dist/server.js', format: 'cjs' },
name: 'Server',
plugins: [
babel({ exclude: '**/node_modules/**' }),
babel({ exclude: ['**/node_modules/**'] }),
commonjs({
exclude: 'node_modules/**',
}),
Expand Down
17 changes: 8 additions & 9 deletions src/server/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,27 @@ export class InMemory {
/**
* Write the game state to the in-memory object.
* @param {string} id - The game id.
* @param {object} store - A Redux store to persist.
* @param {object} store - A game state to persist.
*/
set(id, store) {
this.games.set(id, store);
async set(id, state) {
return await this.games.set(id, state);
}

/**
* Read the game state from the in-memory object.
* @param {string} id - The game id.
* @returns {object} - A Redux store with the game state, or undefined
* @returns {object} - A game state, or undefined
* if no game is found with this id.
*/
get(id) {
return this.games.get(id);
async get(id) {
return await this.games.get(id);
}

/**
* Read the game state from the in-memory object.
* @param {string} id - The game id.
* @returns {boolean} - True if a game with this id exists.
*/
has(id) {
return this.games.has(id);
async has(id) {
return await this.games.has(id);
}
}
14 changes: 9 additions & 5 deletions src/server/db.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@
import { InMemory } from './db';
import * as Redux from 'redux';

test('basic', () => {
test('basic', async () => {
const db = new InMemory();
const reducer = () => {};
const store = Redux.createStore(reducer);

// Must return undefined when no game exists.
expect(db.get('gameID')).toEqual(undefined);
let state = await db.get('gameID');
expect(state).toEqual(undefined);

// Create game.
db.set('gameID', store);
await db.set('gameID', store.getState());
// Must return created game.
expect(db.get('gameID')).toEqual(store);
state = await db.get('gameID');
expect(state).toEqual(store.getState());

// Must return true if game exists
expect(db.has('gameID')).toEqual(true);
let has = await db.has('gameID');
expect(has).toEqual(true);
});
51 changes: 27 additions & 24 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ function Server({ games, db }) {
const nsp = app._io.of(game.name);

nsp.on('connection', socket => {
socket.on('action', (action, stateID, gameID, playerID) => {
const store = db.get(gameID);
socket.on('action', async (action, stateID, gameID, playerID) => {
let state = await db.get(gameID);

if (store === undefined) {
if (state === undefined) {
return { error: 'game not found' };
}

const state = store.getState();
const reducer = createGameReducer({
game,
numPlayers: state.ctx.numPlayers,
});
const store = Redux.createStore(reducer, state);

// The null player is a view-only player.
if (playerID == null) {
Expand All @@ -55,33 +59,30 @@ function Server({ games, db }) {
if (state._id == stateID) {
// Update server's version of the store.
store.dispatch(action);
const state = store.getState();
state = store.getState();

// Get clients connected to this current game.
const roomClients = roomInfo.get(gameID);
for (const client of roomClients.values()) {
const playerID = clientInfo.get(client).playerID;
const newState = Object.assign({}, state, {
G: game.playerView(state.G, state.ctx, playerID),
});

if (client === socket.id) {
socket.emit('sync', gameID, {
...state,
G: game.playerView(state.G, state.ctx, playerID),
});
socket.emit('sync', gameID, newState);
} else {
socket.to(client).emit('sync', gameID, {
...state,
G: game.playerView(state.G, state.ctx, playerID),
});
socket.to(client).emit('sync', gameID, newState);
}
}

db.set(gameID, store);
db.set(gameID, store.getState());
}
});

socket.on('sync', (gameID, playerID, numPlayers) => {
socket.on('sync', async (gameID, playerID, numPlayers) => {
socket.join(gameID);

const reducer = createGameReducer({ game, numPlayers });
let roomClients = roomInfo.get(gameID);
if (roomClients === undefined) {
roomClients = new Set();
Expand All @@ -91,18 +92,20 @@ function Server({ games, db }) {

clientInfo.set(socket.id, { gameID, playerID });

let store = db.get(gameID);
if (store === undefined) {
const reducer = createGameReducer({ game, numPlayers });
store = Redux.createStore(reducer);
db.set(gameID, store);
let state = await db.get(gameID);
if (state === undefined) {
const store = Redux.createStore(reducer);
state = store.getState();
await db.set(gameID, state);
}

const state = store.getState();
socket.emit('sync', gameID, {
...state,
const newState = Object.assign({}, state, {
G: game.playerView(state.G, state.ctx, playerID),
});

socket.emit('sync', gameID, newState);

return;
});

socket.on('disconnect', () => {
Expand Down
64 changes: 37 additions & 27 deletions src/server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ jest.mock('koa-socket', () => {
this.broadcast = { emit: jest.fn() };
}

receive(type, ...args) {
this.callbacks[type](args[0], args[1], args[2], args[3], args[4]);
async receive(type, ...args) {
await this.callbacks[type](args[0], args[1], args[2], args[3], args[4]);
return;
}

on(type, callback) {
Expand Down Expand Up @@ -65,7 +66,7 @@ test('basic', () => {
io.socket.receive('disconnect');
});

test('sync', () => {
test('sync', async () => {
const server = Server({ games: [game] });
const io = server.context.io;
expect(server).not.toBe(undefined);
Expand All @@ -74,40 +75,42 @@ test('sync', () => {

// Sync causes the server to respond.
expect(io.socket.emit).toHaveBeenCalledTimes(0);
io.socket.receive('sync', 'gameID');
await io.socket.receive('sync', 'gameID');

expect(io.socket.emit).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalled();

// Sync a second time does not create a game.
spy.mockReset();
io.socket.receive('sync', 'gameID');
await io.socket.receive('sync', 'gameID');

expect(io.socket.emit).toHaveBeenCalledTimes(2);
expect(spy).not.toHaveBeenCalled();

spy.mockRestore();
});

test('action', () => {
test('action', async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async not needed, because not await exist

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, but I forgot to remove it after I clean await s when it didn't work

const server = Server({ games: [game] });
const io = server.context.io;
const action = ActionCreators.gameEvent('endTurn');

io.socket.receive('action', action);
await io.socket.receive('action', action);
expect(io.socket.emit).toHaveBeenCalledTimes(0);
io.socket.emit.mockReset();

io.socket.receive('sync', 'gameID');
await io.socket.receive('sync', 'gameID');
io.socket.id = 'second';
io.socket.receive('sync', 'gameID');
await io.socket.receive('sync', 'gameID');
io.socket.emit.mockReset();

// View-only players cannot send actions.
io.socket.receive('action', action, 0, 'gameID', null);
await io.socket.receive('action', action, 0, 'gameID', null);
expect(io.socket.emit).not.toHaveBeenCalled();

// Actions are broadcasted as state updates.
// The playerID parameter is necessary to account for view-only players.
io.socket.receive('action', action, 0, 'gameID', '0');
await io.socket.receive('action', action, 0, 'gameID', '0');
expect(io.socket.emit).lastCalledWith('sync', 'gameID', {
G: {},
_id: 1,
Expand Down Expand Up @@ -141,55 +144,55 @@ test('action', () => {
io.socket.emit.mockReset();

// ... but not if the gameID is not known.
io.socket.receive('action', action, 1, 'unknown', '1');
await io.socket.receive('action', action, 1, 'unknown', '1');
expect(io.socket.emit).toHaveBeenCalledTimes(0);

// ... and not if the _id doesn't match the internal state.
io.socket.receive('action', action, 100, 'gameID', '1');
await io.socket.receive('action', action, 100, 'gameID', '1');
expect(io.socket.emit).toHaveBeenCalledTimes(0);

// ... and not if player != currentPlayer
io.socket.receive('action', action, 1, 'gameID', '100');
await io.socket.receive('action', action, 1, 'gameID', '100');
expect(io.socket.emit).toHaveBeenCalledTimes(0);

// Another broadcasted action.
io.socket.receive('action', action, 1, 'gameID', '1');
await io.socket.receive('action', action, 1, 'gameID', '1');
expect(io.socket.emit).toHaveBeenCalledTimes(2);
});

test('playerView (sync)', () => {
test('playerView (sync)', async () => {
// Write the player into G.
const game = Game({
playerView: (G, ctx, player) => {
return { ...G, player };
return Object.assign({}, G, { player });
},
});

const server = Server({ games: [game] });
const io = server.context.io;

io.socket.receive('sync', 'gameID', 0);
await io.socket.receive('sync', 'gameID', 0);
expect(io.socket.emit).toHaveBeenCalledTimes(1);
expect(io.socket.emit.mock.calls[0][2].G).toEqual({ player: 0 });
});

test('playerView (action)', () => {
test('playerView (action)', async () => {
const game = Game({
playerView: (G, ctx, player) => {
return { ...G, player };
return Object.assign({}, G, { player });
},
});
const server = Server({ games: [game] });
const io = server.context.io;
const action = ActionCreators.gameEvent('endTurn');

io.socket.id = 'first';
io.socket.receive('sync', 'gameID', '0', 2);
await io.socket.receive('sync', 'gameID', '0', 2);
io.socket.id = 'second';
io.socket.receive('sync', 'gameID', '1', 2);
await io.socket.receive('sync', 'gameID', '1', 2);
io.socket.emit.mockReset();

io.socket.receive('action', action, 0, 'gameID', '0');
await io.socket.receive('action', action, 0, 'gameID', '0');
expect(io.socket.emit).toHaveBeenCalledTimes(2);

const G_player0 = io.socket.emit.mock.calls[0][2].G;
Expand All @@ -199,19 +202,26 @@ test('playerView (action)', () => {
expect(G_player1.player).toBe('1');
});

test('custom db implementation', () => {
test('custom db implementation', async () => {
let getId = null;

class Custom {
get(id) {
constructor() {
this.games = new Map();
}
async get(id) {
getId = id;
return await this.games.get(id);
}
async set(id, state) {
return await this.games.set(id, state);
}
set() {}
}

const game = Game({});
const server = Server({ games: [game], db: new Custom() });
const io = server.context.io;

io.socket.receive('sync', 'gameID');
await io.socket.receive('sync', 'gameID');
expect(getId).toBe('gameID');
});