diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/errors.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/errors.rs new file mode 100644 index 0000000..7124e58 --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/errors.rs @@ -0,0 +1,10 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum TicTacToeError { + TileOutOfBounds, + TileAlreadySet, + GameAlreadyOver, + NotPlayersTurn, + GameAlreadyStarted, +} diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/mod.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/mod.rs new file mode 100644 index 0000000..83c013b --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/mod.rs @@ -0,0 +1,5 @@ +pub use play::*; +pub use setup_game::*; + +pub mod play; +pub mod setup_game; diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/play.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/play.rs new file mode 100644 index 0000000..0fd8342 --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/play.rs @@ -0,0 +1,21 @@ +use crate::errors::TicTacToeError; +use crate::state::game::*; +use anchor_lang::prelude::*; + +pub fn play(ctx: Context, tile: Tile) -> Result<()> { + let game = &mut ctx.accounts.game; + + require!( + game.current_player() == ctx.accounts.player.key(), + TicTacToeError::NotPlayersTurn + ); + + game.play(&tile) +} + +#[derive(Accounts)] +pub struct Play<'info> { + #[account(mut)] + pub game: Account<'info, Game>, + pub player: Signer<'info>, +} diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/setup_game.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/setup_game.rs new file mode 100644 index 0000000..e001f58 --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/instructions/setup_game.rs @@ -0,0 +1,17 @@ +use crate::state::game::*; +use anchor_lang::prelude::*; + +pub fn setup_game(ctx: Context, player_two: Pubkey) -> Result<()> { + let game = &mut ctx.accounts.game; + game.set_players([ctx.accounts.player_one.key(), player_two]); + game.start() +} + +#[derive(Accounts)] +pub struct SetupGame<'info> { + #[account(init, payer = player_one, space = Game::MAXIMUM_SIZE + 8)] + pub game: Account<'info, Game>, + #[account(mut)] + pub player_one: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/lib.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/lib.rs index 5b1f6ef..f7353f5 100644 --- a/programs/tic-tac-toe/programs/tic-tac-toe/src/lib.rs +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/lib.rs @@ -1,8 +1,10 @@ -use num_derive::*; -use num_traits::*; -use std::mem; - use anchor_lang::prelude::*; +use instructions::*; +use state::game::Tile; + +pub mod errors; +pub mod instructions; +pub mod state; // this key needs to be changed to whatever public key is returned by "anchor keys list" declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -12,174 +14,10 @@ pub mod tic_tac_toe { use super::*; pub fn setup_game(ctx: Context, player_two: Pubkey) -> Result<()> { - let game = &mut ctx.accounts.game; - game.players = [ctx.accounts.player_one.key(), player_two]; - game.turn = 1; - Ok(()) + instructions::setup_game::setup_game(ctx, player_two) } pub fn play(ctx: Context, tile: Tile) -> Result<()> { - let game = &mut ctx.accounts.game; - - require!( - game.current_player() == ctx.accounts.player.key(), - TicTacToeError::NotPlayersTurn - ); - - game.play(&tile) + instructions::play::play(ctx, tile) } } - -#[error_code] -pub enum TicTacToeError { - TileOutOfBounds, - TileAlreadySet, - GameAlreadyOver, - NotPlayersTurn, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct Tile { - row: u8, - column: u8, -} - -#[account] -#[derive(Default)] -pub struct Game { - players: [Pubkey; 2], // 64 - turn: u8, // 1 - board: [[Option; 3]; 3], // 9 * (1 + 1) = 18 - state: GameState, // 32 + 1 -} - -impl Game { - const MAXIMUM_SIZE: usize = mem::size_of::() + 9; - - pub fn is_active(&self) -> bool { - self.state == GameState::Active - } - - fn current_player_index(&self) -> usize { - ((self.turn - 1) % 2) as usize - } - - pub fn current_player(&self) -> Pubkey { - self.players[self.current_player_index()] - } - - pub fn play(&mut self, tile: &Tile) -> Result<()> { - if !self.is_active() { - return Err(TicTacToeError::GameAlreadyOver.into()); - } - match tile { - tile @ Tile { - row: 0..=2, - column: 0..=2, - } => match self.board[tile.row as usize][tile.column as usize] { - Some(_) => return Err(TicTacToeError::TileAlreadySet.into()), - None => { - self.board[tile.row as usize][tile.column as usize] = - Some(Sign::from_usize(self.current_player_index()).unwrap()); - } - }, - _ => return Err(TicTacToeError::TileOutOfBounds.into()), - } - - self.update_state(); - - if GameState::Active == self.state { - self.turn += 1; - } - - Ok(()) - } - - fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool { - let [first, second, third] = trio; - self.board[first.0][first.1].is_some() - && self.board[first.0][first.1] == self.board[second.0][second.1] - && self.board[first.0][first.1] == self.board[third.0][third.1] - } - - fn update_state(&mut self) { - for i in 0..=2 { - // three of the same in one row - if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) { - self.state = GameState::Won { - winner: self.current_player(), - }; - return; - } - // three of the same in one column - if self.is_winning_trio([(0, i), (1, i), (2, i)]) { - self.state = GameState::Won { - winner: self.current_player(), - }; - return; - } - } - - // three of the same in one diagonal - if self.is_winning_trio([(0, 0), (1, 1), (2, 2)]) - || self.is_winning_trio([(0, 2), (1, 1), (2, 0)]) - { - self.state = GameState::Won { - winner: self.current_player(), - }; - return; - } - - // reaching this code means the game has not been won, - // so if there are unfilled tiles left, it's still active - for row in 0..=2 { - for column in 0..=2 { - if self.board[row][column].is_none() { - return; - } - } - } - - // game has not been won - // game has no more free tiles - // -> game ends in a tie - self.state = GameState::Tie; - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] -pub enum GameState { - Active, - Tie, - Won { winner: Pubkey }, -} - -impl Default for GameState { - fn default() -> Self { - Self::Active - } -} - -#[derive( - AnchorSerialize, AnchorDeserialize, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, -)] -pub enum Sign { - X, - O, -} - -#[derive(Accounts)] -pub struct SetupGame<'info> { - #[account(init, payer = player_one, space = Game::MAXIMUM_SIZE + 8)] - pub game: Account<'info, Game>, - #[account(mut)] - pub player_one: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct Play<'info> { - #[account(mut)] - pub game: Account<'info, Game>, - pub player: Signer<'info>, -} diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/state/game.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/state/game.rs new file mode 100644 index 0000000..0c60b3d --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/state/game.rs @@ -0,0 +1,147 @@ +use crate::errors::TicTacToeError; +use anchor_lang::prelude::*; +use num_derive::*; +use num_traits::*; +use std::mem; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct Tile { + row: u8, + column: u8, +} + +#[account] +#[derive(Default)] +pub struct Game { + players: [Pubkey; 2], // 64 + turn: u8, // 1 + board: [[Option; 3]; 3], // 9 * (1 + 1) = 18 + state: GameState, // 32 + 1 +} + +impl Game { + pub const MAXIMUM_SIZE: usize = mem::size_of::() + 9; + + pub fn start(&mut self) -> Result<()> { + if self.turn == 0 { + self.turn = 1; + Ok(()) + } else { + err!(TicTacToeError::GameAlreadyStarted) + } + } + + pub fn set_players(&mut self, players: [Pubkey; 2]) { + self.players = players; + } + + pub fn is_active(&self) -> bool { + self.state == GameState::Active + } + + fn current_player_index(&self) -> usize { + ((self.turn - 1) % 2) as usize + } + + pub fn current_player(&self) -> Pubkey { + self.players[self.current_player_index()] + } + + pub fn play(&mut self, tile: &Tile) -> Result<()> { + require!(self.is_active(), TicTacToeError::GameAlreadyOver); + + match tile { + tile @ Tile { + row: 0..=2, + column: 0..=2, + } => match self.board[tile.row as usize][tile.column as usize] { + Some(_) => return Err(TicTacToeError::TileAlreadySet.into()), + None => { + self.board[tile.row as usize][tile.column as usize] = + Some(Sign::from_usize(self.current_player_index()).unwrap()); + } + }, + _ => return Err(TicTacToeError::TileOutOfBounds.into()), + } + + self.update_state(); + + if GameState::Active == self.state { + self.turn += 1; + } + + Ok(()) + } + + fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool { + let [first, second, third] = trio; + self.board[first.0][first.1].is_some() + && self.board[first.0][first.1] == self.board[second.0][second.1] + && self.board[first.0][first.1] == self.board[third.0][third.1] + } + + fn update_state(&mut self) { + for i in 0..=2 { + // three of the same in one row + if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) { + self.state = GameState::Won { + winner: self.current_player(), + }; + return; + } + // three of the same in one column + if self.is_winning_trio([(0, i), (1, i), (2, i)]) { + self.state = GameState::Won { + winner: self.current_player(), + }; + return; + } + } + + // three of the same in one diagonal + if self.is_winning_trio([(0, 0), (1, 1), (2, 2)]) + || self.is_winning_trio([(0, 2), (1, 1), (2, 0)]) + { + self.state = GameState::Won { + winner: self.current_player(), + }; + return; + } + + // reaching this code means the game has not been won, + // so if there are unfilled tiles left, it's still active + for row in 0..=2 { + for column in 0..=2 { + if self.board[row][column].is_none() { + return; + } + } + } + + // game has not been won + // game has no more free tiles + // -> game ends in a tie + self.state = GameState::Tie; + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] +pub enum GameState { + Active, + Tie, + Won { winner: Pubkey }, +} + +impl Default for GameState { + fn default() -> Self { + Self::Active + } +} + +#[derive( + AnchorSerialize, AnchorDeserialize, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, +)] +pub enum Sign { + X, + O, +} diff --git a/programs/tic-tac-toe/programs/tic-tac-toe/src/state/mod.rs b/programs/tic-tac-toe/programs/tic-tac-toe/src/state/mod.rs new file mode 100644 index 0000000..f9da73e --- /dev/null +++ b/programs/tic-tac-toe/programs/tic-tac-toe/src/state/mod.rs @@ -0,0 +1,3 @@ +pub use game::*; + +pub mod game; diff --git a/src/chapter_3/milestone_project_tic-tac-toe.md b/src/chapter_3/milestone_project_tic-tac-toe.md index 71fdab1..e0c1519 100644 --- a/src/chapter_3/milestone_project_tic-tac-toe.md +++ b/src/chapter_3/milestone_project_tic-tac-toe.md @@ -9,6 +9,8 @@ anchor init tic-tac-toe The program will have 2 instructions. First, we need to setup the game. We need to save who is playing it and create a board to play on. Then, the player take turns until there is a winner or a tie. +We recommend to keep programs in a single `lib.rs` file until they get too big. We would keep this program in a single file too but have split up the reference implementation for demonstration purposes. Check out the section at the end of this chapter to learn how to split up a program into multiple files. + ## Setting up the game ### State @@ -212,9 +214,8 @@ impl Game { } pub fn play(&mut self, tile: &Tile) -> Result<()> { - if !self.is_active() { - return err!(TicTacToeError::GameAlreadyOver); - } + require!(self.is_active(), TicTacToeError::GameAlreadyOver); + match tile { tile @ Tile { @@ -496,4 +497,30 @@ Here is your deployment checklist 🚀 There is more to deployments than this e.g. understanding how the BPFLoader works, how to manage keys, how to upgrade your programs and more. Keep reading to learn more! +## Program directory organization +> [Program Code](https://github.com/project-serum/anchor-book/tree/master/programs/tic-tac-toe) + +Eventually, some programs become too big to keep them in a single file and it makes sense to break them up. + +Splitting a program into multiple files works almost the exact same way as splitting up a regular rust program, so if you haven't already, now is the time to read all about that in the [rust book](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html). + +We recommend the following directory structure (using the tic-tac-toe program as an example): +``` +. ++-- lib.rs ++-- errors.rs ++-- instructions +| +-- play.rs +| +-- setup_game.rs +| +-- mod.rs ++-- state +| +-- game.rs +| +-- mod.rs +``` + +The crucial difference to a normal rust layout is the way that instructions have to be imported. The `lib.rs` file has to import each instruction module with a wildcard import (e.g. `use instructions::play::*;`). This has to be done because the `#[program]` macro depends on generated code inside each instruction file. + +To make the imports shorter you can re-export the instruction modules in the `mod.rs` file in the instructions directory with the `pub use` syntax and then import all instructions in the `lib.rs` file with `use instructions::*;`. + + Well done! You've finished the essentials section. You can now move on to the more advanced parts of Anchor.