Improve game setup experience: X now shares game key and accepts O
This commit is contained in:
@ -41,6 +41,8 @@ impl Default for GridItem {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
enum State {
|
enum State {
|
||||||
|
WaitingForO,
|
||||||
|
ORequestPending,
|
||||||
XMove,
|
XMove,
|
||||||
OMove,
|
OMove,
|
||||||
XWon,
|
XWon,
|
||||||
@ -49,35 +51,70 @@ enum State {
|
|||||||
}
|
}
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> State {
|
fn default() -> State {
|
||||||
State::XMove
|
State::WaitingForO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
struct Game {
|
struct Game {
|
||||||
players: [Pubkey; 2],
|
player_x: Pubkey,
|
||||||
|
player_o: Option<Pubkey>,
|
||||||
state: State,
|
state: State,
|
||||||
grid: [GridItem; 9],
|
grid: [GridItem; 9],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new(player1: Pubkey, player2: Pubkey) -> Game {
|
pub fn create(player_x: &Pubkey) -> Game {
|
||||||
let mut game = Game::default();
|
let mut game = Game::default();
|
||||||
game.players = [player1, player2];
|
game.player_x = *player_x;
|
||||||
|
assert_eq!(game.state, State::WaitingForO);
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn new(player_x: Pubkey, player_o: Pubkey) -> Game {
|
||||||
|
let mut game = Game::create(&player_x);
|
||||||
|
game.join(&player_o).unwrap();
|
||||||
|
game.accept().unwrap();
|
||||||
|
game
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join(self: &mut Game, player_o: &Pubkey) -> Result<()> {
|
||||||
|
if self.state == State::WaitingForO {
|
||||||
|
self.player_o = Some(*player_o);
|
||||||
|
self.state = State::ORequestPending;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::NotYourTurn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept(self: &mut Game) -> Result<()> {
|
||||||
|
if self.state == State::ORequestPending {
|
||||||
|
assert!(self.player_o.is_some());
|
||||||
|
self.state = State::XMove;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::NotYourTurn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: &mut Game) -> Result<()> {
|
||||||
|
if self.state == State::ORequestPending {
|
||||||
|
assert!(self.player_o.is_some());
|
||||||
|
self.player_o = None;
|
||||||
|
self.state = State::WaitingForO;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::NotYourTurn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn same(x_or_o: GridItem, triple: &[GridItem]) -> bool {
|
fn same(x_or_o: GridItem, triple: &[GridItem]) -> bool {
|
||||||
triple.iter().all(|&i| i == x_or_o)
|
triple.iter().all(|&i| i == x_or_o)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_move(self: &mut Game, player: Pubkey, x: usize, y: usize) -> Result<()> {
|
pub fn next_move(self: &mut Game, player: Pubkey, x: usize, y: usize) -> Result<()> {
|
||||||
let index = self
|
|
||||||
.players
|
|
||||||
.iter()
|
|
||||||
.position(|&p| p == player)
|
|
||||||
.ok_or(Error::PlayerNotFound)?;
|
|
||||||
|
|
||||||
let grid_index = y * 3 + x;
|
let grid_index = y * 3 + x;
|
||||||
if grid_index >= self.grid.len() || self.grid[grid_index] != GridItem::Free {
|
if grid_index >= self.grid.len() || self.grid[grid_index] != GridItem::Free {
|
||||||
return Err(Error::InvalidMove);
|
return Err(Error::InvalidMove);
|
||||||
@ -85,15 +122,15 @@ impl Game {
|
|||||||
|
|
||||||
let (x_or_o, won_state) = match self.state {
|
let (x_or_o, won_state) = match self.state {
|
||||||
State::XMove => {
|
State::XMove => {
|
||||||
if index != 0 {
|
if player != self.player_x {
|
||||||
return Err(Error::NotYourTurn)?;
|
return Err(Error::PlayerNotFound)?;
|
||||||
}
|
}
|
||||||
self.state = State::OMove;
|
self.state = State::OMove;
|
||||||
(GridItem::X, State::XWon)
|
(GridItem::X, State::XWon)
|
||||||
}
|
}
|
||||||
State::OMove => {
|
State::OMove => {
|
||||||
if index != 1 {
|
if player != self.player_o.unwrap() {
|
||||||
return Err(Error::NotYourTurn)?;
|
return Err(Error::PlayerNotFound)?;
|
||||||
}
|
}
|
||||||
self.state = State::XMove;
|
self.state = State::XMove;
|
||||||
(GridItem::O, State::OWon)
|
(GridItem::O, State::OWon)
|
||||||
@ -129,8 +166,11 @@ impl Game {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
enum Command {
|
enum Command {
|
||||||
Init(Pubkey, Pubkey), // player1, player2
|
Init, // player X initializes a new game
|
||||||
Move(Pubkey, u8, u8), // player, x, y
|
Join, // player O wants to join
|
||||||
|
Accept, // player X accepts the Join request
|
||||||
|
Reject, // player X rejects the Join request
|
||||||
|
Move(u8, u8), // player X/O mark board position (x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
@ -151,33 +191,49 @@ impl TicTacToeProgram {
|
|||||||
} else if input.len() < len + 1 {
|
} else if input.len() < len + 1 {
|
||||||
Err(Error::InvalidUserdata)
|
Err(Error::InvalidUserdata)
|
||||||
} else {
|
} else {
|
||||||
serde_cbor::from_slice(&input[1..len + 1]).map_err(|err| {
|
serde_cbor::from_slice(&input[1..=len]).map_err(|err| {
|
||||||
error!("Unable to deserialize game: {:?}", err);
|
error!("Unable to deserialize game: {:?}", err);
|
||||||
Error::InvalidUserdata
|
Error::InvalidUserdata
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_command(self: &mut TicTacToeProgram, cmd: &Command) -> Result<()> {
|
fn dispatch_command(self: &mut TicTacToeProgram, cmd: &Command, player: &Pubkey) -> Result<()> {
|
||||||
info!("dispatch_command: cmd={:?}", cmd);
|
info!("dispatch_command: cmd={:?} player={}", cmd, player);
|
||||||
info!("dispatch_command: account={:?}", self);
|
info!("dispatch_command: account={:?}", self);
|
||||||
match cmd {
|
|
||||||
Command::Init(player_1, player_2) => {
|
if let Command::Init = cmd {
|
||||||
if let Some(_) = self.game {
|
return if self.game.is_some() {
|
||||||
Err(Error::GameInProgress)
|
Err(Error::GameInProgress)
|
||||||
} else {
|
} else {
|
||||||
let game = Game::new(*player_1, *player_2);
|
let game = Game::create(player);
|
||||||
self.game = Some(game);
|
self.game = Some(game);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref mut game) = self.game {
|
||||||
|
match cmd {
|
||||||
|
Command::Join => game.join(player),
|
||||||
|
Command::Accept => {
|
||||||
|
if *player == game.player_x {
|
||||||
|
game.accept()
|
||||||
|
} else {
|
||||||
|
Err(Error::PlayerNotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Command::Reject => {
|
||||||
Command::Move(player, x, y) => {
|
if *player == game.player_x {
|
||||||
if let Some(ref mut game) = self.game {
|
game.reject()
|
||||||
game.next_move(*player, *x as usize, *y as usize)
|
} else {
|
||||||
} else {
|
Err(Error::PlayerNotFound)
|
||||||
Err(Error::NoGame)
|
}
|
||||||
}
|
}
|
||||||
|
Command::Move(x, y) => game.next_move(*player, *x as usize, *y as usize),
|
||||||
|
Command::Init => panic!("Unreachable"),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::NoGame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +251,7 @@ impl TicTacToeProgram {
|
|||||||
|
|
||||||
assert!(self_serialized.len() <= 255);
|
assert!(self_serialized.len() <= 255);
|
||||||
output[0] = self_serialized.len() as u8;
|
output[0] = self_serialized.len() as u8;
|
||||||
output[1..self_serialized.len() + 1].clone_from_slice(&self_serialized);
|
output[1..=self_serialized.len()].clone_from_slice(&self_serialized);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,11 +267,11 @@ impl TicTacToeProgram {
|
|||||||
// accounts[1] must always be the Tic-tac-toe game state account
|
// accounts[1] must always be the Tic-tac-toe game state account
|
||||||
if accounts.len() < 2 || !Self::check_id(&accounts[1].program_id) {
|
if accounts.len() < 2 || !Self::check_id(&accounts[1].program_id) {
|
||||||
error!("accounts[1] is not assigned to the TICTACTOE_PROGRAM_ID");
|
error!("accounts[1] is not assigned to the TICTACTOE_PROGRAM_ID");
|
||||||
return Err(Error::InvalidArguments);
|
Err(Error::InvalidArguments)?;
|
||||||
}
|
}
|
||||||
if accounts[1].userdata.is_empty() {
|
if accounts[1].userdata.is_empty() {
|
||||||
error!("accounts[1] userdata is empty");
|
error!("accounts[1] userdata is empty");
|
||||||
return Err(Error::InvalidArguments);
|
Err(Error::InvalidArguments)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut program_state = Self::deserialize(&accounts[1].userdata)?;
|
let mut program_state = Self::deserialize(&accounts[1].userdata)?;
|
||||||
@ -225,25 +281,22 @@ impl TicTacToeProgram {
|
|||||||
Error::InvalidUserdata
|
Error::InvalidUserdata
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match command {
|
if let Command::Init = command {
|
||||||
Command::Init(_, _) => {
|
// Init must be signed by the game state account itself, who's private key is
|
||||||
// Init() must be signed by the game state account itself, who's private key is
|
// known only to player X
|
||||||
// known only to one of the players.
|
if !Self::check_id(&accounts[0].program_id) {
|
||||||
if !Self::check_id(&accounts[0].program_id) {
|
error!("accounts[0] is not assigned to the TICTACTOE_PROGRAM_ID");
|
||||||
error!("accounts[0] is not assigned to the TICTACTOE_PROGRAM_ID");
|
return Err(Error::InvalidArguments);
|
||||||
return Err(Error::InvalidArguments);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::Move(player, _, _) => {
|
|
||||||
// Move() must be signed by the player that is wanting to make the next move.
|
|
||||||
if player != tx.keys[0] {
|
|
||||||
error!("keys[0]({})/player({}) mismatch", tx.keys[0], player);
|
|
||||||
return Err(Error::InvalidArguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
program_state.dispatch_command(&command)?;
|
// player X public key is in keys[2]
|
||||||
|
if accounts.len() < 3 {
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
program_state.dispatch_command(&command, &tx.keys[2])?;
|
||||||
|
} else {
|
||||||
|
program_state.dispatch_command(&command, &tx.keys[0])?;
|
||||||
|
}
|
||||||
program_state.serialize(&mut accounts[1].userdata)?;
|
program_state.serialize(&mut accounts[1].userdata)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -273,11 +326,8 @@ mod test {
|
|||||||
let mut account = TicTacToeProgram::default();
|
let mut account = TicTacToeProgram::default();
|
||||||
assert!(account.game.is_none());
|
assert!(account.game.is_none());
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
account.dispatch_command(&Command::Init, &player_x).unwrap();
|
||||||
account
|
|
||||||
.dispatch_command(&Command::Init(player_1, player_2))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut userdata = vec![0xff; 256];
|
let mut userdata = vec![0xff; 256];
|
||||||
account.serialize(&mut userdata).unwrap();
|
account.serialize(&mut userdata).unwrap();
|
||||||
@ -311,21 +361,21 @@ mod test {
|
|||||||
X| |
|
X| |
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
let player_o = Pubkey::new(&[2; 32]);
|
||||||
|
|
||||||
let mut g = Game::new(player_1, player_2);
|
let mut g = Game::new(player_x, player_o);
|
||||||
assert_eq!(g.state, State::XMove);
|
assert_eq!(g.state, State::XMove);
|
||||||
|
|
||||||
g.next_move(player_1, 0, 0).unwrap();
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
assert_eq!(g.state, State::OMove);
|
assert_eq!(g.state, State::OMove);
|
||||||
g.next_move(player_2, 1, 0).unwrap();
|
g.next_move(player_o, 1, 0).unwrap();
|
||||||
assert_eq!(g.state, State::XMove);
|
assert_eq!(g.state, State::XMove);
|
||||||
g.next_move(player_1, 0, 1).unwrap();
|
g.next_move(player_x, 0, 1).unwrap();
|
||||||
assert_eq!(g.state, State::OMove);
|
assert_eq!(g.state, State::OMove);
|
||||||
g.next_move(player_2, 1, 1).unwrap();
|
g.next_move(player_o, 1, 1).unwrap();
|
||||||
assert_eq!(g.state, State::XMove);
|
assert_eq!(g.state, State::XMove);
|
||||||
g.next_move(player_1, 0, 2).unwrap();
|
g.next_move(player_x, 0, 2).unwrap();
|
||||||
assert_eq!(g.state, State::XWon);
|
assert_eq!(g.state, State::XWon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,20 +389,20 @@ mod test {
|
|||||||
X| |
|
X| |
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
let player_o = Pubkey::new(&[2; 32]);
|
||||||
let mut g = Game::new(player_1, player_2);
|
let mut g = Game::new(player_x, player_o);
|
||||||
|
|
||||||
g.next_move(player_1, 0, 0).unwrap();
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
g.next_move(player_2, 1, 0).unwrap();
|
g.next_move(player_o, 1, 0).unwrap();
|
||||||
g.next_move(player_1, 2, 0).unwrap();
|
g.next_move(player_x, 2, 0).unwrap();
|
||||||
g.next_move(player_2, 0, 1).unwrap();
|
g.next_move(player_o, 0, 1).unwrap();
|
||||||
g.next_move(player_1, 1, 1).unwrap();
|
g.next_move(player_x, 1, 1).unwrap();
|
||||||
g.next_move(player_2, 2, 1).unwrap();
|
g.next_move(player_o, 2, 1).unwrap();
|
||||||
g.next_move(player_1, 0, 2).unwrap();
|
g.next_move(player_x, 0, 2).unwrap();
|
||||||
assert_eq!(g.state, State::XWon);
|
assert_eq!(g.state, State::XWon);
|
||||||
|
|
||||||
assert_eq!(g.next_move(player_2, 1, 2), Err(Error::NotYourTurn));
|
assert_eq!(g.next_move(player_o, 1, 2), Err(Error::NotYourTurn));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -365,19 +415,19 @@ mod test {
|
|||||||
O|O|O
|
O|O|O
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
let player_o = Pubkey::new(&[2; 32]);
|
||||||
let mut g = Game::new(player_1, player_2);
|
let mut g = Game::new(player_x, player_o);
|
||||||
|
|
||||||
g.next_move(player_1, 0, 0).unwrap();
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
g.next_move(player_2, 0, 2).unwrap();
|
g.next_move(player_o, 0, 2).unwrap();
|
||||||
g.next_move(player_1, 1, 0).unwrap();
|
g.next_move(player_x, 1, 0).unwrap();
|
||||||
g.next_move(player_2, 1, 2).unwrap();
|
g.next_move(player_o, 1, 2).unwrap();
|
||||||
g.next_move(player_1, 0, 1).unwrap();
|
g.next_move(player_x, 0, 1).unwrap();
|
||||||
g.next_move(player_2, 2, 2).unwrap();
|
g.next_move(player_o, 2, 2).unwrap();
|
||||||
assert_eq!(g.state, State::OWon);
|
assert_eq!(g.state, State::OWon);
|
||||||
|
|
||||||
assert!(g.next_move(player_1, 1, 2).is_err());
|
assert!(g.next_move(player_x, 1, 2).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -390,19 +440,19 @@ mod test {
|
|||||||
O|X|X
|
O|X|X
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
let player_o = Pubkey::new(&[2; 32]);
|
||||||
let mut g = Game::new(player_1, player_2);
|
let mut g = Game::new(player_x, player_o);
|
||||||
|
|
||||||
g.next_move(player_1, 0, 0).unwrap();
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
g.next_move(player_2, 1, 0).unwrap();
|
g.next_move(player_o, 1, 0).unwrap();
|
||||||
g.next_move(player_1, 2, 0).unwrap();
|
g.next_move(player_x, 2, 0).unwrap();
|
||||||
g.next_move(player_2, 0, 1).unwrap();
|
g.next_move(player_o, 0, 1).unwrap();
|
||||||
g.next_move(player_1, 1, 1).unwrap();
|
g.next_move(player_x, 1, 1).unwrap();
|
||||||
g.next_move(player_2, 2, 1).unwrap();
|
g.next_move(player_o, 2, 1).unwrap();
|
||||||
g.next_move(player_1, 1, 2).unwrap();
|
g.next_move(player_x, 1, 2).unwrap();
|
||||||
g.next_move(player_2, 0, 2).unwrap();
|
g.next_move(player_o, 0, 2).unwrap();
|
||||||
g.next_move(player_1, 2, 2).unwrap();
|
g.next_move(player_x, 2, 2).unwrap();
|
||||||
assert_eq!(g.state, State::XWon);
|
assert_eq!(g.state, State::XWon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,20 +466,40 @@ mod test {
|
|||||||
X|X|O
|
X|X|O
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let player_1 = Pubkey::new(&[1; 32]);
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
let player_2 = Pubkey::new(&[2; 32]);
|
let player_o = Pubkey::new(&[2; 32]);
|
||||||
let mut g = Game::new(player_1, player_2);
|
let mut g = Game::new(player_x, player_o);
|
||||||
|
|
||||||
g.next_move(player_1, 0, 0).unwrap();
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
g.next_move(player_2, 1, 1).unwrap();
|
g.next_move(player_o, 1, 1).unwrap();
|
||||||
g.next_move(player_1, 0, 2).unwrap();
|
g.next_move(player_x, 0, 2).unwrap();
|
||||||
g.next_move(player_2, 0, 1).unwrap();
|
g.next_move(player_o, 0, 1).unwrap();
|
||||||
g.next_move(player_1, 2, 1).unwrap();
|
g.next_move(player_x, 2, 1).unwrap();
|
||||||
g.next_move(player_2, 1, 0).unwrap();
|
g.next_move(player_o, 1, 0).unwrap();
|
||||||
g.next_move(player_1, 1, 2).unwrap();
|
g.next_move(player_x, 1, 2).unwrap();
|
||||||
g.next_move(player_2, 2, 2).unwrap();
|
g.next_move(player_o, 2, 2).unwrap();
|
||||||
g.next_move(player_1, 2, 0).unwrap();
|
g.next_move(player_x, 2, 0).unwrap();
|
||||||
|
|
||||||
assert_eq!(g.state, State::Draw);
|
assert_eq!(g.state, State::Draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn solo() {
|
||||||
|
/*
|
||||||
|
X|O|
|
||||||
|
-+-+-
|
||||||
|
| |
|
||||||
|
-+-+-
|
||||||
|
| |
|
||||||
|
*/
|
||||||
|
|
||||||
|
let player_x = Pubkey::new(&[1; 32]);
|
||||||
|
|
||||||
|
let mut g = Game::new(player_x, player_x);
|
||||||
|
assert_eq!(g.state, State::XMove);
|
||||||
|
g.next_move(player_x, 0, 0).unwrap();
|
||||||
|
assert_eq!(g.state, State::OMove);
|
||||||
|
g.next_move(player_x, 1, 0).unwrap();
|
||||||
|
assert_eq!(g.state, State::XMove);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user