Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Examples/Front-end] Tic-tac-toe #18526

Merged
merged 24 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/tic-tac-toe/cli/testnet.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PKG=0x1812951b018fcd9f2262d160acdaff5b432011c58fae80eb981c7be3370167da
CAP=0x71a69d9d7319c0c86e0a5266746f85481840064e19fdb491ce83843851f5fe9d
9 changes: 9 additions & 0 deletions examples/tic-tac-toe/move/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "tic_tac_toe"
edition = "2024.beta"

[dependencies]
Sui = { local = "../../../crates/sui-framework/packages/sui-framework" }

[addresses]
tic_tac_toe = "0x0"
321 changes: 321 additions & 0 deletions examples/tic-tac-toe/move/sources/owned.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// An implementation of Tic Tac Toe, using owned objects.
///
/// The `Game` object is owned by an admin, so players cannot mutate the game
/// board directly. Instead, they convey their intention to place a mark by
/// transferring a `Mark` object to the `Game`.
///
/// This means that every move takes two owned object fast path operations --
/// one by the player, and one by the admin. The admin could be a third party
/// running a centralized service that monitors marker placement events and
/// responds to them, or it could be a 1-of-2 multisig address shared between
/// the two players, as demonstrated in the demo app.
///
/// The `shared` module shows a variant of this game implemented using shared
/// objects, which provides different trade-offs: Using shared objects is more
/// expensive, however the implementation is more straightforward and each move
/// only requires one transaction.
module tic_tac_toe::owned {
use sui::event;
use sui::transfer::Receiving;

// === Object Types ===

/// The state of an active game of tic-tac-toe.
public struct Game has key, store {
id: UID,
/// Marks on the board.
board: vector<u8>,
/// The next turn to be played.
turn: u8,
/// The address expected to send moves on behalf of X.
x: address,
/// The address expected to send moves on behalf of O.
o: address,
/// Public key of the admin address.
admin: vector<u8>,
}

/// The player that the next turn is expected from is given a `TurnCap`.
public struct TurnCap has key {
id: UID,
game: ID,
}

/// A request to make a play -- only the player with the `TurnCap` can
/// create and send `Mark`s.
public struct Mark has key, store {
id: UID,
player: address,
row: u8,
col: u8,
}

/// An NFT representing a finished game. Sent to the winning player if there
/// is one, or to both players in the case of a draw.
public struct Trophy has key {
id: UID,
/// Whether the game was won or drawn.
status: u8,
/// The state of the board at the end of the game.
board: vector<u8>,
/// The number of turns played
turn: u8,
/// The other player (relative to the player who owns this Trophy).
other: address,
}

// === Event Types ===

public struct MarkSent has copy, drop {
game: ID,
mark: ID,
}

public struct GameEnd has copy, drop {
game: ID,
}

// === Constants ===

// Marks
const MARK__: u8 = 0;
const MARK_X: u8 = 1;
const MARK_O: u8 = 2;

// Trophy status
const TROPHY_NONE: u8 = 0;
const TROPHY_DRAW: u8 = 1;
const TROPHY_WIN: u8 = 2;

// === Errors ===

#[error]
const EInvalidLocation: vector<u8> =
b"Move was for a position that doesn't exist on the board";

#[error]
const EWrongPlayer: vector<u8> =
b"Game expected a move from another player";

#[error]
const ENotFinished: vector<u8> =
b"Game has not reached an end condition";

#[error]
const EAlreadyFinished: vector<u8> =
b"Can't place a mark on a finished game";

#[error]
const EInvalidEndState: vector<u8> =
b"Game reached an end state that wasn't expected";

// === Public Functions ===

/// Create a new game, played by `x` and `o`. The game should be
/// transfered to the address that will administrate the game. If
/// that address is a multi-sig of the two players, its public key
/// should be passed as `admin`.
public fun new(x: address, o: address, admin: vector<u8>, ctx: &mut TxContext): Game {
let game = Game {
id: object::new(ctx),
board: vector[
MARK__, MARK__, MARK__,
MARK__, MARK__, MARK__,
MARK__, MARK__, MARK__,
],
turn: 0,
x,
o,
admin,
};

let turn = TurnCap {
id: object::new(ctx),
game: object::id(&game),
};

// X is the first player, so send the capability to them.
transfer::transfer(turn, x);
game
}

/// Called by the active player to express their intention to make a move.
/// This consumes the `TurnCap` to prevent a player from making more than
/// one move on their turn.
public fun send_mark(cap: TurnCap, row: u8, col: u8, ctx: &mut TxContext) {
assert!(row < 3 && col < 3, EInvalidLocation);

let TurnCap { id, game } = cap;
id.delete();

let mark = Mark {
id: object::new(ctx),
player: ctx.sender(),
row,
col,
};

event::emit(MarkSent { game, mark: object::id(&mark) });
transfer::transfer(mark, game.to_address());
}

/// Called by the admin (who owns the `Game`), to commit a player's
/// intention to make a move. If the game should end, `Trophy`s are sent to
/// the appropriate players, if the game should continue, a new `TurnCap` is
/// sent to the player who should make the next move.
public fun place_mark(
game: &mut Game,
mark: Receiving<Mark>,
ctx: &mut TxContext,
) {
assert!(game.ended() == TROPHY_NONE, EAlreadyFinished);

// Fetch the mark on behalf of the game -- only works if the mark in
// question was sent to this game.
let Mark { id, row, col, player } = transfer::receive(&mut game.id, mark);
id.delete();

// Confirm that the mark is from the player we expect -- it should not
// be possible to hit this assertion, because the `Mark`s can only be
// created by the address that owns the `TurnCap` which cannot be
// transferred, and is always held by `game.next_player()`.
let (me, them, sentinel) = game.next_player();
assert!(me == player, EWrongPlayer);

if (game[row, col] == MARK__) {
*(&mut game[row, col]) = sentinel;
game.turn = game.turn + 1;
};

// Check win condition -- if there is a winner, send them the trophy,
// otherwise, create a new turn cap and send that to the next player.
let end = game.ended();
if (end == TROPHY_WIN) {
transfer::transfer(game.mint_trophy(end, them, ctx), me);
event::emit(GameEnd { game: object::id(game) });
} else if (end == TROPHY_DRAW) {
transfer::transfer(game.mint_trophy(end, them, ctx), me);
transfer::transfer(game.mint_trophy(end, me, ctx), them);
event::emit(GameEnd { game: object::id(game) });
} else if (end == TROPHY_NONE) {
let cap = TurnCap { id: object::new(ctx), game: object::id(game) };
let (to, _, _) = game.next_player();
transfer::transfer(cap, to);
} else {
abort EInvalidEndState
}
}

public fun burn(game: Game) {
assert!(game.ended() != TROPHY_NONE, ENotFinished);
let Game { id, .. } = game;
id.delete();
}

/// Test whether the game has reached an end condition or not.
public fun ended(game: &Game): u8 {
if (
// Test rows
test_triple(game, 0, 1, 2) ||
test_triple(game, 3, 4, 5) ||
test_triple(game, 6, 7, 8) ||
// Test columns
test_triple(game, 0, 3, 6) ||
test_triple(game, 1, 4, 7) ||
test_triple(game, 2, 5, 8) ||
// Test diagonals
test_triple(game, 0, 4, 8) ||
test_triple(game, 2, 4, 6)
) {
TROPHY_WIN
} else if (game.turn == 9) {
TROPHY_DRAW
} else {
TROPHY_NONE
}
}

#[syntax(index)]
public fun mark(game: &Game, row: u8, col: u8): &u8 {
&game.board[(row * 3 + col) as u64]
}

#[syntax(index)]
fun mark_mut(game: &mut Game, row: u8, col: u8): &mut u8 {
&mut game.board[(row * 3 + col) as u64]
}

// === Private Helpers ===

/// Address of the player the move is expected from, the address of the
/// other player, and the mark to use for the upcoming move.
fun next_player(game: &Game): (address, address, u8) {
if (game.turn % 2 == 0) {
(game.x, game.o, MARK_X)
} else {
(game.o, game.x, MARK_O)
}
}

/// Test whether the values at the triple of positions all match each other
/// (and are not all EMPTY).
fun test_triple(game: &Game, x: u8, y: u8, z: u8): bool {
let x = game.board[x as u64];
let y = game.board[y as u64];
let z = game.board[z as u64];

MARK__ != x && x == y && y == z
}

/// Create a trophy from the current state of the `game`, that indicates
/// that a player won or drew against `other` player.
fun mint_trophy(
game: &Game,
status: u8,
other: address,
ctx: &mut TxContext,
): Trophy {
Trophy {
id: object::new(ctx),
status,
board: game.board,
turn: game.turn,
other,
}
}

// === Test Helpers ===
#[test_only] public use fun game_board as Game.board;
#[test_only] public use fun trophy_status as Trophy.status;
#[test_only] public use fun trophy_board as Trophy.board;
#[test_only] public use fun trophy_turn as Trophy.turn;
#[test_only] public use fun trophy_other as Trophy.other;

#[test_only]
public fun game_board(game: &Game): vector<u8> {
game.board
}

#[test_only]
public fun trophy_status(trophy: &Trophy): u8 {
trophy.status
}

#[test_only]
public fun trophy_board(trophy: &Trophy): vector<u8> {
trophy.board
}

#[test_only]
public fun trophy_turn(trophy: &Trophy): u8 {
trophy.turn
}

#[test_only]
public fun trophy_other(trophy: &Trophy): address {
trophy.other
}
}
Loading
Loading