Browse Source

examples: Tictactoe (#609)

vicyyn 4 years ago
parent
commit
541dbfbe37

+ 3 - 0
examples/tictactoe/Anchor.toml

@@ -0,0 +1,3 @@
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/tictactoe/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 12 - 0
examples/tictactoe/migrations/deploy.js

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+}

+ 19 - 0
examples/tictactoe/programs/tictactoe/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "tictactoe"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "tictactoe"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { path = "../../../../lang" }
+anchor-spl = { path = "../../../../spl" }

+ 2 - 0
examples/tictactoe/programs/tictactoe/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 216 - 0
examples/tictactoe/programs/tictactoe/src/lib.rs

@@ -0,0 +1,216 @@
+use anchor_lang::prelude::*;
+use std::str::FromStr;
+
+const BOARD_ITEM_FREE: u8 = 0; // Free slot
+const BOARD_ITEM_X: u8 = 1; // Player X
+const BOARD_ITEM_O: u8 = 2; // Player O
+
+/// Game State
+/// 0 - Waiting
+/// 1 - XMove
+/// 2 - OMove
+/// 3 - XWon
+/// 4 - OWon
+/// 5 - Draw
+
+#[program]
+pub mod tictactoe {
+    use super::*;
+
+    pub fn initialize_dashboard(ctx: Context<Initializedashboard>) -> ProgramResult {
+        let dashboard = &mut ctx.accounts.dashboard;
+        dashboard.game_count = 0;
+        dashboard.address = *dashboard.to_account_info().key;
+        Ok(())
+    }
+
+    pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
+        let dashboard = &mut ctx.accounts.dashboard;
+        let game = &mut ctx.accounts.game;
+        dashboard.game_count = dashboard.game_count + 1;
+        dashboard.latest_game = *game.to_account_info().key;
+        game.player_x = *ctx.accounts.player_x.key;
+        Ok(())
+    }
+
+    pub fn player_join(ctx: Context<Playerjoin>) -> ProgramResult {
+        let game = &mut ctx.accounts.game;
+        game.player_o = *ctx.accounts.player_o.key;
+        game.game_state = 1;
+        Ok(())
+    }
+
+    #[access_control(Playermove::accounts(&ctx, x_or_o, player_move))]
+    pub fn player_move(ctx: Context<Playermove>, x_or_o: u8, player_move: u8) -> ProgramResult {
+        let game = &mut ctx.accounts.game;
+        game.board[player_move as usize] = x_or_o;
+        game.status(x_or_o);
+        Ok(())
+    }
+
+    pub fn status(ctx: Context<Status>) -> ProgramResult {
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct Status<'info> {
+    dashboard: ProgramAccount<'info, Dashboard>,
+    game: ProgramAccount<'info, Game>,
+}
+
+#[derive(Accounts)]
+pub struct Initializedashboard<'info> {
+    #[account(init)]
+    dashboard: ProgramAccount<'info, Dashboard>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Initialize<'info> {
+    #[account(signer)]
+    player_x: AccountInfo<'info>,
+    #[account(mut)]
+    dashboard: ProgramAccount<'info, Dashboard>,
+    #[account(init)]
+    game: ProgramAccount<'info, Game>,
+}
+
+#[derive(Accounts)]
+pub struct Playerjoin<'info> {
+    #[account(signer)]
+    player_o: AccountInfo<'info>,
+    #[account(mut, constraint = game.game_state != 0 && game.player_x != Pubkey::default())]
+    game: ProgramAccount<'info, Game>,
+}
+
+#[derive(Accounts)]
+pub struct Playermove<'info> {
+    #[account(signer)]
+    player: AccountInfo<'info>,
+    #[account(mut)]
+    game: ProgramAccount<'info, Game>,
+}
+
+impl<'info> Playermove<'info> {
+    pub fn accounts(ctx: &Context<Playermove>, x_or_o: u8, player_move: u8) -> Result<()> {
+        if ctx.accounts.game.board[player_move as usize] != 0 {
+            return Err(ErrorCode::Illegalmove.into());
+        }
+        if x_or_o == BOARD_ITEM_X {
+            return Playermove::player_x_checks(ctx);
+        } else if x_or_o == BOARD_ITEM_O {
+            return Playermove::player_o_checks(ctx);
+        } else {
+            return Err(ErrorCode::UnexpectedValue.into());
+        }
+    }
+
+    pub fn player_x_checks(ctx: &Context<Playermove>) -> Result<()> {
+        if ctx.accounts.game.player_x != *ctx.accounts.player.key {
+            return Err(ErrorCode::Unauthorized.into());
+        }
+        if ctx.accounts.game.game_state != 1 {
+            return Err(ErrorCode::Gamestate.into());
+        }
+        Ok(())
+    }
+
+    pub fn player_o_checks(ctx: &Context<Playermove>) -> Result<()> {
+        if ctx.accounts.game.player_o != *ctx.accounts.player.key {
+            return Err(ErrorCode::Unauthorized.into());
+        }
+        if ctx.accounts.game.game_state != 2 {
+            return Err(ErrorCode::Gamestate.into());
+        }
+        Ok(())
+    }
+}
+
+#[account]
+pub struct Dashboard {
+    game_count: u64,
+    latest_game: Pubkey,
+    address: Pubkey,
+}
+
+#[account]
+#[derive(Default)]
+pub struct Game {
+    keep_alive: [u64; 2],
+    player_x: Pubkey,
+    player_o: Pubkey,
+    game_state: u8,
+    board: [u8; 9],
+}
+
+#[event]
+pub struct GameStatus {
+    keep_alive: [u64; 2],
+    player_x: Pubkey,
+    player_o: Pubkey,
+    game_state: u8,
+    board: [u8; 9],
+}
+
+impl From<GameStatus> for Game {
+    fn from(status: GameStatus) -> Self {
+        Self {
+            keep_alive: status.keep_alive,
+            player_x: status.player_x,
+            player_o: status.player_o,
+            game_state: status.game_state,
+            board: status.board,
+        }
+    }
+}
+
+impl Game {
+    pub fn status(self: &mut Game, x_or_o: u8) {
+        let winner =
+            // Check rows.
+            Game::same(x_or_o, &self.board[0..3])
+            || Game::same(x_or_o, &self.board[3..6])
+            || Game::same(x_or_o, &self.board[6..9])
+            // Check columns.
+            || Game::same(x_or_o, &[self.board[0], self.board[3], self.board[6]])
+            || Game::same(x_or_o, &[self.board[1], self.board[4], self.board[7]])
+            || Game::same(x_or_o, &[self.board[2], self.board[5], self.board[8]])
+            // Check both diagonals.
+            || Game::same(x_or_o, &[self.board[0], self.board[4], self.board[8]])
+            || Game::same(x_or_o, &[self.board[2], self.board[4], self.board[6]]);
+
+        if winner {
+            self.game_state = x_or_o + 2;
+        } else if self.board.iter().all(|&p| p != BOARD_ITEM_FREE) {
+            self.game_state = 5;
+        } else {
+            if x_or_o == BOARD_ITEM_X {
+                self.game_state = 2;
+            } else {
+                self.game_state = 1;
+            }
+        }
+    }
+
+    pub fn same(x_or_o: u8, triple: &[u8]) -> bool {
+        triple.iter().all(|&i| i == x_or_o)
+    }
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("You are not authorized to perform this action.")]
+    Unauthorized,
+    #[msg("Wrong dashboard")]
+    Wrongdashboard,
+    #[msg("Wrong expected state")]
+    Gamestate,
+    #[msg("Dashboard already initialized")]
+    Initialized,
+    #[msg("Unexpected value")]
+    UnexpectedValue,
+    #[msg("Illegal move")]
+    Illegalmove,
+}

