Skip to content
On this page

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.

python
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:

VariantDescriptionValue
game_finishedThe game ended properly(string) Set by the model apply_ lambda or by calling game_set_outcome.
game_abortedThe game was aborted(unit) Set by calling game_set_outcome.
player_inactiveOne player timed out and the other called starved.(nat) Id of the player who timed out
player_double_playedOne 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.

python
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)),
)
python
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. :::

python
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),
)
python
transfer = sp.record(
    sender = 1,
    receiver = 2,
    bonds = sp.map({0: 10})
)

Settlements โ€‹

python
t_settlements = sp.TMap(t_outcome, sp.TList(t_transfer))
python
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.

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

python
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:

python
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: {}
}
python
t_bonds = sp.TMap(sp.TInt, sp.TMap(sp.TInt, sp.TInt))
python
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

python
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

offchain views and signatures

python
@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.
    )
python
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 :::

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

python
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. :::