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 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<SetupGame>, 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<Play>, 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<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.
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.