#![cfg_attr(not(feature = "std"), no_std)]
mod mock;
#[cfg(test)]
mod tests;
use codec::{Decode, Encode};
use frame_support::{
decl_module, decl_storage, decl_event, decl_error, ensure,
storage::StorageMap,
traits::Get,
};
use frame_system::{self as system, ensure_signed};
use sp_runtime::traits::{
Hash, IdentifyAccount,
Member, Verify, Zero, AccountIdConversion,
};
use sp_runtime::{ModuleId, RuntimeDebug, DispatchResult, DispatchError};
use sp_std::{prelude::*, vec::Vec};
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub struct AppInitiateRequest<AccountId, BlockNumber> {
nonce: u128,
player_num: u8,
players: Vec<AccountId>,
timeout: BlockNumber,
min_stone_offchain: u8,
max_stone_onchain: u8,
}
pub type AppInitiateRequestOf<T> = AppInitiateRequest<
<T as system::Trait>::AccountId,
<T as system::Trait>::BlockNumber,
>;
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub struct AppState<BlockNumber, Hash> {
seq_num: u128,
board_state: Vec<u8>,
timeout: BlockNumber,
session_id: Hash,
}
pub type AppStateOf<T> = AppState<
<T as system::Trait>::BlockNumber,
<T as system::Trait>::Hash,
>;
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub struct StateProof<BlockNumber, Hash, Signature> {
app_state: AppState<BlockNumber, Hash>,
sigs: Vec<Signature>,
}
pub type StateProofOf<T> = StateProof<
<T as system::Trait>::BlockNumber,
<T as system::Trait>::Hash,
<T as Trait>::Signature,
>;
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub enum AppStatus {
Idle = 0,
Settle = 1,
Action = 2,
Finalized = 3,
}
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub struct GomokuInfo<AccountId, BlockNumber> {
players: Vec<AccountId>,
player_num: u8,
seq_num: u128,
timeout: BlockNumber,
deadline: BlockNumber,
status: AppStatus,
gomoku_state: GomokuState,
}
pub type GomokuInfoOf<T> = GomokuInfo<
<T as system::Trait>::AccountId,
<T as system::Trait>::BlockNumber,
>;
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
pub enum StateKey {
TurnColor = 0,
WinnerColor = 1,
FullState = 2,
}
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Encode, Decode, RuntimeDebug)]
struct GomokuState {
board_state: Option<Vec<u8>>, stone_num: Option<u16>, stone_num_onchain: Option<u16>, state_key: Option<StateKey>, min_stone_offchain: u8, max_stone_onchain: u8, }
#[derive(Eq, PartialEq)]
pub enum Color {
Black = 1,
White = 2,
}
pub const MULTI_GOMOKU_ID: ModuleId = ModuleId(*b"m_gomoku");
pub trait Trait: system::Trait {
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type Public: IdentifyAccount<AccountId = Self::AccountId>;
type Signature: Verify<Signer = <Self as Trait>::Public> + Member + Decode + Encode;
}
decl_storage! {
trait Store for Module<T: Trait> as MultiGomoku {
pub MultiGomokuInfoMap get(fn gmoku_info):
map hasher(blake2_128_concat) T::Hash => Option<GomokuInfoOf<T>>;
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = 19_000_000 + T::DbWeight::get().reads_writes(1, 1)]
fn app_initiate(
origin,
initiate_request: AppInitiateRequestOf<T>
) -> DispatchResult {
ensure_signed(origin)?;
let session_id = Self::get_session_id(initiate_request.nonce, initiate_request.players.clone());
ensure!(
MultiGomokuInfoMap::<T>::contains_key(&session_id) == false,
"AppId already exists"
);
Self::is_ordered_account(initiate_request.players.clone())?;
let gomoku_state = GomokuState {
board_state: None,
stone_num: None,
stone_num_onchain: None,
state_key: None,
min_stone_offchain: initiate_request.min_stone_offchain,
max_stone_onchain: initiate_request.max_stone_onchain,
};
let gomoku_info = GomokuInfoOf::<T> {
players: initiate_request.players,
player_num: initiate_request.player_num,
seq_num: 0,
timeout: initiate_request.timeout,
deadline: Zero::zero(),
status: AppStatus::Idle,
gomoku_state: gomoku_state,
};
MultiGomokuInfoMap::<T>::insert(session_id, gomoku_info);
Ok(())
}
#[weight = 49_000_000 + T::DbWeight::get().reads_writes(1, 1)]
fn update_by_state(
origin,
state_proof: StateProofOf<T>
) -> DispatchResult {
ensure_signed(origin)?;
let session_id = state_proof.app_state.session_id;
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => Err(Error::<T>::MultiGomokuInfoNotExist)?,
};
let mut new_gomoku_info: GomokuInfoOf<T> = Self::intend_settle(gomoku_info, state_proof.clone())?;
let _state = state_proof.app_state.board_state;
ensure!(
_state.len() == 228,
"invalid state length"
);
let count = 0;
if _state[0] != 0 {
new_gomoku_info = Self::win_game(_state[0], new_gomoku_info.clone())?;
} else {
let mut _state_iter = _state.iter();
for _i in 0..4 {
_state_iter.next();
}
let count = _state_iter.filter(|&x| *x != 0).count() as u8;
ensure!(
count >= new_gomoku_info.gomoku_state.min_stone_offchain,
"not enough offchain stones"
);
}
new_gomoku_info.gomoku_state.board_state = Some(_state);
new_gomoku_info.gomoku_state.stone_num = Some(count);
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info.clone()));
Self::deposit_event(RawEvent::IntendSettle(session_id, new_gomoku_info.seq_num));
Ok(())
}
#[weight = 46_000_000 + T::DbWeight::get().reads_writes(1, 2)]
fn update_by_action(
origin,
session_id: T::Hash,
action: Vec<u8>
) -> DispatchResult {
let caller = ensure_signed(origin)?;
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => Err(Error::<T>::MultiGomokuInfoNotExist)?,
};
let mut new_gomoku_info = Self::apply_action(gomoku_info)?;
let gomoku_state = new_gomoku_info.gomoku_state.clone();
let mut board_state = match gomoku_state.board_state {
Some(state) => state,
None => Err(Error::<T>::EmptyBoardState)?,
};
let turn_color: usize = board_state[1] as usize;
let black_id = board_state[2];
if black_id == 1 {
ensure!(
caller == new_gomoku_info.players[turn_color - 1],
"Not your turn"
);
} else if black_id == 2 {
ensure!(
caller == new_gomoku_info.players[2 - turn_color],
"Not your turn"
)
} else {
Err(Error::<T>::InvalidBlackId)?
}
ensure!(
action.len() == 2,
"invalid action length"
);
let x = action[0];
let y = action[1];
ensure!(
Self::check_boundary(x, y),
"out of boundary"
);
let index: usize = Self::state_index(x, y);
ensure!(
board_state[index] == 0,
"slot is occupied"
);
board_state[index] = turn_color as u8;
let new_stone_num = gomoku_state.stone_num.unwrap_or(0) + 1;
let new_stone_num_onchain = gomoku_state.stone_num_onchain.unwrap_or(0) + 1;
new_gomoku_info.gomoku_state = GomokuState {
board_state: Some(board_state.clone()),
stone_num: Some(new_stone_num),
stone_num_onchain: Some(new_stone_num_onchain),
state_key: gomoku_state.state_key.clone(),
min_stone_offchain: gomoku_state.min_stone_offchain,
max_stone_onchain: gomoku_state.max_stone_onchain,
};
if Self::check_five(board_state.clone(), x, y, 1, 0) || Self::check_five(board_state.clone(), x, y, 0, 1) || Self::check_five(board_state.clone(), x, y, 1, 1) || Self::check_five(board_state.clone(), x, y, 1, -1) {
new_gomoku_info = Self::win_game(turn_color as u8, new_gomoku_info)?;
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info));
return Ok(());
}
if new_stone_num == 225
|| new_stone_num_onchain as u8 > gomoku_state.max_stone_onchain {
board_state[1] = 0;
new_gomoku_info.gomoku_state = GomokuState {
board_state: Some(board_state.clone()),
stone_num: Some(new_stone_num),
stone_num_onchain: Some(new_stone_num_onchain),
state_key: gomoku_state.state_key.clone(),
min_stone_offchain: gomoku_state.min_stone_offchain,
max_stone_onchain: gomoku_state.max_stone_onchain,
};
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info));
} else {
if turn_color == Color::Black as usize {
board_state[1] = 2;
} else {
board_state[1] = 1;
}
new_gomoku_info.gomoku_state = GomokuState {
board_state: Some(board_state),
stone_num: Some(new_stone_num),
stone_num_onchain: Some(new_stone_num_onchain),
state_key: gomoku_state.state_key,
min_stone_offchain: gomoku_state.min_stone_offchain,
max_stone_onchain: gomoku_state.max_stone_onchain,
};
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info));
}
Ok(())
}
#[weight = 30_000_000 + T::DbWeight::get().reads_writes(1, 1)]
fn finalize_on_action_timeout(
origin,
session_id: T::Hash
) -> DispatchResult {
ensure_signed(origin)?;
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => Err(Error::<T>::MultiGomokuInfoNotExist)?,
};
let block_number = frame_system::Module::<T>::block_number();
if gomoku_info.status == AppStatus::Action {
ensure!(
block_number > gomoku_info.deadline,
"deadline does not passes"
);
} else if gomoku_info.status == AppStatus::Settle {
ensure!(
block_number > gomoku_info.deadline + gomoku_info.timeout,
"while setting"
);
} else {
return Ok(());
}
let board_state = match gomoku_info.clone().gomoku_state.board_state {
Some(state) => state,
None => Err(Error::<T>::EmptyBoardState)?,
};
if board_state[1] == Color::Black as u8 {
let new_gomoku_info = Self::win_game(2, gomoku_info)?;
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info));
} else if board_state[1] == Color::White as u8 {
let new_gomoku_info = Self::win_game(1, gomoku_info)?;
MultiGomokuInfoMap::<T>::mutate(session_id, |info| *info = Some(new_gomoku_info));
} else {
return Ok(());
}
Ok(())
}
#[weight = 12_000_000 + T::DbWeight::get().reads(1)]
pub fn is_finalized(
origin,
session_id: T::Hash
) -> DispatchResult {
ensure_signed(origin)?;
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => Err(Error::<T>::MultiGomokuInfoNotExist)?,
};
ensure!(
gomoku_info.status == AppStatus::Finalized,
"NotFinalized"
);
Ok(())
}
#[weight = 12_000_000 + T::DbWeight::get().reads(1)]
pub fn get_outcome(
origin,
session_id: T::Hash,
query: u8,
) -> DispatchResult {
ensure_signed(origin)?;
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => Err(Error::<T>::MultiGomokuInfoNotExist)?,
};
let board_state = match gomoku_info.gomoku_state.board_state {
Some(state) => state,
None => Err(Error::<T>::EmptyBoardState)?,
};
ensure!(
board_state[0] == query,
"FalseOutcome"
);
Ok(())
}
}
}
decl_event! (
pub enum Event<T> where
<T as system::Trait>::Hash
{
IntendSettle(Hash, u128),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
MultiGomokuInfoNotExist,
EmptyBoardState,
InvalidBlackId,
}
}
impl<T: Trait> Module<T> {
pub fn get_session_id(
nonce: u128,
players: Vec<T::AccountId>,
) -> T::Hash {
let multi_gomoku_app_account = Self::app_account();
let mut encoded = multi_gomoku_app_account.encode();
encoded.extend(nonce.encode());
players.into_iter()
.for_each(|players| { encoded.extend(players.encode()); });
let session_id = T::Hashing::hash(&encoded);
return session_id;
}
pub fn get_state(session_id: T::Hash, key: u8) -> Option<Vec<u8>> {
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => return None
};
let board_state = gomoku_info.gomoku_state.board_state.unwrap();
if key == StateKey::WinnerColor as u8 {
let state = vec![board_state[0]];
return Some(state);
} else if key == StateKey::TurnColor as u8 {
let state = vec![board_state[1]];
return Some(state);
} else if key == StateKey::FullState as u8 {
return Some(board_state);
} else {
return None;
}
}
pub fn get_status(session_id: T::Hash) -> Option<AppStatus> {
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => return None
};
return Some(gomoku_info.status);
}
pub fn get_settle_finalized_time(session_id: T::Hash) -> Option<T::BlockNumber> {
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => return None
};
if gomoku_info.status == AppStatus::Settle {
return Some(gomoku_info.deadline);
} else {
return None;
}
}
pub fn get_action_deadline(session_id: T::Hash) -> Option<T::BlockNumber> {
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => return None
};
if gomoku_info.status == AppStatus::Action {
return Some(gomoku_info.deadline);
} else if gomoku_info.status == AppStatus::Settle {
return Some(gomoku_info.deadline + gomoku_info.timeout);
} else {
return None;
}
}
pub fn get_seq_num(session_id: T::Hash) -> Option<u128> {
let gomoku_info = match MultiGomokuInfoMap::<T>::get(session_id) {
Some(info) => info,
None => return None
};
return Some(gomoku_info.seq_num);
}
pub fn app_account() -> T::AccountId {
MULTI_GOMOKU_ID.into_account()
}
fn intend_settle(
mut gomoku_info: GomokuInfoOf<T>,
state_proof: StateProofOf<T>
) -> Result<GomokuInfoOf<T>, DispatchError> {
let app_state = state_proof.app_state;
let encoded = Self::encode_app_state(app_state.clone());
Self::valid_signers(state_proof.sigs, &encoded, gomoku_info.players.clone())?;
ensure!(
gomoku_info.status != AppStatus::Finalized,
"app state is finalized"
);
ensure!(
gomoku_info.seq_num < app_state.seq_num,
"invalid sequence number"
);
gomoku_info.deadline = frame_system::Module::<T>::block_number() + gomoku_info.deadline;
gomoku_info.status = AppStatus::Settle;
Ok(gomoku_info)
}
fn apply_action(
mut gomoku_info: GomokuInfoOf<T>
) -> Result<GomokuInfoOf<T>, DispatchError> {
ensure!(
gomoku_info.status != AppStatus::Finalized,
"app state is finalized"
);
let block_number = frame_system::Module::<T>::block_number();
if gomoku_info.status == AppStatus::Settle && block_number > gomoku_info.deadline {
gomoku_info.seq_num = gomoku_info.seq_num + 1;
gomoku_info.deadline = block_number + gomoku_info.timeout;
gomoku_info.status = AppStatus::Action;
} else {
ensure!(
gomoku_info.status == AppStatus::Action,
"app not in action mode"
);
gomoku_info.seq_num = gomoku_info.seq_num + 1;
gomoku_info.deadline = block_number + gomoku_info.timeout;
gomoku_info.status = AppStatus::Action;
}
Ok(gomoku_info)
}
fn is_ordered_account(
players: Vec<T::AccountId>
) -> Result<(), DispatchError> {
let mut prev = &players[0];
for i in 1..players.len() {
ensure!(
prev < &players[1],
"player is not ascending order"
);
prev = &players[i];
}
Ok(())
}
fn valid_signers(
signatures: Vec<<T as Trait>::Signature>,
encoded: &[u8],
signers: Vec<T::AccountId>,
) -> Result<(), DispatchError> {
for i in 0..signers.len() {
let signature = &signatures[i];
ensure!(
signature.verify(encoded, &signers[i]),
"Check co-sigs failed"
);
}
Ok(())
}
fn win_game(
winner: u8,
mut gomoku_info: GomokuInfoOf<T>
) -> Result<GomokuInfoOf<T>, DispatchError> {
ensure!(
u8::min_value() <= winner && winner <= 2,
"invalid winner state"
);
let mut new_board_state = gomoku_info.gomoku_state.board_state.unwrap_or(vec![0; 228]);
new_board_state[0] = winner;
if winner != 0 { new_board_state[1] = 0;
gomoku_info.status = AppStatus::Finalized;
gomoku_info.gomoku_state.board_state = Some(new_board_state);
} else {
gomoku_info.gomoku_state.board_state = Some(new_board_state);
}
return Ok(gomoku_info);
}
fn check_five(
_board_state: Vec<u8>,
_x: u8,
_y: u8,
_xdir: i8,
_ydir: i8,
) -> bool {
let mut count: u8 = 0;
count += Self::count_stone(_board_state.clone(), _x, _y, _xdir, _ydir).unwrap();
count += Self::count_stone(_board_state, _x, _y, -1 * _xdir, -1 * _ydir).unwrap() - 1; if count >= 5 {
return true
} else {
return false;
}
}
fn count_stone(
_board_state: Vec<u8>,
_x: u8,
_y: u8,
_xdir: i8,
_ydir: i8
) -> Option<u8> {
let mut count: u8 = 1;
while count <= 5 {
let x = (_x as i8 + _xdir * count as i8) as u8;
let y = (_y as i8 + _ydir * count as i8) as u8;
if Self::check_boundary(x, y)
&& (_board_state[Self::state_index(x, y)] == _board_state[Self::state_index(_x, _y)]) {
count += 1;
} else {
return Some(count);
}
}
return None;
}
fn check_boundary(x: u8, y: u8) -> bool {
let board_dimention = 15;
if x < board_dimention && y < board_dimention {
return true;
} else {
return false;
}
}
fn state_index(x: u8, y: u8) -> usize {
let board_dimention = 15;
let index = (3 + board_dimention * x + y) as usize;
return index;
}
fn encode_app_state(
app_state: AppStateOf<T>
) -> Vec<u8> {
let mut encoded = app_state.seq_num.encode();
app_state.board_state.iter()
.for_each(|state| { encoded.extend(state.encode()); });
encoded.extend(app_state.timeout.encode());
encoded.extend(app_state.session_id.encode());
return encoded;
}
}