question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Illegal moves API

See original GitHub issue

Between 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) and moves.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:open
  • Created 5 years ago
  • Reactions:2
  • Comments:23 (17 by maintainers)

github_iconTop GitHub Comments

1reaction
coyotte508commented, May 9, 2020

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:

{
  moves: {
    check: {
      move(G, ctx, data) {
         // ...
      },
      available(G, ctx) {
        return [0,1,2,3,4,5,6,7,8].filter(x => !G.cells[x]);
      }
    }
  }
}

Then boardgame.io needs to have the list of available moves somewhere, probably in Context, generated like this:

function generateAvailableMoves (Game, G, ctx) {
  // maybe not needed? Or done elsewhere
  delete ctx.availableMoves;

  const moveDefs = currentPhaseStageMoveDefs(Game, G, ctx);

  const availableMoves = {};

  for (const [moveName, moveDef] of Object.entries(moveDefs)) {
    if (moveDef.available) {
      const moves = moveDef.available(G, ctx);
      if (moves.length > 0) {
        availableMoves[moveName] = moveDef.available(G, ctx);
      }
    } else {
      // Game didn't define the `available` function on the move, we just 
      // say that the move is possible 
      availableMoves[moveName] = true;
    }
  }

  ctx.availableMoves = availableMoves;
}

So for a tictactoe game, ctx.availableMoves would look like this:

// If moves.check.available was defined
{
  cells: [0, 2, 3, 6]
}

// If moves.check.available was NOT defined
{
  cells: true
}

Then to check if it’s an illegal move, on boardgame.io’s side:

function executeMove(Game, G, ctx, move: {name: string, arg: any}) {
  // ...

  if (! (move.name) in ctx.availableMoves) {
    throw InvalidMove(...);
  }

  const available: any[] | true = ctx.availableMoves[move.name];

  if (Array.isArray(available)) {
    if (!available.some(item => equalsDeep(item, move.data))) {
      throw InvalidMove(...);
    }
  }

  // Can even be combined with an additional .isValid function in the move definition
  const moveDef = currentPhaseStageMoveDefs(Game, G, ctx)[move.name];

  if (moveDef.isValid && moveDef.isValid(G, ctx, move.data) === false) {
      throw InvalidMove(...);
  }

  // Then, execute the move
}

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 the isValid API (included in the code snippet above) makes a lot of sense for that particular move:

{
  phases: {
    hinting: {
      moves: {
        hint: {
          move(G, ctx, hint) {
            G.hint = hint;
          },
          isValid(G, ctx, hint) {
            if (typeof hint !== "string" || !hint.length) {
              // No specific error message, the error is obvious
              return false;
            }

            // Throws an assertion error with a message
            assert(hint.length <= 20, "Hint is too long");

            // Probably not needed, since `false` and `undefined` are different values
            return true;
          }
        }
      }
    },
    guessing: {
      moves: {
        guess: {
          move(G, ctx, imageId) {
            G.players[ctx.currentPlayer].guess = imageId;
          },
          available(G, ctx) {
             return G.images;
          }
        }
      }
    }
  }
}

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 an equalsDeep function, but it can be more elaborate in a later API. For example the game can provide a custom match function and return something like [{range: [10, 200]}, ...] in move.available(...) and the game’s match function would match {range: [10, 200]} to 10, 11, 12, …

1reaction
Izhakicommented, Oct 22, 2018

@Stefan-Hanke

Instead of thinking about validation to check whether a move is valid, I like to think about the set of valid moves.

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.

Given that set, validation becomes checking whether the move is contained, and be used by the UI to indicate valid moves. When the game logic rejects a move, there must be a bug somewhere.

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).

Instead of throwing exceptions, I’m pretty much into using algebraic types.

This, in my view, is better than undefined.

It is also better than the following alternative:

return {
  error: undefined, // reason if illegal
  G
}

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).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Illegal moves API · Issue #292 · boardgameio/boardgame.io
A move function that returns undefined denotes an illegal move: moves: { clickCell(G, ctx, id) { if (G.cells[id] !== null) return undefined; ......
Read more >
How to find illegal moves in move generator using Stockfish ...
I'm making a chess program in Unity and I've made a perft function to find the bugs in my move generator. I've downloaded...
Read more >
Is it illegal to use API which are extracted using reverse ...
According to the 2014 ruling of the 9th Circuit Federal Court of Appeals in Oracle vs Google, duplicating a API can be a...
Read more >
Lichess.org API reference
Welcome to the reference for the Lichess API! Lichess is free/libre, open-source chess server powered by volunteers and donations.
Read more >
Documentation - PokéAPI
No authentication is required to access this API, and all resources are fully open and available. Since the move to static hosting in...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found