boardgameio / boardgame.io

State Management and Multiplayer Networking for Turn-Based Games
https://boardgame.io
MIT License
9.99k stars 704 forks source link

Randomized Starting Player #601

Closed ghost closed 4 years ago

ghost commented 4 years ago

As instructed to by @nicolodavis, raising an issue after first discussing this on Gitter.

Issue

Is there a mechanism for randomizing the starting play order of a game, so that a random player starts each game?

ghost commented 4 years ago

Copying suggestion from @syndesis_gitlab on Gitter:

Wouldn't TurnOrder.CUSTOM let you do this?

turn: {
  order: {
    // Get the initial value of playOrderPos.
    // This is called at the beginning of the phase.
    first: (G, ctx) => 0,

    // Get the next value of playOrderPos.
    // This is called at the end of each turn.
    // The phase ends if this returns undefined.
    next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.numPlayers,

    // OPTIONAL:
    // Override the initial value of playOrder.
    // This is called at the beginning of the game / phase.
    playOrder: (G, ctx) => [...],
  }
}

The playOrder piece.

delucis commented 4 years ago

There are a couple of approaches you could use I think. These all use the randomness API provided by boardgame.io to ensure predictable results across clients and server.

  1. Pick a random player to start, but maintain the usual play order. Kind of like playing clockwise in a boardgame but starting with a random player:

    turn: {
      order: {
        // Die returns values 1...n, but we need 0...n-1
        first: (G, ctx) => ctx.random.Die(ctx.numPlayers) - 1,
      },
    }
  2. Randomise the play order completely, so the game starts at the beginning of the play order, but the players have been randomly shuffled:

    turn: {
      order: {
        playOrder: (G, ctx) => {
          // generate an array of sequential player IDs ['0', '1', '2', ...]
          const defaultPlayOrder = Array(ctx.numPlayers).fill().map((_, i) => i + '');
          // return a shuffled version of the array
          return ctx.random.Shuffle(defaultPlayOrder);
        },
      },
    }

In these two cases, if you set turn.order inside a phase configuration its first or playOrder will be run each time the phase begins, so if you return to that phase, the order will be randomised again.

If you only want to randomise the starting player once during game setup, you can adapt these approaches using your setup function:

  1. Random first player set during game set-up:

    const game = {
      setup: ctx => {
        const startingPlayer = ctx.random.Die(ctx.numPlayers) - 1;
        return { startingPlayer }
      },
      turn: {
        order: {
          first: G => G.startingPlayer,
        },
      },
    }
  2. Random play order created during game set-up (using the TurnOrder.CUSTOM_FROM helper):

    const { TurnOrder } = require('boardgame.io/core');
    
    const game = {
      setup: ctx => {
        const defaultPlayOrder = Array(ctx.numPlayers).fill().map((_, i) => i + '');
        const playOrder = ctx.random.Shuffle(defaultPlayOrder);
        return { playOrder }
      },
      turn: {
        order: TurnOrder.CUSTOM_FROM('playOrder'),
      },
    }
ghost commented 4 years ago

Thanks for the quick response @delucis!

I think option 3 is best for my case as it is only ever a 2 player game, and the randomization only needs to occur once during setup.

Does this option work when generating state across clients and a remote server? @nicolodavis raised this as a possible issue on Gitter.

turn: {
  order: {
    // Get the initial value of playOrderPos.
    // This is called at the beginning of the phase.
    first: (G, ctx) => 0,
  }
}

https://boardgame.io/documentation/#/turn-order?id=creating-a-custom-turn-order

One question on the documentation. In the example above it says "at the beginning of the phase.". Can I take that to mean the starting player for each turn?

delucis commented 4 years ago

Does this option work when generating state across clients and a remote server?

Yes. If you were using Math.random() to generate randomness, it would produce different results on client and server and weirdness could follow. The ctx.random functions use a pseudo-random number generator that will produce the same result on the server and the client. There’s more detail about the randomness API in the docs.

In the example above it says "at the beginning of the phase.". Can I take that to mean the starting player for each turn?

This applies if you’re using Phases, which provide a way to have different game configurations during different periods of your game. For example, if you have a game that has two different turn orders in different parts of it. Every time a phase begins — i.e. we move to it from another phase — it will call first to get the first player in the phase.

If you’re not using phases, just a top-level turn configuration, first will be called once at the start of the game and then never again. After that, the next function is called each turn to see who the next player is. Most often that’s just incrementing through the playOrder step by step, but you can have whatever logic you want in next.

ghost commented 4 years ago

If you’re not using phases, just a top-level turn configuration, first will be called once at the start of the game and then never again.

I didn't realise that you could have a top level property of first in game.turn. Something like:

const game = {
  setup: ctx => {
    const startingPlayer = ctx.random.Die(ctx.numPlayers) - 1;
    return { startingPlayer }
  },
  turn: {
    first: G => G.startingPlayer,
  },
}
delucis commented 4 years ago

Ah, no, sorry, I meant a top-level turn in the game:

const game = {
  turn: {
    order: {
      first: G => G.startingPlayer,
    },
  },
}

As opposed to a turn configuration inside a phase:

const game = {
  phases: {
    'phase-name': {
      turn: {
        order: {
          first: G => G.startingPlayer,
        },
      },
    },
  },
}
ghost commented 4 years ago

Awesome! Thanks for all the help 👍