add recommended program structure (#30)
This commit is contained in:
parent
f3e5b84443
commit
839642907b
|
@ -0,0 +1,10 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[error_code]
|
||||
pub enum TicTacToeError {
|
||||
TileOutOfBounds,
|
||||
TileAlreadySet,
|
||||
GameAlreadyOver,
|
||||
NotPlayersTurn,
|
||||
GameAlreadyStarted,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub use play::*;
|
||||
pub use setup_game::*;
|
||||
|
||||
pub mod play;
|
||||
pub mod setup_game;
|
|
@ -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>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub use game::*;
|
||||
|
||||
pub mod game;
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue