add recommended program structure (#30)

This commit is contained in:
Paul 2022-03-11 13:56:10 -05:00 committed by GitHub
parent f3e5b84443
commit 839642907b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 241 additions and 173 deletions

View File

@ -0,0 +1,10 @@
use anchor_lang::prelude::*;
#[error_code]
pub enum TicTacToeError {
TileOutOfBounds,
TileAlreadySet,
GameAlreadyOver,
NotPlayersTurn,
GameAlreadyStarted,
}

View File

@ -0,0 +1,5 @@
pub use play::*;
pub use setup_game::*;
pub mod play;
pub mod setup_game;

View File

@ -0,0 +1,21 @@
use crate::errors::TicTacToeError;
use crate::state::game::*;
use anchor_lang::prelude::*;
pub fn play(ctx: Context<Play>, 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>,
}

View File

@ -0,0 +1,17 @@
use crate::state::game::*;
use anchor_lang::prelude::*;
pub fn setup_game(ctx: Context<SetupGame>, 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>,
}

View File

@ -1,8 +1,10 @@
use num_derive::*;
use num_traits::*;
use std::mem;
use anchor_lang::prelude::*; 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" // this key needs to be changed to whatever public key is returned by "anchor keys list"
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
@ -12,174 +14,10 @@ pub mod tic_tac_toe {
use super::*; use super::*;
pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> { pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> {
let game = &mut ctx.accounts.game; instructions::setup_game::setup_game(ctx, player_two)
game.players = [ctx.accounts.player_one.key(), player_two];
game.turn = 1;
Ok(())
} }
pub fn play(ctx: Context<Play>, tile: Tile) -> Result<()> { pub fn play(ctx: Context<Play>, tile: Tile) -> Result<()> {
let game = &mut ctx.accounts.game; instructions::play::play(ctx, tile)
require!(
game.current_player() == ctx.accounts.player.key(),
TicTacToeError::NotPlayersTurn
);
game.play(&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<Sign>; 3]; 3], // 9 * (1 + 1) = 18
state: GameState, // 32 + 1
}
impl Game {
const MAXIMUM_SIZE: usize = mem::size_of::<Game>() + 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>,
}

View File

@ -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<Sign>; 3]; 3], // 9 * (1 + 1) = 18
state: GameState, // 32 + 1
}
impl Game {
pub const MAXIMUM_SIZE: usize = mem::size_of::<Game>() + 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,
}

View File

@ -0,0 +1,3 @@
pub use game::*;
pub mod game;

View File

@ -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. 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 ## Setting up the game
### State ### State
@ -212,9 +214,8 @@ impl Game {
} }
pub fn play(&mut self, tile: &Tile) -> Result<()> { pub fn play(&mut self, tile: &Tile) -> Result<()> {
if !self.is_active() { require!(self.is_active(), TicTacToeError::GameAlreadyOver);
return err!(TicTacToeError::GameAlreadyOver);
}
match tile { match tile {
tile 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! 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. Well done! You've finished the essentials section. You can now move on to the more advanced parts of Anchor.