Skip to content
On this page

5. Play a game โ€‹

A move within a game is an updated new game play signed by both players.

If everything goes well, both players sign each play action .

Compute new_current and new_state โ€‹

INFO

More info about offchain views and signatures can be found in offchain views and signatures

To compute the new_current and new_state you can use the offchain view.
It's both useful to compute your moves and to verify other's.

python
@sp.offchain_view
def offchain_compute_game_play(self, game, move_data):
    sp.set_type(game, t_game)
    sp.set_type(move_data, sp.TBytes)

Sign a play action โ€‹

Each player needs to sign a pack of a pair of "Play", <game_id>, <new_current>, <new_state>, <move_data>.

<game_id> the game id (see compute_game_id).
<new_current> the current returned by the offchain view offchain_play
<new_state> will be passed to the apply_ lambda of the model referenced in the constants.
<move_data> bytes representing the player move, passed to the apply_ lambda of the model.

The pair is of type string bytes $current bytesย bytes.

In SmartPy it corresponds to a call to the action_play method of game_platform.py

python
def action_play(game_id, new_current, new_state, move_data):
    game_id     = sp.set_type_expr(game_id, sp.TBytes)
    new_current = sp.set_type_expr(new_current, types.t_current)
    new_state   = sp.set_type_expr(new_state, sp.TBytes)
    move_data   = sp.set_type_expr(move_data, sp.TBytes)
    return sp.pack(("Play", game_id, new_current, new_state, move_data))

Push the last state onchain โ€‹

python
@sp.entrypoint
def game_play(self, params):
    sp.set_type(
        params,
        sp.TRecord(
            game_id=sp.TBytes,
            new_current=t_current,  # Returned by offchain_play
            new_state=sp.TBytes,   # Returned by offchain_play
            move_data=sp.TBytes,  # Data that describes the move
            # Public key of each player associated with the signature
            # of the state
            signatures=sp.TMap(sp.TKey, sp.TSignature),
        )
    )
python
# ... (new game scenario)

def action_play_sigs(game_id, game, move_data):
    action_play = gp.action_play(game_id, game.current, game.state, move_data)
    signatures = make_signatures(player1, player2, action_play)
    return (game, action_play, move_data, signatures)

offchain_compute_game_play = TestView(platform, platform.offchain_compute_game_play)
sc += offchain_compute_game_play
data = {}
move_nb = 0

sc.h3("Move 0")
move_data = sp.pack(sp.record(i = 1, j = 1))
offchain_compute_game_play.compute(sp.record(
    data = platform.data,
    params = sp.record(
        game      = game,
        move_data = move_data,
    )
)).run(sender = player1)

data[move_nb] = action_play_sigs(game_id, offchain_compute_game_play.data.result.open_some())
move_nb += 1

# ... (some other moves)

platform.game_play(
    game_id     = game_id,
    new_current = data[move_nb-1][0].current,
    new_state   = data[move_nb-1][0].state,
    move_data   = data[move_nb-1][2]
    signatures  = data[move_nb-1][3]
).run(sender = player1)

See game id, offchain_play and signature of the state.

INFO

Nothing prevents players from signing a state that doesn't correspond to what the apply_ lambda would have done.

This means that if and only if everyone agrees the rules of the game can be violated.

Playing without other signature โ€‹

If the other player indicated that you are not playing (see non playing opponent) or if the other player doesn't want to sign your move, you can push the last state agreed onchain and call the entrypoint game_play with only one signature: yours.

The conditions to validate a play with only one signature are :

  • The new_current.move_nb equals onchain_current.move_nbย +ย 1
  • The signature corresponds to the current player's signature.
  • new_current and new_state equals to what the apply_ returns when called with the move_data

apply_ returns an outcome โ€‹

If apply_ returns an outcome after one of the player played without outcome, the outcome is marked pending. The other player is now asked to call the entrypoint with the same move_action but signed with it's own signature otherwise he will loose a starved dispute.

Agree on the outcome โ€‹

At any time a player can send a request to end the game with a defined outcome as if it were returned by the apply_ lambda or abort it.

For example if the game cannot be finished players can agree to abort it.

In SmartPy it corresponds to a call to the action_new_outcome method of game_platform.py

python
def action_new_outcome(game_id, new_outcome, timeout):
    game_id     = sp.set_type_expr(game_id, sp.TBytes)
    new_outcome = sp.set_type_expr(new_outcome, sp.TVariant(
        game_finished = sp.TString,
        game_aborted  = sp.TUnit
    ))
    timeout     = sp.set_type_expr(now, sp.TTimestamp)
    return sp.pack(("New Outcome", game_id, new_outcome, timeout))

It can be pushed on-chain with signatures by calling game_set_outcome.

The new_outcome action expires depending on the timestamp it held.

python
@sp.entrypoint
def game_set_outcome(self, params):
    sp.set_type(
        params,
        sp.TRecord(
            game_id=sp.TBytes,
            outcome=sp.TVariant(
                game_finished=sp.TString, game_aborted=sp.TUnit
            ),
            # Timeout after which the proposal expires.
            # It should be a very short delay.
            timeout=sp.TTimestamp,
            # Public key of each player associated with the signature of the
            # `action_new_outcome`.
            signatures=sp.TMap(sp.TKey, sp.TSignature),
        )
    )
python
# ... (playing game scenario)

def action_outcome_sig(game_id, outcome, timeout):
    action_new_outcome = gp.action_new_outcome(game_id, outcome, timeout)
    signatures = sp.make_signature(player1, player2, action_new_outcome)
    return (action_new_outcome, signatures)

import time
timeout = sp.timestamp(time.time()).add_minutes(2)
signatures = action_outcome_sigs(game_id, "draw", timeout)
platform.game_set_state(
    game_id    = game_id,
    outcome    = sp.variant(game_finished, "draw"),
    signatures = signatures
).run(sender = player1)

Abort the game โ€‹

See agree on the outcome