Illegal moves API
See original GitHub issueBetween the problem and proposed solution below, the former is more important - someone else might come up with a better solution.
Background
Some moves are illegal. The logic behind an illegal move depends on any combination of these:
G
ctx
- Additional arguments passed
Note that it may be desired to know if a move is legal before actually attempting to dispatch it. Examples:
- A poker client disabling some buttons.
- A player getting an immediate feedback that a stone cannot be place in a particular position in a game of GO.
Also note that in the last example some reason might be useful (“Stone will have no liberties”). This is to highlight that just returning a boolean may not suffice.
Currently
Client
Here’s an example from the docs:
class TicTacToeBoard extends React.Component {
onClick(id) {
if (this.isActive(id)) {
this.props.moves.clickCell(id);
this.props.events.endTurn();
}
}
isActive(id) {
if (!this.props.isActive) return false;
if (this.props.G.cells[id] !== null) return false;
return true;
}
...
}
But this doesn’t prevent a hacker dispatching an illegal move to the server. So…
Server
A move function that returns undefined denotes an illegal move:
moves: {
clickCell(G, ctx, id) {
if (G.cells[id] !== null) return undefined;
...
},
},
Note that you cannot invoke a move to check if it’s illegal - if it is legal, it will be executed; so the check needs to live elsewhere.
In other words, the fact that one can return undefined
from the move function fuses together two separate concerns:
- The execution of the move.
- The legal check.
The issue
There’s potential repetition here. Clean coders will extract the related logic into a function; something like:
const isCellFree = (G, ctx, id) => G.cells[id] === null;
And will use this in both client (react) and game config.
This is a simple case. If such function needs to return a reason it may involve quite a few checks with some games.
Proposed solution
I believe that the logic of weather a move is illegal should live as close as possible to the move definition itself.
So how about:
moves: {
clickCell: {
reduce: (G, ctx, id) => {
const cells = [...G.cells];
cells[id] = ctx.currentPlayer;
return { ...G, cells };
},
isValid: (G, ctx, id) => G.cells[id] === null,
},
},
Notes:
- Internally, the framework will call
isValid
(if provided) before calling reduce. - Perhaps allow both current and proposed versions (for cases where
isValid
is not needed). - Client API will be
moves.clickCell(id)
andmoves.clickCell.isValid(id)
Revision
Consider:
const clickCell = (G, ctx, id) => {
const cells = [ ...G.cells ];
cells[id] = ctx.currentPlayer;
return { ...G, cells };
};
clickCell.isValid = (G, ctx, id) => G.cells[id] === null;
const TicTacToe = Game({
moves: {
clickCell,
},
});
Which doesn’t change the current API, yet allow to plug in move validation.
Others may have better suggestions.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:23 (17 by maintainers)
My personal concern is not only whether to check if the move is legal, but also to give the client the list of possible moves. The client shouldn’t have to test every possible move and check if it’s valid or not before displaying them in the UI.
It’s especially true if the backend / client run separate stacks, i.e. the logic would be on the server side, and the client doesn’t run bgio (because someone made a bot to play the game, or wants to make an android application for the game, …), then the client absolutely needs to have a list of possible moves.
My suggested interface is this, for tic-tac-toe:
Then boardgame.io needs to have the list of available moves somewhere, probably in
Context
, generated like this:So for a tictactoe game,
ctx.availableMoves
would look like this:Then to check if it’s an illegal move, on boardgame.io’s side:
With that, for tic tac toe, all invalid inputs, like an already check cell, or even invalid cell ids like
1.2
,"abcd"
,10000
are easily filtered.Some games, like Dixit, can have free text input. In which case, the
availableMoves
API is not enough, and theisValid
API (included in the code snippet above) makes a lot of sense for that particular move:I think I would love those APIs. And keep the possibility to return
INVALID_MOVE
/ throw an exception in the actual move code, for more complex situations.Currently, to check if a move’s data corresponds to the
availableMoves
given, I suggested anequalsDeep
function, but it can be more elaborate in a later API. For example the game can provide a custommatch
function and return something like[{range: [10, 200]}, ...]
inmove.available(...)
and the game’smatch
function would match{range: [10, 200]}
to10
,11
,12
, …@Stefan-Hanke
That makes sense! But how do you derive such set without calling an
isMoveValid
of sorts behind the scenes?Again, I feel it is important we keep (client-code/consumer) testing in mind here.
That’s all good, but makes an assumption that hackers will not dispatch to the server illegal moves.
In a client/server settings the server must have a way to assert a move is valid regardless of the ui. (I can easily see UIs simply disabling a control if a move is illegal without guarding the code; even if the latter is the case it is easy to hack around).
But I feel I’m just re-iterating what everyone agree with already - a proper game execution will have to involve a valid check before the move is executed, albeit the client responsibility (unless
isValidMove
or the set of valid moves is part of the framework in which case the framework will invoke it automatically before executing a move - the benefits of which is currently being debated).This, in my view, is better than
undefined
.It is also better than the following alternative:
But only so long everyone is happy with the issues I’ve highlighted above related to SOC (interestingly, algebraic types is a typical hint for lack of SOC - that’s what Maybes are for, but I doubt you want to go there).