4. New game โ
A game is a means to change the state of a channel. Each games has a current state. This state evolves according to the rules defined by its model. A game is attached to a channel.
Structures of a game โ
Outcomes โ
The outcome of a game. Partially controlled by the apply_
lambda of the model and partially controlled by the gamePlatform.
t_outcome_content = sp.TVariant(
game_finished=sp.TString,
player_inactive=sp.TInt,
player_double_played=sp.TInt
)
t_outcome = sp.TOption(sp.TVariant(t_outcome_content
pending=t_outcome_content,
final=t_outcome_content,
))
If outcome
is none
or the outcome is pending, the game is still running. Otherwise, (i.e. the outcome is variant "final") the game ended with one of the following results:
Variant | Description | Value |
---|---|---|
game_finished | The game ended properly | (string) Set by the model apply_ lambda or by calling game_set_outcome . |
game_aborted | The game was aborted | (unit) Set by calling game_set_outcome . |
player_inactive | One player timed out and the other called starved. | (nat) Id of the player who timed out |
player_double_played | One player double played and the other called double_signed. | (nat) Id of the player who double-played |
Constants โ
The constants
structure contains everything that was agreed by the players at the beginning of the game.
t_constants = sp.TRecord(
model_id = sp.TBytes,
channel_id = sp.TBytes,
# Incremental player id starting at 1 associated to address
players_addr = sp.TMap(sp.TInt, sp.TAddress),
# A nonce that players gave to this game.
game_nonce = sp.TString,
# Number of seconds a player has to play on-chain
# after the other player called starving.
play_delay = sp.TNat,
settlements = t_settlements,
bonds = sp.TMap(sp.TInt, sp.TMap(sp.TInt, sp.TInt)),
)
constants = sp.record(
model_id = model_id,
channel_id = channel_id,
players_addr = {1:player1.address, 2:player2.address},
game_nonce = "game1",
play_delay = 3600 * 24,
settlements = settlements,
bonds = {1:{0:20}, 2:{0:20}}
)
See channel id calculation, model id calculation and settlements.
Transfers โ
A transfer describes a combination of tokens that is sent from a sender to a receiver.
You can use the player id 0
to set the platform as a sender.
In this case the game or the model must have special permissions from the platform admin to perform the transfer. :::
t_transfer = sp.TRecord(
# Player ID of the sender. Use 0 to send from the platform.
sender = sp.TInt,
# Player ID of the receiver.
receiver = sp.TInt,
# Bonds (token id: amount) that will be transferred.
bonds = sp.TMap(sp.TNat, sp.TNat),
)
transfer = sp.record(
sender = 1,
receiver = 2,
bonds = sp.map({0: 10})
)
Settlements โ
t_settlements = sp.TMap(t_outcome, sp.TList(t_transfer))
settlements = sp.map({
# (Outcome, Outcome Value) : # List of transfers
sp.variant("game_finished", "player_1_won"): [sp.record(sender = 2, receiver = 1, bonds = {0: 10})],
sp.variant("game_finished", "player_2_won"): [sp.record(sender = 1, receiver = 2, bonds = {0: 10})],
sp.variant("game_finished", "draw") : [],
sp.variant("player_inactive", 1): [sp.record(sender = 1, receiver = 2, bonds = {0: 15})],
sp.variant("player_inactive", 2): [sp.record(sender = 2, receiver = 1, bonds = {0: 15})],
sp.variant("player_double_played", 1): [sp.record(sender = 1, receiver = 2, bonds = {0: 20})],
sp.variant("player_double_played", 2): [sp.record(sender = 2, receiver = 1, bonds = {0: 20})],
sp.variant("game_aborted", sp.unit): [],
})
# In this example "player_1_won", "player_2_won" and "draw" are managed by the model: tictactoe
Settlements associates an outcome to a list of transfers.
Each outcome defined by the model and each standard outcome (player_inactive
, player_double_play
, game_aborted
) MUST appear in the settlement.
Keep player_double_played
> player_inactive
>
game_finished
. :::
Current โ
The current structure is managed by the game platform.
t_current = sp.TRecord(
move_nb=sp.TNat, # Incremental id of the move.
player=sp.TInt, # Id of the current player
outcome=t_outcome,
)
See outcome.
Game โ
The game structure is managed by the game platform.
t_game = sp.TRecord(
addr_players=sp.TMap(sp.TAddress, sp.TInt),
constants=t_constants,
current=t_current,
state=sp.TBytes,
settled=sp.TBool,
timeouts=sp.TMap(sp.TInt, sp.TTimestamp),
)
game_bonds
โ
game_bonds
defines what each player agreed to post as bond until the game is settled.
The index corresponds to the player id (1 for player 1...). The value is a map of tokens and amount.
For each player, the bonds corresponds to **the maximum
that a player could be asked to pay** by a settlement for each token. :::
Example:
settlements = sp.map({
# (Outcome, Outcome Value) : # List of transfers
sp.variant("game_finished", "player_1_won"): [sp.record(sender = 1, receiver = 2, bonds = {0: 10})],
sp.variant("player_inactive", 1): [sp.record(sender = 1, receiver = 2, bonds = {0: 15}),
sp.record(sender = 1, receiver = 2, bonds = {0: 5})],
sp.variant("player_double_played", 1): [sp.record(sender = 1, receiver = 2, bonds = {1: 42})],
})
bonds = {
1: {
0: 20, # For the outcome ("player_inactive", 1)
1: 42 # For the outcome ("player_double_played", 1)
},
2: {}
}
t_bonds = sp.TMap(sp.TInt, sp.TMap(sp.TInt, sp.TInt))
bonds = {1:{0:20}, 2:{0:20}}
Start a new game โ
Each player is responsible for verifying the other
players' bonds before signing a game (see game bonds). If this step is neglected, a player may not receive their settlements.
If the other player has a running withdraw_request
: make sure he has sufficient channel bonds to cover the withdraw + all the non-settled game bonds.
Otherwise, ask him to push new bonds or cancel the withdraw_request
. :::
All players need to sign a pack
of a pair of "New Game"
, <constants>
, <initParams>
.
<constants>
game constants agreed by the players. <initParams>
params for the init
lambda of the model.
The pair is of type string $constants bytes
.
In SmartPy this corresponds to a call to the action_new_game
method of game_platform.py
def action_new_game(constants, params):
constants = sp.set_type_expr(constants, types.t_constants)
params = sp.set_type_expr(params, sp.TBytes)
return sp.pack(("New Game", constants, params))
Compute the game structure off-chain โ
More info about offchain views and signatures can be found in
@sp.offchain_view
def offchain_new_game(self, params):
sp.set_type(params,
sp.TRecord(
constants=t_constants, # Constants of the game
params=sp.TBytes, # Init params for the `init` lambda of the model.
signatures=sp.TMap(sp.TKey, sp.Signature), # Signatures.
)
import smartpy as sp
class TestView(sp.Contract):
def __init__(self, c, f):
self.c = c
self.f = f.f
self.init(result = sp.none)
@sp.entrypoint
def compute(self, data, params):
self.c.data = data
b = sp.bind_block()
with b:
self.f(self.c, params)
self.data.result = sp.some(b.value)
def make_signatures(p1, p2, x):
# This function is illustrative.
# In real life examples, other player's signature are received from them
# You shouldn't know their secret_key'
sig1 = sp.make_signature(p1.secret_key, x)
sig2 = sp.make_signature(p2.secret_key, x)
return sp.map({p1.public_key: sig1, p2.public_key: sig2})
@sp.add_test(name="New Game")
def test():
sc = sp.test_scenario()
# ... (scenario + platform origination + constants creation)
def new_game_sigs(constants, params = sp.unit):
new_game = gp.action_new_game(constants, sp.pack(params))
return make_signatures(player1, player2, new_game)
offchain_new_game = TestView(platform, platform.offchain_new_game)
sc += offchain_new_game
offchain_new_game.compute(sp.record(
data = platform.data,
params = sp.record(
constants = constants,
params = sp.pack(sp.unit),
signatures = new_game_signatures
)
)).run(sender = player1)
game = sc.compute(offchain_new_game.data.result.open_some())
See constants, model class.
Push a new game on the platform โ
Pushing a new game on-chain is not necessary if there is no
conflict between players :::
import smartpy as sp
# ... Build constants, gather other's signature and sign the new_game structure
platform.new_game(
constants = constants,
params = sp.pack(sp.unit),
signatures = new_game_signatures
).run(sender = player2.address)
Compute the game_id
โ
The game id is the BLAKE2B
hash of the sp.pair(channel_id, sp.pair(model_id, game_nonce))
.
import smartpy as sp
gp = sp.io.import_template("state_channel_games/game_platform.py")
game_id = gp.compute_game_id(constants)
The constants
contains the id of the channel which depends
on the platform address. :::