Skip to content

Commit

Permalink
Merge branch 'main' into refactorPlayerView
Browse files Browse the repository at this point in the history
  • Loading branch information
vdfdev committed Jul 26, 2021
2 parents 87ddff9 + afee0b7 commit d1aa29c
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 41 deletions.
6 changes: 6 additions & 0 deletions docs/documentation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### v0.45.2

#### Bugfixes

* [[9753c0e](https://github.com/boardgameio/boardgame.io/commit/9753c0e)] fix: Don’t leak `STRIP_TRANSIENTS` action ([#961](https://github.com/boardgameio/boardgame.io/pull/961))

### v0.45.1

#### Breaking Changes
Expand Down
6 changes: 6 additions & 0 deletions docs/documentation/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ A plugin is an object that contains the following fields.
// for the master instead.
noClient: ({ G, ctx, game, data, api }) => boolean,

// Function that allows the plugin to indicate that the
// current action should be declared invalid and cancelled.
// If `isInvalid` returns an error message, the whole update
// will be abandoned and an error returned to the client.
isInvalid: ({ G, ctx, game, data, api }) => false | string,

// Function that can filter `data` to hide secret state
// before sending it to a specific client.
// `playerID` could also be null or undefined for spectators.
Expand Down
82 changes: 51 additions & 31 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "boardgame.io",
"version": "0.45.1",
"version": "0.45.2",
"description": "library for turn-based games",
"repository": "https://github.com/boardgameio/boardgame.io",
"scripts": {
Expand Down
15 changes: 14 additions & 1 deletion src/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
gameEvent,
patch,
} from '../core/action-creators';
import * as Actions from '../core/action-types';
import Debug from './debug/Debug.svelte';
import { error } from '../core/logger';
import type { LogEntry, State, SyncInfo } from '../types';
Expand Down Expand Up @@ -144,7 +145,7 @@ describe('multiplayer', () => {

beforeAll(() => {
client = Client({
game: { moves: { A: () => {} } },
game: { moves: { A: () => {}, Invalid: () => INVALID_MOVE } },
multiplayer: SocketIO({ server: host + ':' + port }),
});
client.start();
Expand Down Expand Up @@ -173,6 +174,18 @@ describe('multiplayer', () => {
expect(client.transport.onAction).toHaveBeenCalled();
});

test('strip transients action not sent to transport', () => {
jest.spyOn(client.transport, 'onAction');
const state = { G: {}, ctx: { phase: '' }, plugins: {} };
const filteredMetadata = [];
client.store.dispatch(sync({ state, filteredMetadata } as SyncInfo));
client.moves.Invalid();
expect(client.transport.onAction).not.toHaveBeenCalledWith(
expect.any(Object),
{ type: Actions.STRIP_TRANSIENTS }
);
});

test('Sends and receives chat messages', () => {
jest.spyOn(client.transport, 'onAction');
client.updatePlayerID('0');
Expand Down
10 changes: 8 additions & 2 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ type ClientAction =
| ActionShape.Sync
| ActionShape.Update
| ActionShape.Patch;
type Action = CredentialedActionShape.Any | ClientAction;
type Action =
| CredentialedActionShape.Any
| ActionShape.StripTransients
| ClientAction;

export interface DebugOpt {
target?: HTMLElement;
Expand Down Expand Up @@ -287,7 +290,10 @@ export class _ClientImpl<G extends any = any> {
const baseState = store.getState();
const result = next(action);

if (!('clientOnly' in action)) {
if (
!('clientOnly' in action) &&
action.type !== Actions.STRIP_TRANSIENTS
) {
this.transport.onAction(baseState, action);
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ export enum ActionErrorType {
ActionDisabled = 'action/action_disabled',
// The requested action is not currently possible
ActionInvalid = 'action/action_invalid',
// The requested action was declared invalid by a plugin
PluginActionInvalid = 'action/plugin_invalid',
}
90 changes: 90 additions & 0 deletions src/core/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,96 @@ describe('Events API', () => {
});
});

describe('Plugin Invalid Action API', () => {
const pluginName = 'validator';
const message = 'G.value must divide by 5';
const game: Game<{ value: number }> = {
setup: () => ({ value: 5 }),
plugins: [
{
name: pluginName,
isInvalid: ({ G }) => {
if (G.value % 5 !== 0) return message;
return false;
},
},
],
moves: {
setValue: (G, _ctx, arg) => {
G.value = arg;
},
},
phases: {
unenterable: {
onBegin: () => ({ value: 13 }),
},
enterable: {
onBegin: () => ({ value: 25 }),
},
},
};

let state: State;
beforeEach(() => {
state = InitializeGame({ game });
});

describe('multiplayer client', () => {
const reducer = CreateGameReducer({ game });

test('move is cancelled if plugin declares it invalid', () => {
state = reducer(state, makeMove('setValue', [6], '0'));
expect(state.G).toMatchObject({ value: 5 });
expect(state['transients'].error).toEqual({
type: 'action/plugin_invalid',
payload: { plugin: pluginName, message },
});
});

test('move is processed if no plugin declares it invalid', () => {
state = reducer(state, makeMove('setValue', [15], '0'));
expect(state.G).toMatchObject({ value: 15 });
expect(state['transients']).toBeUndefined();
});

test('event is cancelled if plugin declares it invalid', () => {
state = reducer(state, gameEvent('setPhase', 'unenterable', '0'));
expect(state.G).toMatchObject({ value: 5 });
expect(state.ctx.phase).toBe(null);
expect(state['transients'].error).toEqual({
type: 'action/plugin_invalid',
payload: { plugin: pluginName, message },
});
});

test('event is processed if no plugin declares it invalid', () => {
state = reducer(state, gameEvent('setPhase', 'enterable', '0'));
expect(state.G).toMatchObject({ value: 25 });
expect(state.ctx.phase).toBe('enterable');
expect(state['transients']).toBeUndefined();
});
});

describe('local client', () => {
const reducer = CreateGameReducer({ game, isClient: true });

test('move is cancelled if plugin declares it invalid', () => {
state = reducer(state, makeMove('setValue', [6], '0'));
expect(state.G).toMatchObject({ value: 5 });
expect(state['transients'].error).toEqual({
type: 'action/plugin_invalid',
payload: { plugin: pluginName, message },
});
});

test('move is processed if no plugin declares it invalid', () => {
state = reducer(state, makeMove('setValue', [15], '0'));
expect(state.G).toMatchObject({ value: 15 });
expect(state['transients']).toBeUndefined();
});
});
});

describe('Random inside setup()', () => {
const game1: Game = {
seed: 'seed1',
Expand Down

0 comments on commit d1aa29c

Please sign in to comment.