diff --git a/examples/tictactoe/Anchor.toml b/examples/tictactoe/Anchor.toml new file mode 100644 index 000000000..b9705f038 --- /dev/null +++ b/examples/tictactoe/Anchor.toml @@ -0,0 +1,3 @@ +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" diff --git a/examples/tictactoe/Cargo.toml b/examples/tictactoe/Cargo.toml new file mode 100644 index 000000000..a60de986d --- /dev/null +++ b/examples/tictactoe/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/tictactoe/migrations/deploy.js b/examples/tictactoe/migrations/deploy.js new file mode 100644 index 000000000..325cf3d0e --- /dev/null +++ b/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. +} diff --git a/examples/tictactoe/programs/tictactoe/Cargo.toml b/examples/tictactoe/programs/tictactoe/Cargo.toml new file mode 100644 index 000000000..02a017a85 --- /dev/null +++ b/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" } diff --git a/examples/tictactoe/programs/tictactoe/Xargo.toml b/examples/tictactoe/programs/tictactoe/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/examples/tictactoe/programs/tictactoe/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/tictactoe/programs/tictactoe/src/lib.rs b/examples/tictactoe/programs/tictactoe/src/lib.rs new file mode 100644 index 000000000..b60b0994e --- /dev/null +++ b/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) -> ProgramResult { + let dashboard = &mut ctx.accounts.dashboard; + dashboard.game_count = 0; + dashboard.address = *dashboard.to_account_info().key; + Ok(()) + } + + pub fn initialize(ctx: Context) -> 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) -> 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, 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) -> 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, 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) -> 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) -> 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 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, +} diff --git a/examples/tictactoe/tests/tictactoe.js b/examples/tictactoe/tests/tictactoe.js new file mode 100644 index 000000000..3429fbe82 --- /dev/null +++ b/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) + }); + +});