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 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>,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.
|
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.
|
||||||
|
|
Loading…
Reference in New Issue