+ 157 - 0
examples/tictactoe/tests/tictactoe.js

@@ -0,0 +1,157 @@
+const anchor = require('@project-serum/anchor');
+
+describe('tictactoe', () => {
+
+  anchor.setProvider(anchor.Provider.env());
+  const program = anchor.workspace.Tictactoe;
+  let dashboard = anchor.web3.Keypair.generate()
+  let game = anchor.web3.Keypair.generate()
+  let player_o = anchor.web3.Keypair.generate()
+
+  it('Initialize Dashboard', async () => {
+    const tx = await program.rpc.initializeDashboard({
+      accounts: {
+        authority: program.provider.wallet.publicKey,
+        dashboard: dashboard.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [dashboard],
+      instructions: [await program.account.dashboard.createInstruction(dashboard)]
+    })
+
+    console.log("transaction: ", tx)
+  });
+
+  it('Initialize Game', async () => {
+    const tx = await program.rpc.initialize({
+      accounts: {
+        playerX: program.provider.wallet.publicKey,
+        dashboard: dashboard.publicKey,
+        game: game.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [game],
+      instructions: [await program.account.game.createInstruction(game)]
+    })
+
+    console.log("transaction: ", tx)
+  });
+
+  it('Player O joins', async () => {
+    const tx = await program.rpc.playerJoin({
+      accounts: {
+        playerO: player_o.publicKey,
+        game: game.publicKey,
+      },
+      signers: [player_o],
+    })
+
+    console.log("transaction: ", tx)
+  });
+
+  it('Player x plays', async () => {
+    const tx = await program.rpc.playerMove(1, 0, {
+      accounts: {
+        player: program.provider.wallet.publicKey,
+        game: game.publicKey,
+      },
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player o plays', async () => {
+    const tx = await program.rpc.playerMove(2, 1, {
+      accounts: {
+        player: player_o.publicKey,
+        game: game.publicKey,
+      },
+      signers: [player_o]
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player x plays', async () => {
+    const tx = await program.rpc.playerMove(1, 3, {
+      accounts: {
+        player: program.provider.wallet.publicKey,
+        game: game.publicKey,
+      },
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player o plays', async () => {
+    const tx = await program.rpc.playerMove(2, 6, {
+      accounts: {
+        player: player_o.publicKey,
+        game: game.publicKey,
+      },
+      signers: [player_o]
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player x plays', async () => {
+    const tx = await program.rpc.playerMove(1, 2, {
+      accounts: {
+        player: program.provider.wallet.publicKey,
+        game: game.publicKey,
+      },
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player o plays', async () => {
+    const tx = await program.rpc.playerMove(2, 4, {
+      accounts: {
+        player: player_o.publicKey,
+        game: game.publicKey,
+      },
+      signers: [player_o]
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player x plays', async () => {
+    const tx = await program.rpc.playerMove(1, 5, {
+      accounts: {
+        player: program.provider.wallet.publicKey,
+        game: game.publicKey,
+      },
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player o plays', async () => {
+    const tx = await program.rpc.playerMove(2, 8, {
+      accounts: {
+        player: player_o.publicKey,
+        game: game.publicKey,
+      },
+      signers: [player_o]
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Player x plays', async () => {
+    const tx = await program.rpc.playerMove(1, 7, {
+      accounts: {
+        player: program.provider.wallet.publicKey,
+        game: game.publicKey,
+      },
+    })
+    console.log("transaction: ", tx)
+  });
+
+  it('Status', async () => {
+    const tx = await program.rpc.status({
+      accounts: {
+        dashboard: dashboard.publicKey,
+        game: game.publicKey,
+      },
+    })
+
+    console.log("transaction: ", tx)
+  });
+
+});