diff --git a/Cargo.lock b/Cargo.lock
index f7b578b..681c1dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2767,6 +2767,7 @@ dependencies = [
"pallet-grandpa",
"pallet-hexalem",
"pallet-sudo",
+ "pallet-tic-tac-toe",
"pallet-timestamp",
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
@@ -4690,6 +4691,22 @@ dependencies = [
"sp-std",
]
+[[package]]
+name = "pallet-tic-tac-toe"
+version = "4.0.0-dev"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
[[package]]
name = "pallet-timestamp"
version = "4.0.0-dev"
diff --git a/Cargo.toml b/Cargo.toml
index cf946ec..63df40c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ version = "4.0.0-dev"
[workspace]
members = [
"node",
- "pallets/hexalem",
+ "pallets/*",
"runtime",
]
resolver = "2"
@@ -111,6 +111,7 @@ try-runtime-cli = { git = "https://github.com/parityt
# Substrate Gaming Pallets
pallet-hexalem = { path = "pallets/hexalem", default-features = false }
+pallet-tic-tac-toe = { path = "pallets/tic-tac-toe", default-features = false }
# Substrate Gaming Runtime
hexalem-runtime = { path = "runtime" }
diff --git a/pallets/tic-tac-toe/Cargo.toml b/pallets/tic-tac-toe/Cargo.toml
new file mode 100644
index 0000000..78a4ea4
--- /dev/null
+++ b/pallets/tic-tac-toe/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+description = "Sample pallet for the tic-tac-toe game"
+name = "pallet-tic-tac-toe"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[package.metadata.docs.rs]
+targets = [ "x86_64-unknown-linux-gnu" ]
+
+[dependencies]
+# Substrate (wasm)
+frame-benchmarking = { workspace = true, optional = true }
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+parity-scale-codec = { workspace = true, features = [ "derive", "max-encoded-len" ] }
+scale-info = { workspace = true, features = [ "derive" ] }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+[dev-dependencies]
+pallet-balances = { workspace = true }
+
+[features]
+default = [ "std" ]
+runtime-benchmarks = [
+ "frame-benchmarking",
+]
+std = [
+ "parity-scale-codec/std",
+ "scale-info/std",
+ "frame-benchmarking/std",
+ "frame-support/std",
+ "frame-system/std",
+ "pallet-balances/std",
+ "sp-core/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "sp-io/std",
+]
+try-runtime = [ "frame-support/try-runtime" ]
diff --git a/pallets/tic-tac-toe/README.md b/pallets/tic-tac-toe/README.md
new file mode 100644
index 0000000..a000d6d
--- /dev/null
+++ b/pallets/tic-tac-toe/README.md
@@ -0,0 +1,3 @@
+# Pallet tic-tac-toe
+
+This pallet is meant to be a template for other game developers into how to create a backend for a Substrate-based game.
diff --git a/pallets/tic-tac-toe/benchmarking/Cargo.toml b/pallets/tic-tac-toe/benchmarking/Cargo.toml
new file mode 100644
index 0000000..38b5e0c
--- /dev/null
+++ b/pallets/tic-tac-toe/benchmarking/Cargo.toml
@@ -0,0 +1,57 @@
+[package]
+description = "Ajuna Network pallet used for NFT Staking benchmarking"
+name = "pallet-ajuna-nft-staking-benchmarking"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[package.metadata.docs.rs]
+targets = [ "x86_64-unknown-linux-gnu" ]
+
+[dependencies]
+# General (wasm)
+log = { workspace = true }
+
+# Substrate (wasm)
+frame-benchmarking = { workspace = true, optional = true }
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+pallet-balances = { workspace = true }
+pallet-nfts = { workspace = true }
+parity-scale-codec = { workspace = true, features = [ "derive", "max-encoded-len" ] }
+scale-info = { workspace = true, features = [ "derive" ] }
+sp-io = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+# Ajuna
+pallet-ajuna-nft-staking = { workspace = true }
+
+[dev-dependencies]
+sp-core = { workspace = true }
+
+[features]
+default = [ "std" ]
+runtime-benchmarks = [
+ "frame-benchmarking",
+ "pallet-nfts/runtime-benchmarks",
+ "pallet-ajuna-nft-staking/runtime-benchmarks",
+]
+std = [
+ "parity-scale-codec/std",
+ "scale-info/std",
+ "sp-io/std",
+ "pallet-balances/std",
+ "pallet-nfts/std",
+ "pallet-ajuna-nft-staking/std",
+ "frame-benchmarking/std",
+ "frame-support/std",
+ "frame-system/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "log/std",
+]
+try-runtime = [ "frame-support/try-runtime" ]
diff --git a/pallets/tic-tac-toe/benchmarking/src/lib.rs b/pallets/tic-tac-toe/benchmarking/src/lib.rs
new file mode 100644
index 0000000..a7ca46b
--- /dev/null
+++ b/pallets/tic-tac-toe/benchmarking/src/lib.rs
@@ -0,0 +1,561 @@
+// Ajuna Node
+// Copyright (C) 2022 BlogaTech AG
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#![cfg(feature = "runtime-benchmarks")]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+mod mock;
+
+use frame_benchmarking::benchmarks;
+use frame_support::{
+ pallet_prelude::*,
+ traits::{
+ tokens::nonfungibles_v2::{Create, Mutate},
+ Currency, Get,
+ },
+};
+use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
+use pallet_ajuna_nft_staking::{
+ BenchmarkHelper as NftStakingBenchmarkHelper, Config as NftStakingConfig, *,
+};
+use pallet_nfts::{BenchmarkHelper, ItemConfig};
+use sp_runtime::{
+ bounded_vec,
+ traits::{One, UniqueSaturatedFrom, UniqueSaturatedInto},
+ DispatchError,
+};
+
+// Creator's collections.
+const CONTRACT_COLLECTION: u16 = 0;
+const REWARD_COLLECTION: u16 = 1;
+// Staker's collections.
+const STAKE_COLLECTION: u16 = 2;
+const FEE_COLLECTION: u16 = 3;
+// Sniper's collections.
+const SNIPER_STAKE_COLLECTION: u16 = 4;
+const SNIPER_FEE_COLLECTION: u16 = 5;
+// Unified attribute value for all contracts.
+const ATTRIBUTE_VALUE: u8 = 10;
+
+enum Mode {
+ Staker,
+ Sniper,
+}
+impl Mode {
+ fn collections(self) -> (u16, u16) {
+ match self {
+ Self::Staker => (STAKE_COLLECTION, FEE_COLLECTION),
+ Self::Sniper => (SNIPER_STAKE_COLLECTION, SNIPER_FEE_COLLECTION),
+ }
+ }
+}
+
+pub struct Pallet(pallet_ajuna_nft_staking::Pallet);
+pub trait Config: NftStakingConfig + pallet_nfts::Config + pallet_balances::Config {}
+
+type AccountIdOf = ::AccountId;
+type CurrencyOf = ::Currency;
+type BalanceOf = as Currency>>::Balance;
+type CollectionIdOf = ::CollectionId;
+type ItemIdOf = ::ItemId;
+type ContractOf = Contract<
+ BalanceOf,
+ CollectionIdOf,
+ ::ItemId,
+ BlockNumberFor,
+ ::KeyLimit,
+ ::ValueLimit,
+>;
+
+type NftCurrencyOf = ::Currency;
+type NftBalanceOf = as Currency>>::Balance;
+type NftCollectionIdOf = ::CollectionId;
+type CollectionDeposit = ::CollectionDeposit;
+type ItemDeposit = ::ItemDeposit;
+type CollectionConfigOf =
+ pallet_nfts::CollectionConfig, BlockNumberFor, NftCollectionIdOf>;
+
+fn account(name: &'static str) -> T::AccountId {
+ let account = frame_benchmarking::account(name, Default::default(), Default::default());
+ CurrencyOf::::make_free_balance_be(&account, 999_999_999_u64.unique_saturated_into());
+ account
+}
+
+fn assert_last_event(avatars_event: Event) {
+ let event = ::RuntimeEvent::from(avatars_event);
+ frame_system::Pallet::::assert_last_event(event.into());
+}
+
+fn create_creator(reward_item: Option>) -> Result {
+ let creator = account::("creator");
+ create_contract_collection::(&creator)?; // reserve CONTRACT_COLLECTION
+ create_collections::(&creator, 1)?; // reserve REWARD_COLLECTION
+ if let Some(item_ids) = reward_item {
+ item_ids
+ .into_iter()
+ .try_for_each(|item_id| mint_item::(&creator, REWARD_COLLECTION, item_id))?;
+ }
+ Creator::::put(&creator);
+ Ok(creator)
+}
+
+fn create_contract_collection(creator: &T::AccountId) -> DispatchResult {
+ create_collection::(creator)?;
+ ContractCollectionId::::put(CollectionIdOf::::from(CONTRACT_COLLECTION));
+ Ok(())
+}
+
+fn create_collections(creator: &T::AccountId, n: usize) -> DispatchResult {
+ (0..n).try_for_each(|_| create_collection::(creator))
+}
+
+fn create_collection(owner: &T::AccountId) -> DispatchResult {
+ NftCurrencyOf::::deposit_creating(owner, CollectionDeposit::::get());
+ as Create>>::create_collection(
+ owner,
+ owner,
+ &pallet_nfts::CollectionConfig {
+ settings: Default::default(),
+ max_supply: Default::default(),
+ mint_settings: Default::default(),
+ },
+ )?;
+ Ok(())
+}
+
+fn create_contract(
+ creator: T::AccountId,
+ contract_id: ItemIdOf,
+ contract: ContractOf,
+) -> DispatchResult {
+ pallet_ajuna_nft_staking::Pallet::::create(
+ RawOrigin::Signed(creator).into(),
+ contract_id,
+ contract,
+ None,
+ None,
+ )
+}
+
+fn accept_contract(
+ num_stake_clauses: u32,
+ num_fee_clauses: u32,
+ staker: T::AccountId,
+ contract_id: ItemIdOf,
+ mode: Mode,
+) -> DispatchResult {
+ let (stakes, fees) = stakes_and_fees::(num_stake_clauses, num_fee_clauses, &staker, mode)?;
+ pallet_ajuna_nft_staking::Pallet::::accept(
+ RawOrigin::Signed(staker).into(),
+ contract_id,
+ stakes,
+ fees,
+ )
+}
+
+fn mint_item(owner: &T::AccountId, collection_id: u16, item_id: u16) -> DispatchResult {
+ let collection_id = &T::Helper::collection(collection_id);
+ let item_id = &T::Helper::item(item_id);
+ NftCurrencyOf::::deposit_creating(owner, ItemDeposit::::get());
+ as Mutate>::mint_into(
+ collection_id,
+ item_id,
+ owner,
+ &ItemConfig::default(),
+ false,
+ )?;
+ Ok(())
+}
+
+fn set_attribute(
+ collection_id: u16,
+ item_id: u16,
+ key: u8,
+ value: u8,
+) -> DispatchResult {
+ let collection_id = &T::Helper::collection(collection_id);
+ let item_id = &T::Helper::item(item_id);
+ as Mutate>::set_attribute(
+ collection_id,
+ item_id,
+ &[key],
+ &[value],
+ )?;
+ Ok(())
+}
+
+fn stakes_and_fees(
+ num_stake_clauses: u32,
+ num_fee_clauses: u32,
+ who: &T::AccountId,
+ mode: Mode,
+) -> Result<(Vec>, Vec>), DispatchError> {
+ let (stake_collection, fee_collection) = mode.collections();
+ let mut stakes = Vec::new();
+ let mut fees = Vec::new();
+ for i in 0..num_stake_clauses {
+ let item_id = i as u16;
+ let attr_key = i;
+ mint_item::(who, stake_collection, item_id)?;
+ set_attribute::(stake_collection, item_id, (attr_key as u8) * 3, ATTRIBUTE_VALUE)?;
+ stakes.push(NftId(
+ CollectionIdOf::::unique_saturated_from(stake_collection),
+ T::BenchmarkHelper::item_id(item_id),
+ ));
+ }
+ for i in num_stake_clauses..num_stake_clauses + num_fee_clauses {
+ let item_id = i as u16;
+ let attr_key = i;
+ mint_item::(who, fee_collection, item_id)?;
+ set_attribute::(fee_collection, item_id, (attr_key as u8) * 3, ATTRIBUTE_VALUE)?;
+ fees.push(NftId(
+ CollectionIdOf::::unique_saturated_from(fee_collection),
+ T::BenchmarkHelper::item_id(item_id),
+ ));
+ }
+ Ok((stakes, fees))
+}
+
+fn contract_with(
+ num_stake_clauses: u32,
+ num_fee_clauses: u32,
+ rewards: BoundedRewardsOf,
+ mode: Mode,
+) -> ContractOf {
+ let (stake_collection, fee_collection) = mode.collections();
+ ContractOf:: {
+ activation: None,
+ active_duration: 1_u32.unique_saturated_into(),
+ claim_duration: 1_u32.unique_saturated_into(),
+ stake_duration: 1_u32.unique_saturated_into(),
+ stake_clauses: (0..num_stake_clauses)
+ .map(|i| ContractClause {
+ namespace: AttributeNamespace::Pallet,
+ target_index: i as u8,
+ clause: Clause::HasAttributeWithValue(
+ CollectionIdOf::::unique_saturated_from(stake_collection),
+ T::BenchmarkHelper::contract_key((i as u8) * 3),
+ AttributeValue::Equal(T::BenchmarkHelper::contract_value(ATTRIBUTE_VALUE)),
+ ),
+ })
+ .collect::>()
+ .try_into()
+ .unwrap(),
+ fee_clauses: (num_stake_clauses..num_stake_clauses + num_fee_clauses)
+ .map(|i| ContractClause {
+ namespace: AttributeNamespace::Pallet,
+ target_index: (i - num_stake_clauses) as u8,
+ clause: Clause::HasAttributeWithValue(
+ CollectionIdOf::::unique_saturated_from(fee_collection),
+ T::BenchmarkHelper::contract_key((i as u8) * 3),
+ AttributeValue::Equal(T::BenchmarkHelper::contract_value(ATTRIBUTE_VALUE)),
+ ),
+ })
+ .collect::>()
+ .try_into()
+ .unwrap(),
+ rewards,
+ cancel_fee: 333_u64.unique_saturated_into(),
+ nft_stake_amount: num_stake_clauses as u8,
+ nft_fee_amount: num_fee_clauses as u8,
+ is_snipeable: true,
+ }
+}
+
+benchmarks! {
+ set_creator {
+ let creator = account::("creator");
+ }: _(RawOrigin::Root, creator.clone())
+ verify {
+ assert_last_event::(Event::CreatorSet { creator })
+ }
+
+ set_contract_collection_id {
+ let creator = create_creator::(None)?;
+ let collection_id = CollectionIdOf::::unique_saturated_from(0_u32);
+ ContractCollectionId::::kill();
+ }: _(RawOrigin::Signed(creator), collection_id)
+ verify {
+ assert_last_event::(Event::ContractCollectionSet { collection_id })
+ }
+
+ set_global_config {
+ let creator = create_creator::(None)?;
+ let new_config = GlobalConfig::default();
+ }: _(RawOrigin::Signed(creator), new_config)
+ verify {
+ assert_last_event::(Event::SetGlobalConfig { new_config })
+ }
+
+ create_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let creator = create_creator::(None)?;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+ }: create(RawOrigin::Signed(creator), contract_id, contract, None, None)
+ verify {
+ assert_last_event::(Event::Created { contract_id })
+ }
+
+ create_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 123_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+ let creator = create_creator::(Some(vec![reward_nft_item]))?;
+ }: create(RawOrigin::Signed(creator), contract_id, contract, None, None)
+ verify {
+ assert_last_event::(Event::Created { contract_id })
+ }
+
+ remove_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+ let creator = create_creator::(None)?;
+ create_contract::(creator.clone(), contract_id, contract)?;
+ }: remove(RawOrigin::Signed(creator), contract_id)
+ verify {
+ assert_last_event::(Event::Removed { contract_id })
+ }
+
+ remove_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 2_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+ let creator = create_creator::(Some(vec![reward_nft_item]))?;
+ create_contract::(creator.clone(), contract_id, contract)?;
+ }: remove(RawOrigin::Signed(creator), contract_id)
+ verify {
+ assert_last_event::(Event::Removed { contract_id })
+ }
+
+ accept_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(None)?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ let (stakes, fees) = stakes_and_fees::(m, n, &by, Mode::Staker)?;
+ }: accept(RawOrigin::Signed(by.clone()), contract_id, stakes, fees)
+ verify {
+ assert_last_event::(Event::Accepted { by, contract_id })
+ }
+
+ accept_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 2_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(
+ NftId(REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let contract = contract_with::(m, n, rewards, Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(Some(vec![reward_nft_item]))?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ let (stakes, fees) = stakes_and_fees::(m, n, &by, Mode::Staker)?;
+ }: accept(RawOrigin::Signed(by.clone()), contract_id, stakes, fees)
+ verify {
+ assert_last_event::(Event::Accepted { by, contract_id })
+ }
+
+ cancel_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let mut contract = contract_with::(m, n, rewards, Mode::Staker);
+ contract.stake_duration = 100_u32.unique_saturated_into();
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(None)?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by.clone(), contract_id, Mode::Staker)?;
+ }: cancel(RawOrigin::Signed(by.clone()), contract_id)
+ verify {
+ assert_last_event::(Event::Cancelled { by, contract_id })
+ }
+
+ cancel_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 2_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let mut contract = contract_with::(m, n, rewards, Mode::Staker);
+ contract.stake_duration = 100_u32.unique_saturated_into();
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(Some(vec![reward_nft_item]))?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by.clone(), contract_id, Mode::Staker)?;
+ }: cancel(RawOrigin::Signed(by.clone()), contract_id)
+ verify {
+ assert_last_event::(Event::Cancelled { by, contract_id })
+ }
+
+ claim_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let contract = contract_with::(m, n, rewards.clone(), Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(None)?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by.clone(), contract_id, Mode::Staker)?;
+ }: claim(RawOrigin::Signed(by.clone()), contract_id, None)
+ verify {
+ assert_last_event::(Event::Claimed { by, contract_id, rewards })
+ }
+
+ claim_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 2_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let contract = contract_with::(m, n, rewards.clone(), Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let creator = create_creator::(Some(vec![reward_nft_item]))?;
+ create_contract::(creator, contract_id, contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by.clone(), contract_id, Mode::Staker)?;
+ }: claim(RawOrigin::Signed(by.clone()), contract_id, None)
+ verify {
+ assert_last_event::(Event::Claimed { by, contract_id, rewards })
+ }
+
+ snipe_token_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Tokens(123_u64.unique_saturated_into())];
+ let contract = contract_with::(m, n, rewards.clone(), Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let mut sniper_contract = contract_with::(m, n, rewards.clone(), Mode::Sniper);
+ sniper_contract.stake_duration = 100_u32.unique_saturated_into();
+ let sniper_contract_id = T::BenchmarkHelper::item_id(1_u16);
+
+ let creator = create_creator::(None)?;
+ create_contract::(creator.clone(), contract_id, contract.clone())?;
+ create_contract::(creator, sniper_contract_id, sniper_contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by, contract_id, Mode::Staker)?;
+
+ let sniper = account::("sniper");
+ create_collections::(&sniper, 2)?;
+ accept_contract::(m, n, sniper.clone(), sniper_contract_id, Mode::Sniper)?;
+
+ // Advance block past contract expiry.
+ frame_system::Pallet::::set_block_number(
+ contract.stake_duration + contract.claim_duration + One::one()
+ );
+ }: snipe(RawOrigin::Signed(sniper.clone()), contract_id)
+ verify {
+ assert_last_event::(Event::Sniped { by: sniper, contract_id, rewards })
+ }
+
+ snipe_nft_reward {
+ let m in 0..T::MaxStakingClauses::get();
+ let n in 0..T::MaxFeeClauses::get();
+ let reward_nft_item = 2_u16;
+ let rewards: BoundedRewardsOf = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(reward_nft_item),
+ ))];
+ let contract = contract_with::(m, n, rewards.clone(), Mode::Staker);
+ let contract_id = T::BenchmarkHelper::item_id(0_u16);
+
+ let sniper_reward_nft_item = 123_u16;
+ let sniper_rewards = bounded_vec![Reward::Nft(NftId(
+ REWARD_COLLECTION.unique_saturated_into(),
+ T::BenchmarkHelper::item_id(sniper_reward_nft_item),
+ ))];
+ let mut sniper_contract = contract_with::(m, n, sniper_rewards, Mode::Sniper);
+ sniper_contract.stake_duration = 100_u32.unique_saturated_into();
+ let sniper_contract_id = T::BenchmarkHelper::item_id(1_u16);
+
+ let creator = create_creator::(Some(vec![reward_nft_item, sniper_reward_nft_item]))?;
+ create_contract::(creator.clone(), contract_id, contract.clone())?;
+ create_contract::(creator, sniper_contract_id, sniper_contract)?;
+
+ let by = account::("staker");
+ create_collections::(&by, 2)?;
+ accept_contract::(m, n, by, contract_id, Mode::Staker)?;
+
+ let sniper = account::("sniper");
+ create_collections::(&sniper, 2)?;
+ accept_contract::(m, n, sniper.clone(), sniper_contract_id, Mode::Sniper)?;
+
+ // Advance block past contract expiry.
+ frame_system::Pallet::::set_block_number(
+ contract.stake_duration + contract.claim_duration + One::one()
+ );
+ }: snipe(RawOrigin::Signed(sniper.clone()), contract_id)
+ verify {
+ assert_last_event::(Event::Sniped { by: sniper, contract_id, rewards })
+ }
+
+ impl_benchmark_test_suite!(
+ Pallet,
+ crate::mock::new_test_ext(),
+ crate::mock::Runtime
+ );
+}
diff --git a/pallets/tic-tac-toe/benchmarking/src/mock.rs b/pallets/tic-tac-toe/benchmarking/src/mock.rs
new file mode 100644
index 0000000..37215b7
--- /dev/null
+++ b/pallets/tic-tac-toe/benchmarking/src/mock.rs
@@ -0,0 +1,201 @@
+// Ajuna Node
+// Copyright (C) 2022 BlogaTech AG
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#![cfg(test)]
+
+use frame_support::{parameter_types, traits::AsEnsureOriginWithArg, PalletId};
+use frame_system::{EnsureRoot, EnsureSigned};
+use pallet_nfts::PalletFeatures;
+use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+use sp_core::Get;
+use sp_runtime::{
+ testing::H256,
+ traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify},
+ BuildStorage, MultiSignature,
+};
+
+pub type MockSignature = MultiSignature;
+pub type MockAccountPublic = ::Signer;
+pub type MockAccountId = ::AccountId;
+pub type MockBlock = frame_system::mocking::MockBlock;
+pub type MockBalance = u64;
+pub type MockCollectionId = u32;
+
+impl crate::Config for Runtime {}
+
+// Configure a mock runtime to test the pallet.
+frame_support::construct_runtime!(
+ pub struct Runtime {
+ System: frame_system,
+ Balances: pallet_balances,
+ Nft: pallet_nfts,
+ NftStake: pallet_ajuna_nft_staking,
+ }
+);
+
+impl frame_system::Config for Runtime {
+ type BaseCallFilter = frame_support::traits::Everything;
+ type BlockWeights = ();
+ type BlockLength = ();
+ type RuntimeOrigin = RuntimeOrigin;
+ type RuntimeCall = RuntimeCall;
+ type Hash = H256;
+ type Hashing = BlakeTwo256;
+ type AccountId = MockAccountId;
+ type Lookup = IdentityLookup;
+ type RuntimeEvent = RuntimeEvent;
+ type BlockHashCount = frame_support::traits::ConstU64<250>;
+ type DbWeight = ();
+ type Version = ();
+ type PalletInfo = PalletInfo;
+ type AccountData = pallet_balances::AccountData;
+ type OnNewAccount = ();
+ type OnKilledAccount = ();
+ type SystemWeightInfo = ();
+ type SS58Prefix = frame_support::traits::ConstU16<42>;
+ type OnSetCode = ();
+ type MaxConsumers = frame_support::traits::ConstU32<16>;
+ type Nonce = u32;
+ type Block = MockBlock;
+}
+
+parameter_types! {
+ pub const MockExistentialDeposit: MockBalance = 3;
+}
+
+impl pallet_balances::Config for Runtime {
+ type Balance = MockBalance;
+ type DustRemoval = ();
+ type RuntimeEvent = RuntimeEvent;
+ type ExistentialDeposit = MockExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ type MaxLocks = ();
+ type MaxReserves = ();
+ type ReserveIdentifier = [u8; 8];
+ type FreezeIdentifier = ();
+ type MaxHolds = ();
+ type MaxFreezes = ();
+ type RuntimeHoldReason = ();
+}
+
+parameter_types! {
+ pub const CollectionDeposit: MockBalance = 333;
+ pub const ItemDeposit: MockBalance = 33;
+ pub const MetadataDepositBase: MockBalance = 0;
+ pub const AttributeDepositBase: MockBalance = 0;
+ pub const DepositPerByte: MockBalance = 0;
+ pub const StringLimit: u32 = 128;
+ pub const ApprovalsLimit: u32 = 1;
+ pub const ItemAttributesApprovalsLimit: u32 = 10;
+ pub const MaxTips: u32 = 1;
+ pub const MaxDeadlineDuration: u32 = 1;
+ pub ConfigFeatures: PalletFeatures = PalletFeatures::all_enabled();
+}
+
+#[cfg(feature = "runtime-benchmarks")]
+pub struct Helper;
+#[cfg(feature = "runtime-benchmarks")]
+impl, ItemId: From<[u8; 32]>>
+ pallet_nfts::BenchmarkHelper for Helper
+{
+ fn collection(i: u16) -> CollectionId {
+ i.into()
+ }
+ fn item(i: u16) -> ItemId {
+ let mut id = [0_u8; 32];
+ let bytes = i.to_be_bytes();
+ id[0] = bytes[0];
+ id[1] = bytes[1];
+ id.into()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub struct ParameterGet;
+
+impl Get for ParameterGet {
+ fn get() -> u32 {
+ N
+ }
+}
+
+pub type KeyLimit = ParameterGet<32>;
+pub type ValueLimit = ParameterGet<64>;
+
+impl pallet_nfts::Config for Runtime {
+ type RuntimeEvent = RuntimeEvent;
+ type CollectionId = MockCollectionId;
+ type ItemId = H256;
+ type Currency = Balances;
+ type ForceOrigin = EnsureRoot;
+ type CreateOrigin = AsEnsureOriginWithArg>;
+ type Locker = ();
+ type CollectionDeposit = CollectionDeposit;
+ type ItemDeposit = ItemDeposit;
+ type MetadataDepositBase = MetadataDepositBase;
+ type AttributeDepositBase = AttributeDepositBase;
+ type DepositPerByte = DepositPerByte;
+ type StringLimit = StringLimit;
+ type KeyLimit = KeyLimit;
+ type ValueLimit = ValueLimit;
+ type ApprovalsLimit = ApprovalsLimit;
+ type ItemAttributesApprovalsLimit = ItemAttributesApprovalsLimit;
+ type MaxTips = MaxTips;
+ type MaxDeadlineDuration = MaxDeadlineDuration;
+ type MaxAttributesPerCall = frame_support::traits::ConstU32<2>;
+ type Features = ConfigFeatures;
+ type OffchainSignature = MockSignature;
+ type OffchainPublic = MockAccountPublic;
+ pallet_nfts::runtime_benchmarks_enabled! {
+ type Helper = Helper;
+ }
+ type WeightInfo = ();
+}
+
+parameter_types! {
+ pub const NftStakingPalletId: PalletId = PalletId(*b"aj/nftst");
+ pub const MaxContracts: u32 = 100;
+ pub const MaxStakingClauses: u32 = 10;
+ pub const MaxFeeClauses: u32 = 1;
+ pub const MaxMetadataLenght: u32 = 100;
+}
+
+impl pallet_ajuna_nft_staking::Config for Runtime {
+ type PalletId = NftStakingPalletId;
+ type RuntimeEvent = RuntimeEvent;
+ type Currency = Balances;
+ type CollectionId = MockCollectionId;
+ type ItemId = H256;
+ type ItemConfig = pallet_nfts::ItemConfig;
+ type NftHelper = Nft;
+ type MaxContracts = MaxContracts;
+ type MaxStakingClauses = MaxStakingClauses;
+ type MaxFeeClauses = MaxFeeClauses;
+ type MaxMetadataLength = MaxMetadataLenght;
+ type KeyLimit = KeyLimit;
+ type ValueLimit = ValueLimit;
+ pallet_ajuna_nft_staking::runtime_benchmarks_enabled! {
+ type BenchmarkHelper = ();
+ }
+ type WeightInfo = ();
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ let t = frame_system::GenesisConfig::::default().build_storage().unwrap();
+ sp_io::TestExternalities::new(t)
+}
diff --git a/pallets/tic-tac-toe/src/game.rs b/pallets/tic-tac-toe/src/game.rs
new file mode 100644
index 0000000..c1b4a1e
--- /dev/null
+++ b/pallets/tic-tac-toe/src/game.rs
@@ -0,0 +1,254 @@
+use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+
+pub type GameId = u32;
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub enum Position {
+ #[default]
+ One,
+ Two,
+ Three,
+}
+
+impl From for usize {
+ fn from(value: Position) -> Self {
+ match value {
+ Position::One => 0,
+ Position::Two => 1,
+ Position::Three => 2,
+ }
+ }
+}
+
+impl From for Position {
+ fn from(value: usize) -> Self {
+ match value {
+ 0 => Position::One,
+ 1 => Position::Two,
+ 2 => Position::Three,
+ _ => Position::One,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub struct Coordinates {
+ row: Position,
+ col: Position,
+}
+
+impl From for (usize, usize) {
+ fn from(value: Coordinates) -> Self {
+ (value.row.into(), value.col.into())
+ }
+}
+
+impl From<(usize, usize)> for Coordinates {
+ fn from(value: (usize, usize)) -> Self {
+ Self { row: Position::from(value.0), col: Position::from(value.1) }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub enum CellState {
+ Circle,
+ Cross,
+ #[default]
+ Empty,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub enum CurrentTurn {
+ #[default]
+ PlayerOne,
+ PlayerTwo,
+}
+
+impl CurrentTurn {
+ fn next(&self) -> Self {
+ match self {
+ CurrentTurn::PlayerOne => CurrentTurn::PlayerTwo,
+ CurrentTurn::PlayerTwo => CurrentTurn::PlayerOne,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub enum BoardState {
+ MissingPlayer(AccountId),
+ Playing(AccountId, AccountId, CurrentTurn),
+ Finished(AccountId, AccountId),
+}
+
+impl BoardState
+where
+ AccountId: Clone,
+{
+ fn next_turn(&mut self) {
+ if let BoardState::Playing(_, _, turn) = self {
+ *turn = turn.next();
+ };
+ }
+}
+
+impl BoardState
+where
+ AccountId: Clone,
+{
+ fn to_playing(&self, other: AccountId) -> Option {
+ if let BoardState::MissingPlayer(player) = self {
+ Some(BoardState::Playing(player.clone(), other, CurrentTurn::PlayerOne))
+ } else {
+ None
+ }
+ }
+
+ fn to_finished(&self) -> Option {
+ if let BoardState::Playing(p1, p2, _) = self {
+ Some(BoardState::Finished(p1.clone(), p2.clone()))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub enum PlayResult {
+ Winner(AccountId),
+ Draw,
+ InvalidTurn,
+ InvalidCell,
+ GamePending,
+ GameAlreadyFinished,
+ Continue,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo)]
+pub struct Board {
+ pub(crate) cells: [[CellState; 3]; 3],
+ pub(crate) state: BoardState,
+ pub(crate) moves_played: u8,
+}
+
+impl Board
+where
+ AccountId: Clone,
+{
+ pub(crate) fn new(creator: AccountId) -> Self {
+ Self {
+ cells: [[CellState::default(); 3]; 3],
+ state: BoardState::MissingPlayer(creator),
+ moves_played: 0,
+ }
+ }
+
+ pub(crate) fn get_state(&self) -> BoardState {
+ self.state.clone()
+ }
+
+ pub(crate) fn start_game(&mut self, rival: AccountId)
+ where
+ AccountId: Clone,
+ {
+ self.state = self.state.to_playing(rival).unwrap();
+ }
+
+ pub(crate) fn play_turn(
+ &mut self,
+ player: &AccountId,
+ coordinates: Coordinates,
+ ) -> PlayResult
+ where
+ AccountId: PartialEq + Clone,
+ {
+ match self.state {
+ BoardState::MissingPlayer(_) => PlayResult::GamePending,
+ BoardState::Playing(ref p1, ref p2, turn) => match turn {
+ CurrentTurn::PlayerOne =>
+ if player == p1 {
+ self.execute_move(player, CellState::Circle, coordinates)
+ } else {
+ PlayResult::InvalidTurn
+ },
+ CurrentTurn::PlayerTwo =>
+ if player == p2 {
+ self.execute_move(player, CellState::Cross, coordinates)
+ } else {
+ PlayResult::InvalidTurn
+ },
+ },
+ BoardState::Finished(_, _) => PlayResult::GameAlreadyFinished,
+ }
+ }
+
+ fn execute_move(
+ &mut self,
+ player: &AccountId,
+ cell_type: CellState,
+ coordinates: Coordinates,
+ ) -> PlayResult
+ where
+ AccountId: PartialEq + Clone,
+ {
+ let (row, col) = coordinates.into();
+
+ if self.cells[col][row] == CellState::Empty {
+ self.cells[col][row] = cell_type;
+ self.moves_played += 1;
+ } else {
+ return PlayResult::InvalidCell
+ }
+
+ if self.has_player_won(cell_type) {
+ self.finish_game();
+ PlayResult::Winner(player.clone())
+ } else if self.moves_played == 9 {
+ self.finish_game();
+ PlayResult::Draw
+ } else {
+ self.state.next_turn();
+ PlayResult::Continue
+ }
+ }
+
+ fn has_player_won(&self, cell_type: CellState) -> bool {
+ let mut results = self.check_row(0, cell_type) ||
+ self.check_row(1, cell_type) ||
+ self.check_row(2, cell_type);
+
+ results = results ||
+ self.check_col(0, cell_type) ||
+ self.check_col(1, cell_type) ||
+ self.check_col(2, cell_type);
+
+ results = results || self.check_diagonals(cell_type);
+
+ results
+ }
+
+ fn check_row(&self, row: usize, cell_type: CellState) -> bool {
+ self.cells[0][row] == cell_type &&
+ self.cells[1][row] == cell_type &&
+ self.cells[2][row] == cell_type
+ }
+
+ fn check_col(&self, col: usize, cell_type: CellState) -> bool {
+ self.cells[col][0] == cell_type &&
+ self.cells[col][1] == cell_type &&
+ self.cells[col][2] == cell_type
+ }
+
+ fn check_diagonals(&self, cell_type: CellState) -> bool {
+ (self.cells[0][0] == cell_type &&
+ self.cells[1][1] == cell_type &&
+ self.cells[2][2] == cell_type) ||
+ (self.cells[2][0] == cell_type &&
+ self.cells[1][1] == cell_type &&
+ self.cells[0][2] == cell_type)
+ }
+
+ fn finish_game(&mut self) {
+ self.state = self.state.to_finished().unwrap();
+ }
+}
diff --git a/pallets/tic-tac-toe/src/lib.rs b/pallets/tic-tac-toe/src/lib.rs
new file mode 100644
index 0000000..e45f1b8
--- /dev/null
+++ b/pallets/tic-tac-toe/src/lib.rs
@@ -0,0 +1,231 @@
+// Ajuna Node
+// Copyright (C) 2022 BlogaTech AG
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+#[cfg(test)]
+mod test;
+
+mod game;
+
+use frame_support::pallet_prelude::*;
+use frame_system::pallet_prelude::*;
+use sp_std::prelude::*;
+
+pub(crate) use game::*;
+
+pub use pallet::*;
+
+#[frame_support::pallet]
+pub mod pallet {
+ use super::*;
+
+ pub type BoardOf = Board<::AccountId>;
+
+ #[pallet::pallet]
+ pub struct Pallet(_);
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+ }
+
+ #[pallet::storage]
+ pub type ActivePlayers = StorageMap<_, Identity, T::AccountId, GameId>;
+
+ #[pallet::storage]
+ pub type PendingGames = StorageMap<_, Identity, GameId, BoardOf>;
+
+ #[pallet::storage]
+ pub type ActiveGames = StorageMap<_, Identity, GameId, BoardOf>;
+
+ #[pallet::storage]
+ pub type NextGameId = StorageValue<_, GameId, ValueQuery>;
+
+ #[pallet::storage]
+ pub type LastActiveGameId = StorageValue<_, GameId, ValueQuery>;
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event {
+ /// A new game has been created.
+ GameCreated { game_id: GameId, by: T::AccountId },
+ /// A player has joined a pending game.
+ GameJoined { game_id: GameId, by: T::AccountId },
+ /// A player has executed a move.
+ MovePlayed { at: Coordinates, by: T::AccountId },
+ /// A game has finished, if draw then no winner will be shown.
+ GameFinished { game_id: GameId, winner: Option },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// A player that is involved in an active game cannot create new ones.
+ CannotCreateNewGame,
+ /// A player that is involved in an active game cannot join another one.
+ CannotJoinAnotherGame,
+ /// The player is nt currently involved in any game.
+ PlayerNotActive,
+ /// There are no pending games to join, please create one.
+ NoPendingGamesFound,
+ /// Tried to play while not in their turn
+ InvalidTurn,
+ /// Tried to play on an already filled cell
+ InvalidCell,
+ /// The game is in an incorrect state
+ InvalidGameState,
+ /// The game needs and additional player to start
+ GamePending,
+ /// The game is already over
+ GameAlreadyFinished,
+ }
+
+ #[pallet::call]
+ impl Pallet {
+ #[pallet::call_index(0)]
+ #[pallet::weight({10_000})]
+ pub fn create_game(origin: OriginFor) -> DispatchResult {
+ let account = ensure_signed(origin)?;
+
+ ensure!(!ActivePlayers::::contains_key(&account), Error::::CannotCreateNewGame);
+
+ let game_id = Self::next_game_id();
+ let board = BoardOf::::new(account.clone());
+
+ ActivePlayers::::insert(account.clone(), game_id);
+ PendingGames::::insert(game_id, board);
+
+ Self::deposit_event(Event::GameCreated { game_id, by: account });
+
+ Ok(())
+ }
+
+ #[pallet::call_index(1)]
+ #[pallet::weight({10_000})]
+ pub fn join_pending_game(origin: OriginFor) -> DispatchResult {
+ let account = ensure_signed(origin)?;
+ ensure!(!ActivePlayers::::contains_key(&account), Error::::CannotJoinAnotherGame);
+
+ let game_id = LastActiveGameId::::get();
+ ensure!(PendingGames::