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.
@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
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 โ
@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),
)
)
# ... (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
equalsonchain_current.move_nbย +ย 1
- The signature corresponds to the current player's signature.
new_current
andnew_state
equals to what theapply_
returns when called with themove_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
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.
@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),
)
)
# ... (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)