Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
aff5649b39 | |||
9fd6ffe83f | |||
ff805361a9 | |||
30d1b0b4bf | |||
5233cf1ca6 | |||
d2754fd702 | |||
ce9a0ae215 | |||
067adcdfa8 |
@ -14,7 +14,6 @@ _() {
|
|||||||
_ cargo fmt -- --check
|
_ cargo fmt -- --check
|
||||||
_ cargo build --verbose
|
_ cargo build --verbose
|
||||||
_ cargo test --verbose
|
_ cargo test --verbose
|
||||||
_ cargo clippy -- --deny=warnings
|
|
||||||
|
|
||||||
echo --- ci/localnet-sanity.sh
|
echo --- ci/localnet-sanity.sh
|
||||||
(
|
(
|
||||||
|
@ -24,7 +24,7 @@ usage: $0 [name] [zone] [options...]
|
|||||||
Deploys a CD testnet
|
Deploys a CD testnet
|
||||||
|
|
||||||
name - name of the network
|
name - name of the network
|
||||||
zone - GCE to deploy the network into
|
zone - zone to deploy the network into
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-s edge|beta|stable - Deploy the specified Snap release channel
|
-s edge|beta|stable - Deploy the specified Snap release channel
|
||||||
@ -116,7 +116,7 @@ fi
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
echo --- gce.sh delete
|
echo --- gce.sh delete
|
||||||
time net/gce.sh delete -p "$netName"
|
time net/gce.sh delete -z "$zone" -p "$netName"
|
||||||
if $delete; then
|
if $delete; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
@ -9,11 +9,12 @@ usage() {
|
|||||||
echo "Error: $*"
|
echo "Error: $*"
|
||||||
fi
|
fi
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
usage: $0 [name]
|
usage: $0 [name] [zone]
|
||||||
|
|
||||||
Sanity check a CD testnet
|
Sanity check a CD testnet
|
||||||
|
|
||||||
name - name of the network
|
name - name of the network
|
||||||
|
zone - zone of the network
|
||||||
|
|
||||||
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
Note: the SOLANA_METRICS_CONFIG environment variable is used to configure
|
||||||
metrics
|
metrics
|
||||||
@ -22,11 +23,13 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
netName=$1
|
netName=$1
|
||||||
|
zone=$2
|
||||||
[[ -n $netName ]] || usage ""
|
[[ -n $netName ]] || usage ""
|
||||||
|
[[ -n $zone ]] || usage "Zone not specified"
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
echo --- gce.sh config
|
echo --- gce.sh config
|
||||||
net/gce.sh config -p "$netName"
|
net/gce.sh config -p "$netName" -z "$zone"
|
||||||
net/init-metrics.sh -e
|
net/init-metrics.sh -e
|
||||||
echo --- net.sh sanity
|
echo --- net.sh sanity
|
||||||
net/net.sh sanity \
|
net/net.sh sanity \
|
||||||
|
@ -33,6 +33,7 @@ pay_and_confirm() {
|
|||||||
$solana_wallet "${entrypoint[@]}" confirm "$signature"
|
$solana_wallet "${entrypoint[@]}" confirm "$signature"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$solana_keygen
|
||||||
$solana_wallet "${entrypoint[@]}" address
|
$solana_wallet "${entrypoint[@]}" address
|
||||||
check_balance_output "No account found" "Your balance is: 0"
|
check_balance_output "No account found" "Your balance is: 0"
|
||||||
$solana_wallet "${entrypoint[@]}" airdrop 60
|
$solana_wallet "${entrypoint[@]}" airdrop 60
|
||||||
|
@ -28,6 +28,7 @@ use std::time::Instant;
|
|||||||
use storage_program::StorageProgram;
|
use storage_program::StorageProgram;
|
||||||
use system_program::SystemProgram;
|
use system_program::SystemProgram;
|
||||||
use system_transaction::SystemTransaction;
|
use system_transaction::SystemTransaction;
|
||||||
|
use tictactoe_dashboard_program::TicTacToeDashboardProgram;
|
||||||
use tictactoe_program::TicTacToeProgram;
|
use tictactoe_program::TicTacToeProgram;
|
||||||
use timing::{duration_as_us, timestamp};
|
use timing::{duration_as_us, timestamp};
|
||||||
use transaction::Transaction;
|
use transaction::Transaction;
|
||||||
@ -405,6 +406,10 @@ impl Bank {
|
|||||||
if TicTacToeProgram::process_transaction(&tx, accounts).is_err() {
|
if TicTacToeProgram::process_transaction(&tx, accounts).is_err() {
|
||||||
return Err(BankError::ProgramRuntimeError);
|
return Err(BankError::ProgramRuntimeError);
|
||||||
}
|
}
|
||||||
|
} else if TicTacToeDashboardProgram::check_id(&tx.program_id) {
|
||||||
|
if TicTacToeDashboardProgram::process_transaction(&tx, accounts).is_err() {
|
||||||
|
return Err(BankError::ProgramRuntimeError);
|
||||||
|
}
|
||||||
} else if self.loaded_contract(&tx, accounts) {
|
} else if self.loaded_contract(&tx, accounts) {
|
||||||
} else {
|
} else {
|
||||||
return Err(BankError::UnknownContractId);
|
return Err(BankError::UnknownContractId);
|
||||||
|
@ -60,6 +60,7 @@ pub mod streamer;
|
|||||||
pub mod system_program;
|
pub mod system_program;
|
||||||
pub mod system_transaction;
|
pub mod system_transaction;
|
||||||
pub mod thin_client;
|
pub mod thin_client;
|
||||||
|
pub mod tictactoe_dashboard_program;
|
||||||
pub mod tictactoe_program;
|
pub mod tictactoe_program;
|
||||||
pub mod timing;
|
pub mod timing;
|
||||||
pub mod tpu;
|
pub mod tpu;
|
||||||
|
@ -43,7 +43,7 @@ impl JsonRpcService {
|
|||||||
io.extend_with(rpc.to_delegate());
|
io.extend_with(rpc.to_delegate());
|
||||||
|
|
||||||
let server =
|
let server =
|
||||||
ServerBuilder::with_meta_extractor(io, move |_req: &hyper::Request| Meta {
|
ServerBuilder::with_meta_extractor(io, move |_req: &hyper::Request<hyper::Body>| Meta {
|
||||||
request_processor: request_processor.clone(),
|
request_processor: request_processor.clone(),
|
||||||
transactions_addr,
|
transactions_addr,
|
||||||
drone_addr,
|
drone_addr,
|
||||||
|
165
src/tictactoe_dashboard_program.rs
Normal file
165
src/tictactoe_dashboard_program.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
//! tic-tac-toe dashboard program
|
||||||
|
|
||||||
|
use serde_cbor;
|
||||||
|
use solana_program_interface::account::Account;
|
||||||
|
use solana_program_interface::pubkey::Pubkey;
|
||||||
|
use tictactoe_program::{Error, Game, Result, State, TicTacToeProgram};
|
||||||
|
use transaction::Transaction;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct TicTacToeDashboardProgram {
|
||||||
|
pending: Pubkey, // Latest pending game
|
||||||
|
completed: Vec<Pubkey>, // Last N completed games (0 is the latest)
|
||||||
|
total: usize, // Total number of completed games
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TICTACTOE_DASHBOARD_PROGRAM_ID: [u8; 32] = [
|
||||||
|
4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
impl TicTacToeDashboardProgram {
|
||||||
|
fn deserialize(input: &[u8]) -> Result<TicTacToeDashboardProgram> {
|
||||||
|
if input.len() < 2 {
|
||||||
|
Err(Error::InvalidUserdata)?;
|
||||||
|
}
|
||||||
|
let len = input[0] as usize + (0xFF * input[1] as usize);
|
||||||
|
if len == 0 {
|
||||||
|
Ok(TicTacToeDashboardProgram::default())
|
||||||
|
} else if input.len() < len + 2 {
|
||||||
|
Err(Error::InvalidUserdata)
|
||||||
|
} else {
|
||||||
|
serde_cbor::from_slice(&input[2..(2 + len)]).map_err(|err| {
|
||||||
|
error!("Unable to deserialize game: {:?}", err);
|
||||||
|
Error::InvalidUserdata
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
self: &mut TicTacToeDashboardProgram,
|
||||||
|
game_pubkey: &Pubkey,
|
||||||
|
game: &Game,
|
||||||
|
) -> Result<()> {
|
||||||
|
match game.state {
|
||||||
|
State::Waiting => {
|
||||||
|
self.pending = *game_pubkey;
|
||||||
|
}
|
||||||
|
State::XMove | State::OMove => {
|
||||||
|
// Nothing to do. In progress games are not managed by the dashboard
|
||||||
|
}
|
||||||
|
State::XWon | State::OWon | State::Draw => {
|
||||||
|
if !self.completed.iter().any(|pubkey| pubkey == game_pubkey) {
|
||||||
|
// TODO: Once the PoH high is exposed to programs, it could be used to ensure
|
||||||
|
// that old games are not being re-added and causing |total| to increment
|
||||||
|
// incorrectly.
|
||||||
|
self.total += 1;
|
||||||
|
self.completed.insert(0, *game_pubkey);
|
||||||
|
|
||||||
|
// Only track the last N completed games to
|
||||||
|
// avoid overrunning Account userdata
|
||||||
|
if self.completed.len() > 5 {
|
||||||
|
self.completed.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(self: &TicTacToeDashboardProgram, output: &mut [u8]) -> Result<()> {
|
||||||
|
let self_serialized = serde_cbor::to_vec(self).unwrap();
|
||||||
|
|
||||||
|
if output.len() < 2 + self_serialized.len() {
|
||||||
|
warn!(
|
||||||
|
"{} bytes required to serialize but only have {} bytes",
|
||||||
|
self_serialized.len() + 2,
|
||||||
|
output.len()
|
||||||
|
);
|
||||||
|
return Err(Error::UserdataTooSmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(self_serialized.len() <= 0xFFFF);
|
||||||
|
output[0] = (self_serialized.len() & 0xFF) as u8;
|
||||||
|
output[1] = (self_serialized.len() >> 8) as u8;
|
||||||
|
output[2..(2 + self_serialized.len())].clone_from_slice(&self_serialized);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||||
|
program_id.as_ref() == TICTACTOE_DASHBOARD_PROGRAM_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id() -> Pubkey {
|
||||||
|
Pubkey::new(&TICTACTOE_DASHBOARD_PROGRAM_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_transaction(tx: &Transaction, accounts: &mut [Account]) -> Result<()> {
|
||||||
|
info!("process_transaction: {:?}", tx);
|
||||||
|
// accounts[0] doesn't matter, anybody can cause a dashboard update
|
||||||
|
// accounts[1] must be a Dashboard account
|
||||||
|
// accounts[2] must be a Game account
|
||||||
|
if accounts.len() != 3 {
|
||||||
|
error!("Expected 3 accounts");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
if !Self::check_id(&accounts[1].program_id) {
|
||||||
|
error!("accounts[1] is not a TICTACTOE_DASHBOARD_PROGRAM_ID");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
if accounts[1].userdata.is_empty() {
|
||||||
|
error!("accounts[1] userdata is empty");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dashboard = Self::deserialize(&accounts[1].userdata)?;
|
||||||
|
|
||||||
|
if !TicTacToeProgram::check_id(&accounts[2].program_id) {
|
||||||
|
error!("accounts[2] is not a TICTACTOE_PROGRAM_ID");
|
||||||
|
Err(Error::InvalidArguments)?;
|
||||||
|
}
|
||||||
|
let ttt = TicTacToeProgram::deserialize(&accounts[2].userdata)?;
|
||||||
|
|
||||||
|
match ttt.game {
|
||||||
|
None => Err(Error::NoGame),
|
||||||
|
Some(game) => dashboard.update(&tx.keys[2], &game),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
dashboard.serialize(&mut accounts[1].userdata)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn serde() {
|
||||||
|
let mut dashboard1 = TicTacToeDashboardProgram::default();
|
||||||
|
dashboard1.total = 123;
|
||||||
|
|
||||||
|
let mut userdata = vec![0xff; 256];
|
||||||
|
dashboard1.serialize(&mut userdata).unwrap();
|
||||||
|
|
||||||
|
let dashboard2 = TicTacToeDashboardProgram::deserialize(&userdata).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dashboard1, dashboard2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn serde_userdata_too_small() {
|
||||||
|
let dashboard = TicTacToeDashboardProgram::default();
|
||||||
|
let mut userdata = vec![0xff; 1];
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.serialize(&mut userdata),
|
||||||
|
Err(Error::UserdataTooSmall)
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = TicTacToeDashboardProgram::deserialize(&userdata);
|
||||||
|
assert!(err.is_err());
|
||||||
|
assert_eq!(err.err().unwrap(), Error::InvalidUserdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add tests for business logic
|
||||||
|
}
|
@ -24,25 +24,24 @@ impl std::fmt::Display for Error {
|
|||||||
}
|
}
|
||||||
impl std::error::Error for Error {}
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
enum GridItem {
|
enum BoardItem {
|
||||||
Free,
|
F, // Free
|
||||||
X,
|
X,
|
||||||
O,
|
O,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GridItem {
|
impl Default for BoardItem {
|
||||||
fn default() -> GridItem {
|
fn default() -> BoardItem {
|
||||||
GridItem::Free
|
BoardItem::F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
enum State {
|
pub enum State {
|
||||||
WaitingForO,
|
Waiting,
|
||||||
ORequestPending,
|
|
||||||
XMove,
|
XMove,
|
||||||
OMove,
|
OMove,
|
||||||
XWon,
|
XWon,
|
||||||
@ -51,131 +50,121 @@ enum State {
|
|||||||
}
|
}
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> State {
|
fn default() -> State {
|
||||||
State::WaitingForO
|
State::Waiting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
struct Game {
|
pub struct Game {
|
||||||
player_x: Pubkey,
|
player_x: Pubkey,
|
||||||
player_o: Option<Pubkey>,
|
player_o: Option<Pubkey>,
|
||||||
state: State,
|
pub state: State,
|
||||||
grid: [GridItem; 9],
|
board: [BoardItem; 9],
|
||||||
|
keep_alive: [i64; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn create(player_x: &Pubkey) -> Game {
|
pub fn create(player_x: &Pubkey) -> Game {
|
||||||
let mut game = Game::default();
|
let mut game = Game::default();
|
||||||
game.player_x = *player_x;
|
game.player_x = *player_x;
|
||||||
assert_eq!(game.state, State::WaitingForO);
|
assert_eq!(game.state, State::Waiting);
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn new(player_x: Pubkey, player_o: Pubkey) -> Game {
|
pub fn new(player_x: Pubkey, player_o: Pubkey) -> Game {
|
||||||
let mut game = Game::create(&player_x);
|
let mut game = Game::create(&player_x);
|
||||||
game.join(&player_o).unwrap();
|
game.join(player_o, 0).unwrap();
|
||||||
game.accept().unwrap();
|
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join(self: &mut Game, player_o: &Pubkey) -> Result<()> {
|
pub fn join(self: &mut Game, player_o: Pubkey, timestamp: i64) -> Result<()> {
|
||||||
if self.state == State::WaitingForO {
|
if self.state == State::Waiting {
|
||||||
self.player_o = Some(*player_o);
|
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;
|
self.state = State::XMove;
|
||||||
|
self.keep_alive[1] = timestamp;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NotYourTurn)
|
Err(Error::GameInProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reject(self: &mut Game) -> Result<()> {
|
fn same(x_or_o: BoardItem, triple: &[BoardItem]) -> bool {
|
||||||
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 {
|
|
||||||
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 grid_index = y * 3 + x;
|
let board_index = y * 3 + x;
|
||||||
if grid_index >= self.grid.len() || self.grid[grid_index] != GridItem::Free {
|
if board_index >= self.board.len() || self.board[board_index] != BoardItem::F {
|
||||||
return Err(Error::InvalidMove);
|
Err(Error::InvalidMove)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (x_or_o, won_state) = match self.state {
|
let (x_or_o, won_state) = match self.state {
|
||||||
State::XMove => {
|
State::XMove => {
|
||||||
if player != self.player_x {
|
if player != self.player_x {
|
||||||
return Err(Error::PlayerNotFound)?;
|
return Err(Error::PlayerNotFound);
|
||||||
}
|
}
|
||||||
self.state = State::OMove;
|
self.state = State::OMove;
|
||||||
(GridItem::X, State::XWon)
|
(BoardItem::X, State::XWon)
|
||||||
}
|
}
|
||||||
State::OMove => {
|
State::OMove => {
|
||||||
if player != self.player_o.unwrap() {
|
if player != self.player_o.unwrap() {
|
||||||
return Err(Error::PlayerNotFound)?;
|
return Err(Error::PlayerNotFound);
|
||||||
}
|
}
|
||||||
self.state = State::XMove;
|
self.state = State::XMove;
|
||||||
(GridItem::O, State::OWon)
|
(BoardItem::O, State::OWon)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::NotYourTurn)?;
|
return Err(Error::NotYourTurn);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.grid[grid_index] = x_or_o;
|
self.board[board_index] = x_or_o;
|
||||||
|
|
||||||
let winner =
|
let winner =
|
||||||
// Check rows
|
// Check rows
|
||||||
Game::same(x_or_o, &self.grid[0..3])
|
Game::same(x_or_o, &self.board[0..3])
|
||||||
|| Game::same(x_or_o, &self.grid[3..6])
|
|| Game::same(x_or_o, &self.board[3..6])
|
||||||
|| Game::same(x_or_o, &self.grid[6..9])
|
|| Game::same(x_or_o, &self.board[6..9])
|
||||||
// Check columns
|
// Check columns
|
||||||
|| Game::same(x_or_o, &[self.grid[0], self.grid[3], self.grid[6]])
|
|| Game::same(x_or_o, &[self.board[0], self.board[3], self.board[6]])
|
||||||
|| Game::same(x_or_o, &[self.grid[1], self.grid[4], self.grid[7]])
|
|| Game::same(x_or_o, &[self.board[1], self.board[4], self.board[7]])
|
||||||
|| Game::same(x_or_o, &[self.grid[2], self.grid[5], self.grid[8]])
|
|| Game::same(x_or_o, &[self.board[2], self.board[5], self.board[8]])
|
||||||
// Check both diagonals
|
// Check both diagonals
|
||||||
|| Game::same(x_or_o, &[self.grid[0], self.grid[4], self.grid[8]])
|
|| Game::same(x_or_o, &[self.board[0], self.board[4], self.board[8]])
|
||||||
|| Game::same(x_or_o, &[self.grid[2], self.grid[4], self.grid[6]]);
|
|| Game::same(x_or_o, &[self.board[2], self.board[4], self.board[6]]);
|
||||||
|
|
||||||
if winner {
|
if winner {
|
||||||
self.state = won_state;
|
self.state = won_state;
|
||||||
} else if self.grid.iter().all(|&p| p != GridItem::Free) {
|
} else if self.board.iter().all(|&p| p != BoardItem::F) {
|
||||||
self.state = State::Draw;
|
self.state = State::Draw;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keep_alive(self: &mut Game, player: Pubkey, timestamp: i64) -> Result<()> {
|
||||||
|
if player == self.player_x {
|
||||||
|
self.keep_alive[0] = timestamp;
|
||||||
|
} else if Some(player) == self.player_o {
|
||||||
|
self.keep_alive[1] = timestamp;
|
||||||
|
} else {
|
||||||
|
Err(Error::PlayerNotFound)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
enum Command {
|
enum Command {
|
||||||
Init, // player X initializes a new game
|
Init, // player X initializes a new game
|
||||||
Join, // player O wants to join
|
Join(i64), // player O wants to join (seconds since UNIX epoch)
|
||||||
Accept, // player X accepts the Join request
|
KeepAlive(i64), // player X/O keep alive (seconds since UNIX epoch)
|
||||||
Reject, // player X rejects the Join request
|
Move(u8, u8), // player X/O mark board position (x, y)
|
||||||
Move(u8, u8), // player X/O mark board position (x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct TicTacToeProgram {
|
pub struct TicTacToeProgram {
|
||||||
game: Option<Game>,
|
pub game: Option<Game>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
||||||
@ -183,7 +172,7 @@ pub const TICTACTOE_PROGRAM_ID: [u8; 32] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
impl TicTacToeProgram {
|
impl TicTacToeProgram {
|
||||||
fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> {
|
pub fn deserialize(input: &[u8]) -> Result<TicTacToeProgram> {
|
||||||
let len = input[0] as usize;
|
let len = input[0] as usize;
|
||||||
|
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
@ -214,22 +203,9 @@ impl TicTacToeProgram {
|
|||||||
|
|
||||||
if let Some(ref mut game) = self.game {
|
if let Some(ref mut game) = self.game {
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Join => game.join(player),
|
Command::Join(timestamp) => game.join(*player, *timestamp),
|
||||||
Command::Accept => {
|
|
||||||
if *player == game.player_x {
|
|
||||||
game.accept()
|
|
||||||
} else {
|
|
||||||
Err(Error::PlayerNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::Reject => {
|
|
||||||
if *player == game.player_x {
|
|
||||||
game.reject()
|
|
||||||
} else {
|
|
||||||
Err(Error::PlayerNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::Move(x, y) => game.next_move(*player, *x as usize, *y as usize),
|
Command::Move(x, y) => game.next_move(*player, *x as usize, *y as usize),
|
||||||
|
Command::KeepAlive(timestamp) => game.keep_alive(*player, *timestamp),
|
||||||
Command::Init => panic!("Unreachable"),
|
Command::Init => panic!("Unreachable"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user