diff --git a/.github/workflows/contracts-tests.yml b/.github/workflows/contracts-tests.yml index a6ccbc59d..d9a60eaeb 100644 --- a/.github/workflows/contracts-tests.yml +++ b/.github/workflows/contracts-tests.yml @@ -43,6 +43,9 @@ jobs: with: toolchain: stable targets: wasm32-unknown-unknown + + - name: Install rust-src component + run: rustup component add rust-src --toolchain stable-x86_64-unknown-linux-gnu - name: Prepare Gear Binary run: | diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index d338520eb..3525807a5 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -5,7 +5,7 @@ on: env: CARGO_TERM_COLOR: always - DEFAULT_TOOLCHAIN: 1.78.0 + DEFAULT_TOOLCHAIN: 1.82.0 defaults: run: @@ -30,6 +30,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install rust-src component + run: rustup component add rust-src --toolchain stable-x86_64-unknown-linux-gnu + - name: Build run: 'cargo build --release --workspace;' diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index b8b014a5d..a48b2db3f 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -18,7 +18,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c475258372feefa7fa4307b6cb4006b63108fe3ee36a1afe07f96c4d2ff1d14" dependencies = [ - "derive_more", + "derive_more 0.99.18", ] [[package]] @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -135,43 +135,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "ark-bls12-381" @@ -547,7 +547,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -619,6 +619,39 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "battle" +version = "1.1.0" +dependencies = [ + "battle", + "battle-app", + "battle-client", + "gstd", + "gtest", + "sails-idl-gen", + "sails-rs", + "tokio", +] + +[[package]] +name = "battle-app" +version = "0.1.0" +dependencies = [ + "gstd", + "sails-rs", +] + +[[package]] +name = "battle-client" +version = "1.1.0" +dependencies = [ + "battle-app", + "mockall", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", +] + [[package]] name = "battleship" version = "1.1.0" @@ -905,9 +938,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "camino" @@ -1042,9 +1075,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.30" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -1122,9 +1155,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" @@ -1146,6 +1179,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "concert" +version = "1.1.0" +dependencies = [ + "concert-app", + "gear-wasm-builder", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", +] + +[[package]] +name = "concert-app" +version = "1.1.0" +dependencies = [ + "concert", + "extended_vmt_wasm", + "gclient", + "gstd", + "sails-rs", + "tokio", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1251,7 +1307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3623e2da514b4cb37c33e15831b7b348fa692f9446b84662c5bbd2a9a9205716" dependencies = [ "actor-system-error", - "derive_more", + "derive_more 0.99.18", "gear-core", "gear-core-backend", "gear-core-errors", @@ -1528,7 +1584,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1589,7 +1645,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1611,7 +1667,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1674,7 +1730,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1687,7 +1743,27 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.79", + "syn 2.0.85", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", ] [[package]] @@ -1741,6 +1817,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1944,7 +2026,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1965,7 +2047,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2081,13 +2163,13 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "extended-vft-app" version = "0.1.0" -source = "git+https://github.com/gear-foundation/standards/#a7864f9be28b3c7b154e5ceb4f523cfe1749ad86" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" dependencies = [ "gear-wasm-builder", "gstd", @@ -2098,10 +2180,24 @@ dependencies = [ "vft-service", ] +[[package]] +name = "extended-vmt-app" +version = "0.1.0" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" +dependencies = [ + "gear-wasm-builder", + "gstd", + "log", + "parity-scale-codec", + "sails-rs", + "scale-info", + "vmt-service", +] + [[package]] name = "extended_vft_wasm" version = "0.1.0" -source = "git+https://github.com/gear-foundation/standards/#a7864f9be28b3c7b154e5ceb4f523cfe1749ad86" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" dependencies = [ "extended-vft-app", "sails-client-gen", @@ -2109,6 +2205,17 @@ dependencies = [ "sails-rs", ] +[[package]] +name = "extended_vmt_wasm" +version = "0.1.0" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" +dependencies = [ + "extended-vmt-app", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -2202,6 +2309,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "frame-metadata" version = "15.1.0" @@ -2276,7 +2389,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2289,7 +2402,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2300,7 +2413,7 @@ checksum = "0c3562da4b7b8e24189036c58d459b260e10c8b7af2dd180b7869ae29bb66277" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2408,7 +2521,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2496,7 +2609,7 @@ dependencies = [ "ark-ff", "ark-scale", "ark-serialize", - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", ] @@ -2540,7 +2653,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb4e1640dc870a14e76e94ff05bb802b3b23f785de4832ab6de18101589f1d20" dependencies = [ - "derive_more", + "derive_more 0.99.18", "enum-iterator 1.5.0", "frame-support", "frame-system", @@ -2564,7 +2677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6c7b3dc1307f835f34a958a85e38733dea9ac38c1c7eaf91bf4191c246604e" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2575,7 +2688,7 @@ checksum = "7c16860963f2d96fab0447d166bb1aaca7c375f4449e320d89a127ebf66a2e40" dependencies = [ "blake2", "byteorder", - "derive_more", + "derive_more 0.99.18", "enum-iterator 1.5.0", "gear-core-errors", "gear-wasm-instrument", @@ -2604,7 +2717,7 @@ checksum = "3d78854668c11ea77a4ea23897deb881a6f59721eb22ccdc01915513e6080be7" dependencies = [ "actor-system-error", "blake2", - "derive_more", + "derive_more 0.99.18", "gear-core", "gear-core-errors", "gear-lazy-pages-common", @@ -2622,7 +2735,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd8895323fe7eccfdd7ddee3263c4060e2aab0607b25f224aa7dcfc6779ff01" dependencies = [ - "derive_more", + "derive_more 0.99.18", "enum-iterator 1.5.0", "scale-info", "serde", @@ -2648,7 +2761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a40e337bbdf13d81e515dbff31943bf516bec1e7d2d9482e804b7e63553e3e4" dependencies = [ "cfg-if", - "derive_more", + "derive_more 0.99.18", "errno", "gear-core", "gear-lazy-pages-common", @@ -2841,7 +2954,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0e1fabcfb4c8d9f1c97aa9125963b05d087af8d86e51ab116b557f03b008f7" dependencies = [ - "derive_more", + "derive_more 0.99.18", "enum-iterator 1.5.0", "gwasm-instrument", ] @@ -2882,7 +2995,7 @@ checksum = "553630feadf7b76442b0849fd25fdf89b860d933623aec9693fed19af0400c78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2979,7 +3092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e23628646340b128c4576e55aff9240900219a1f9a5d58a209f93cc68610940" dependencies = [ "blake2", - "derive_more", + "derive_more 0.99.18", "gmeta-codegen", "hex", "scale-info", @@ -2993,7 +3106,7 @@ checksum = "aa7e53b4c825fef578e8342784d1c45f8f83ece78f4b6090ff0f27a118227083" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3037,7 +3150,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3062,7 +3175,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2feb2d9faa9b033d630e92ead8fb291c361281e8566427eab652d4d239a18bc9" dependencies = [ - "derive_more", + "derive_more 0.99.18", "gear-ss58", "hex", "parity-scale-codec", @@ -3121,7 +3234,7 @@ checksum = "6a607026ef46bf04c1a5aa997a50f5e389a062af59ad20caf16d4afa470cede4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3153,7 +3266,7 @@ dependencies = [ "gprimitives", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3171,7 +3284,7 @@ dependencies = [ "cargo_toml", "colored", "core-processor", - "derive_more", + "derive_more 0.99.18", "env_logger 0.10.2", "etc", "gear-common", @@ -4182,7 +4295,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4230,7 +4343,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4244,7 +4357,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4255,7 +4368,7 @@ checksum = "d710e1214dffbab3b5dacb21475dde7d6ed84c69ff722b3a47a782668d44fbac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4266,7 +4379,7 @@ checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4404,6 +4517,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "more-asserts" version = "0.2.2" @@ -4569,7 +4709,7 @@ checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4578,7 +4718,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b4f8406a6dd2971f5f056be7297cc3b3f3105e9d5d54c3a69dfe411e74e643" dependencies = [ - "derive_more", + "derive_more 0.99.18", "num-traits", "parity-scale-codec", "scale-info", @@ -4844,7 +4984,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4879,29 +5019,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -4957,6 +5097,14 @@ dependencies = [ "spki", ] +[[package]] +name = "player-app" +version = "1.1.0" +dependencies = [ + "gstd", + "sails-rs", +] + [[package]] name = "polling" version = "3.7.3" @@ -4998,14 +5146,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -5072,14 +5246,14 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -5272,7 +5446,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -5289,9 +5463,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -5702,7 +5876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" dependencies = [ "byteorder", - "derive_more", + "derive_more 0.99.18", "twox-hash", ] @@ -5773,7 +5947,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -5791,6 +5965,7 @@ dependencies = [ "gtest", "hashbrown 0.14.5", "hex", + "mockall", "parity-scale-codec", "sails-macros", "scale-info", @@ -5825,7 +6000,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e98f3262c250d90e700bb802eb704e1f841e03331c2eb815e46516c4edbf5b27" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "primitive-types", "scale-bits", @@ -5848,11 +6023,11 @@ dependencies = [ [[package]] name = "scale-encode" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba0b9c48dc0eb20c60b083c29447c0c4617cb7c4a4c9fef72aa5c5bc539e15e" +checksum = "528464e6ae6c8f98e2b79633bf79ef939552e795e316579dab09c61670d56602" dependencies = [ - "derive_more", + "derive_more 0.99.18", "parity-scale-codec", "primitive-types", "scale-bits", @@ -5863,26 +6038,26 @@ dependencies = [ [[package]] name = "scale-encode-derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ab7e60e2d9c8d47105f44527b26f04418e5e624ffc034f6b4a86c0ba19c5bf" +checksum = "bef2618f123c88da9cd8853b69d766068f1eddc7692146d7dfe9b89e25ce2efd" dependencies = [ - "darling 0.14.4", - "proc-macro-crate 1.3.1", + "darling 0.20.10", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "1aa7ffc1c0ef49b0452c6e2986abf2b07743320641ffd5fc63d552458e3b779b" dependencies = [ "bitvec", "cfg-if", - "derive_more", + "derive_more 1.0.0", "parity-scale-codec", "scale-info-derive", "serde", @@ -5890,14 +6065,14 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "46385cc24172cf615450267463f937c10072516359b3ff1cb24228a4a08bf951" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.85", ] [[package]] @@ -5919,7 +6094,7 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.79", + "syn 2.0.85", "thiserror", ] @@ -5931,7 +6106,7 @@ checksum = "8cd6ab090d823e75cfdb258aad5fe92e13f2af7d04b43a55d607d25fcc38c811" dependencies = [ "base58", "blake2", - "derive_more", + "derive_more 0.99.18", "either", "frame-metadata 15.1.0", "parity-scale-codec", @@ -6121,9 +6296,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -6150,20 +6325,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.129" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbcf9b78a125ee667ae19388837dd12294b858d101fdd393cb9d5501ef09eb2" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -6381,7 +6556,7 @@ dependencies = [ "bs58 0.5.1", "chacha20", "crossbeam-queue", - "derive_more", + "derive_more 0.99.18", "ed25519-zebra 4.0.3", "either", "event-listener 4.0.3", @@ -6431,7 +6606,7 @@ dependencies = [ "async-lock 3.4.0", "base64 0.21.7", "blake2-rfc", - "derive_more", + "derive_more 0.99.18", "either", "event-listener 4.0.3", "fnv", @@ -6531,7 +6706,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6631,7 +6806,7 @@ checksum = "8dc707d9f5bf155d584900783e328cb3dc79c950f898a18a8f24066f41f040a5" dependencies = [ "quote", "sp-core-hashing", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6656,7 +6831,7 @@ checksum = "f12dae7cf6c1e825d13ffd4ce16bd9309db7c539929d0302b4443ed451a9f4e5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6800,7 +6975,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -6943,7 +7118,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7138,16 +7313,16 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.79", + "syn 2.0.85", "thiserror", "tokio", ] [[package]] name = "subxt-core" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f41eb2e2eea6ed45649508cc735f92c27f1fcfb15229e75f8270ea73177345" +checksum = "3af3b36405538a36b424d229dc908d1396ceb0994c90825ce928709eac1a159a" dependencies = [ "base58", "blake2", @@ -7199,7 +7374,7 @@ dependencies = [ "quote", "scale-typegen", "subxt-codegen", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7228,9 +7403,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -7242,18 +7417,21 @@ name = "syndote" version = "1.1.0" dependencies = [ "gear-wasm-builder", - "gstd", - "gtest", - "syndote-io", - "syndote-player", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", + "syndote-app", ] [[package]] -name = "syndote-io" +name = "syndote-app" version = "1.1.0" dependencies = [ - "gmeta", + "gclient", "gstd", + "sails-rs", + "syndote", + "tokio", ] [[package]] @@ -7261,18 +7439,8 @@ name = "syndote-player" version = "1.1.0" dependencies = [ "gear-wasm-builder", - "gstd", - "syndote-io", - "syndote-player-io", -] - -[[package]] -name = "syndote-player-io" -version = "1.1.0" -dependencies = [ - "gmeta", - "gstd", - "syndote-io", + "player-app", + "sails-idl-gen", ] [[package]] @@ -7401,24 +7569,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7519,9 +7693,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -7541,7 +7715,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7693,7 +7867,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -7937,6 +8111,15 @@ dependencies = [ "vara-man-app", ] +[[package]] +name = "varatube" +version = "1.1.0" +dependencies = [ + "gear-wasm-builder", + "sails-idl-gen", + "varatube-app", +] + [[package]] name = "varatube-app" version = "1.1.0" @@ -7949,15 +8132,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "varatube-wasm" -version = "1.1.0" -dependencies = [ - "gear-wasm-builder", - "sails-idl-gen", - "varatube-app", -] - [[package]] name = "version_check" version = "0.9.5" @@ -7967,7 +8141,19 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vft-service" version = "0.1.0" -source = "git+https://github.com/gear-foundation/standards/#a7864f9be28b3c7b154e5ceb4f523cfe1749ad86" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" +dependencies = [ + "gstd", + "log", + "parity-scale-codec", + "sails-rs", + "scale-info", +] + +[[package]] +name = "vmt-service" +version = "0.1.0" +source = "git+https://github.com/gear-foundation/standards/#673218fcae1dd7540e21b650eea675af0650385e" dependencies = [ "gstd", "log", @@ -8044,6 +8230,23 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warrior-app" +version = "0.1.0" +dependencies = [ + "sails-rs", +] + +[[package]] +name = "warrior-wasm" +version = "1.1.0" +dependencies = [ + "gear-wasm-builder", + "sails-idl-gen", + "sails-rs", + "warrior-app", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -8078,7 +8281,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -8100,7 +8303,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8929,7 +9132,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -8949,7 +9152,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 8c9fe3f95..093e9dd67 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,12 +3,15 @@ resolver = "2" # Keep in the lexicographic order! # Remove a member if it's used as a dependency in the workspace. members = [ + "battle", + "battle/warrior/wasm", "battleship", "car-races/app", "car-races/car-1", "car-races/car-2", "car-races/car-3", "car-races/wasm", + "concert/wasm", "galactic-express", "multisig-wallet", "multisig-wallet/state", @@ -22,8 +25,8 @@ members = [ "rmrk/catalog", "rmrk/resource", "rmrk/state", - "syndote", - "syndote/player", + "syndote/wasm", + "syndote/player/wasm", "tamagotchi", "tamagotchi/state", "tamagotchi-battle", diff --git a/contracts/battle/Cargo.toml b/contracts/battle/Cargo.toml new file mode 100644 index 000000000..acd90d1f5 --- /dev/null +++ b/contracts/battle/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "battle" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +battle-app = { path = "app" } + +[dev-dependencies] +battle = { path = ".", features = ["wasm-binary"] } +battle-client = { path = "client" } +sails-rs = { workspace = true, features = ["gtest"] } +gtest.workspace = true +gstd.workspace = true +tokio = { version = "1.39", features = ["rt", "macros"] } + +[build-dependencies] +battle-app = { path = "app" } +sails-rs = { workspace = true, features = ["wasm-builder"] } +sails-idl-gen.workspace = true + +[features] +wasm-binary = [] diff --git a/contracts/battle/README.md b/contracts/battle/README.md new file mode 100644 index 000000000..3d598d729 --- /dev/null +++ b/contracts/battle/README.md @@ -0,0 +1,21 @@ +## The **battle** program + +The program workspace includes the following packages: +- `battle` is the package allowing to build WASM binary for the program and IDL file for it. + The package also includes integration tests for the program in the `tests` sub-folder +- `battle-app` is the package containing business logic for the program represented by the `BattleService` structure. +- `battle-client` is the package containing the client for the program allowing to interact with it from another program, tests, or + off-chain client. + + +### 🏗️ Building + +```sh +cargo b -r -p "battle" +``` + +### ✅ Testing + +```sh +cargo t -r -p "battle" +``` diff --git a/contracts/battle/app/Cargo.toml b/contracts/battle/app/Cargo.toml new file mode 100644 index 000000000..7f830e554 --- /dev/null +++ b/contracts/battle/app/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "battle-app" +version = "0.1.0" +edition = "2021" + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs.workspace = true diff --git a/contracts/battle/app/src/lib.rs b/contracts/battle/app/src/lib.rs new file mode 100644 index 000000000..cd975474b --- /dev/null +++ b/contracts/battle/app/src/lib.rs @@ -0,0 +1,22 @@ +#![no_std] +#![warn(clippy::new_without_default)] +mod services; + +use crate::services::game::utils::Config; +use services::game::BattleService; + +pub struct Program(()); + +pub struct BattleProgram(()); + +#[sails_rs::program] +impl BattleProgram { + pub fn new(config: Config) -> Self { + BattleService::init(config); + Self(()) + } + + pub fn battle(&self) -> BattleService { + BattleService::new() + } +} diff --git a/contracts/battle/app/src/services/game/funcs.rs b/contracts/battle/app/src/services/game/funcs.rs new file mode 100644 index 000000000..1bdbcff19 --- /dev/null +++ b/contracts/battle/app/src/services/game/funcs.rs @@ -0,0 +1,931 @@ +use crate::services::game::{ + Appearance, Battle, BattleError, BattleResult, Config, Event, Move, Pair, Player, + PlayerSettings, State, Storage, +}; +use gstd::{exec, prelude::*, ReservationId}; +use sails_rs::{gstd::msg, prelude::*}; + +async fn check_owner(warrior_id: ActorId, msg_src: ActorId) -> Result<(), BattleError> { + let request = [ + "Warrior".encode(), + "GetOwner".to_string().encode(), + ().encode(), + ] + .concat(); + + let (_, _, owner): (String, String, ActorId) = + msg::send_bytes_with_gas_for_reply_as(warrior_id, request, 10_000_000_000, 0, 0) + .ok() + .ok_or(BattleError::SendingMessageToWarrior)? + .await + .ok() + .ok_or(BattleError::GetWarriorOwner)?; + + if owner != msg_src { + return Err(BattleError::NotOwnerOfWarrior); + } + Ok(()) +} + +async fn get_appearance(warrior_id: ActorId) -> Result { + let request = [ + "Warrior".encode(), + "GetAppearance".to_string().encode(), + ().encode(), + ] + .concat(); + + let (_, _, appearance): (String, String, Appearance) = + msg::send_bytes_with_gas_for_reply_as(warrior_id, request, 5_000_000_000, 0, 0) + .ok() + .ok_or(BattleError::SendingMessageToWarrior)? + .await + .ok() + .ok_or(BattleError::GetWarriorOwner)?; + + Ok(appearance) +} + +pub async fn create_new_battle( + storage: &mut Storage, + warrior_id: Option, + appearance: Option, + battle_name: String, + user_name: String, + attack: u16, + defence: u16, + dodge: u16, +) -> Result { + let msg_src = msg::source(); + let msg_value = msg::value(); + + let reply = create( + storage, + warrior_id, + appearance, + user_name, + battle_name, + attack, + defence, + dodge, + msg_src, + msg_value, + ) + .await; + if reply.is_err() { + msg::send_with_gas(msg_src, "", 0, msg_value).expect("Error in sending the value"); + } + reply +} + +async fn create( + storage: &mut Storage, + warrior_id: Option, + appearance: Option, + user_name: String, + battle_name: String, + attack: u16, + defence: u16, + dodge: u16, + msg_src: ActorId, + msg_value: u128, +) -> Result { + let time_creation = exec::block_timestamp(); + check_player_settings(attack, defence, dodge, &storage.config)?; + let appearance = if let Some(id) = warrior_id { + check_owner(id, msg_src).await?; + get_appearance(id).await? + } else if let Some(app) = appearance { + app + } else { + return Err(BattleError::IdAndAppearanceIsNone); + }; + + if storage.battles.contains_key(&msg_src) { + return Err(BattleError::AlreadyHaveBattle); + } + + let mut battle = Battle::default(); + let player = Player { + warrior_id, + appearance, + owner: msg_src, + user_name: user_name.clone(), + player_settings: PlayerSettings { + health: storage.config.health, + attack, + defence: defence * 10, + dodge: dodge * 4, + }, + number_of_victories: 0, + ultimate_reload: 0, + reflect_reload: 0, + }; + battle.participants.insert(msg_src, player); + battle.bid = msg_value; + battle.admin = msg_src; + battle.time_creation = time_creation; + battle.battle_name = battle_name; + storage.battles.insert(msg_src, battle); + storage.players_to_battle_id.insert(msg_src, msg_src); + + send_delayed_message_for_cancel_tournament( + msg_src, + time_creation, + storage.config.gas_to_cancel_the_battle, + storage.config.time_to_cancel_the_battle, + ); + Ok(Event::NewBattleCreated { + battle_id: msg_src, + bid: msg_value, + }) +} + +pub async fn battle_registration( + storage: &mut Storage, + admin_id: ActorId, + warrior_id: Option, + appearance: Option, + user_name: String, + attack: u16, + defence: u16, + dodge: u16, +) -> Result { + let msg_src = msg::source(); + let msg_value = msg::value(); + + let reply = register( + storage, admin_id, warrior_id, appearance, user_name, attack, defence, dodge, msg_src, + msg_value, + ) + .await; + if reply.is_err() { + msg::send_with_gas(msg_src, "", 0, msg_value).expect("Error in sending the value"); + } + reply +} + +async fn register( + storage: &mut Storage, + admin_id: ActorId, + warrior_id: Option, + appearance: Option, + user_name: String, + attack: u16, + defence: u16, + dodge: u16, + msg_src: ActorId, + msg_value: u128, +) -> Result { + check_player_settings(attack, defence, dodge, &storage.config)?; + + let appearance = if let Some(id) = warrior_id { + check_owner(id, msg_src).await?; + get_appearance(id).await? + } else if let Some(app) = appearance { + app + } else { + return Err(BattleError::IdAndAppearanceIsNone); + }; + + if storage.players_to_battle_id.contains_key(&msg_src) { + return Err(BattleError::SeveralRegistrations); + } + let battle = storage + .battles + .get_mut(&admin_id) + .ok_or(BattleError::NoSuchGame)?; + + if battle.state != State::Registration { + return Err(BattleError::WrongState); + } + if battle.participants.len() >= storage.config.max_participants.into() { + return Err(BattleError::BattleFull); + } + if battle.bid != msg_value { + return Err(BattleError::WrongBid); + } + + let reservation_id = ReservationId::reserve( + storage.config.reservation_amount, + storage.config.reservation_time, + ) + .expect("Reservation across executions"); + + battle.reservation.insert(msg_src, reservation_id); + battle.participants.insert( + msg_src, + Player { + warrior_id, + appearance, + owner: msg_src, + user_name: user_name.clone(), + player_settings: PlayerSettings { + health: storage.config.health, + attack, + defence: defence * 10, + dodge: dodge * 4, + }, + number_of_victories: 0, + ultimate_reload: 0, + reflect_reload: 0, + }, + ); + storage.players_to_battle_id.insert(msg_src, admin_id); + Ok(Event::PlayerRegistered { + admin_id, + user_name, + bid: msg_value, + }) +} + +pub fn cancel_register(storage: &mut Storage) -> Result { + let msg_src = msg::source(); + let admin_id = storage + .players_to_battle_id + .get(&msg_src) + .ok_or(BattleError::NoSuchPlayer)?; + + let battle = storage + .battles + .get_mut(admin_id) + .ok_or(BattleError::NoSuchGame)?; + + if battle.admin == msg_src { + return Err(BattleError::AccessDenied); + } + if battle.state != State::Registration { + return Err(BattleError::WrongState); + } + let reservation_id = battle + .reservation + .get(&msg_src) + .ok_or(BattleError::NoSuchReservation)?; + + if battle.bid != 0 { + msg::send_with_gas(msg_src, "", 0, battle.bid).expect("Error in sending the value"); + } + reservation_id + .unreserve() + .expect("Unreservation across executions"); + battle.reservation.remove(&msg_src); + battle.participants.remove(&msg_src); + storage.players_to_battle_id.remove(&msg_src); + + Ok(Event::RegisterCanceled { player_id: msg_src }) +} + +pub fn delete_player(storage: &mut Storage, player_id: ActorId) -> Result { + let msg_src = msg::source(); + let admin_id = storage + .players_to_battle_id + .get(&msg_src) + .ok_or(BattleError::NoSuchPlayer)?; + + let battle = storage + .battles + .get_mut(admin_id) + .ok_or(BattleError::NoSuchGame)?; + + if battle.admin != msg_src { + return Err(BattleError::AccessDenied); + } + + if battle.state != State::Registration { + return Err(BattleError::WrongState); + } + + if !battle.participants.contains_key(&player_id) { + return Err(BattleError::NoSuchPlayer); + } + + let reservation_id = battle + .reservation + .get(&player_id) + .ok_or(BattleError::NoSuchReservation)?; + + if battle.bid != 0 { + msg::send_with_gas(player_id, "", 0, battle.bid).expect("Error in sending the value"); + } + reservation_id + .unreserve() + .expect("Unreservation across executions"); + battle.reservation.remove(&player_id); + battle.participants.remove(&player_id); + storage.players_to_battle_id.remove(&player_id); + + Ok(Event::RegisterCanceled { player_id }) +} + +pub fn cancel_tournament(storage: &mut Storage) -> Result { + let msg_src = msg::source(); + let battle = storage + .battles + .get(&msg_src) + .ok_or(BattleError::NoSuchGame)?; + + let game_is_over = matches!(battle.state, State::GameIsOver { .. }); + + battle.participants.iter().for_each(|(id, _)| { + if !game_is_over && battle.bid != 0 { + msg::send_with_gas(*id, "", 0, battle.bid).expect("Error in sending the value"); + } + storage.players_to_battle_id.remove(id); + }); + + battle.defeated_participants.iter().for_each(|(id, _)| { + if !game_is_over && battle.bid != 0 { + msg::send_with_gas(*id, "", 0, battle.bid).expect("Error in sending the value"); + } + storage.players_to_battle_id.remove(id); + }); + + battle.reservation.iter().for_each(|(_, id)| { + let _ = id.unreserve(); + }); + + storage.battles.remove(&msg_src); + + Ok(Event::BattleCanceled { game_id: msg_src }) +} + +pub fn delayed_cancel_tournament( + storage: &mut Storage, + game_id: ActorId, + time_creation: u64, +) -> Result { + if msg::source() != exec::program_id() { + return Err(BattleError::AccessDenied); + } + + let battle = storage + .battles + .get(&game_id) + .ok_or(BattleError::NoSuchGame)?; + + if battle.time_creation != time_creation { + return Err(BattleError::WrongTimeCreation); + } + if !matches!(battle.state, State::Registration) { + return Err(BattleError::WrongState); + } + battle.participants.iter().for_each(|(id, _)| { + if battle.bid != 0 { + msg::send_with_gas(*id, "", 0, battle.bid).expect("Error in sending the value"); + } + storage.players_to_battle_id.remove(id); + }); + + battle.reservation.iter().for_each(|(_, id)| { + let _ = id.unreserve(); + }); + + storage.battles.remove(&game_id); + + Ok(Event::BattleCanceled { game_id }) +} + +pub fn start_battle(storage: &mut Storage) -> Result { + let msg_src = msg::source(); + let battle = storage + .battles + .get_mut(&msg_src) + .ok_or(BattleError::NoSuchGame)?; + + let reservation_id = ReservationId::reserve( + storage.config.reservation_amount, + storage.config.reservation_time, + ) + .expect("Reservation across executions"); + + battle.reservation.insert(msg_src, reservation_id); + + match battle.state { + State::Registration => { + battle.check_min_player_amount()?; + battle.split_into_pairs()?; + battle.send_delayed_message_make_move_from_reservation( + storage.config.time_for_move_in_blocks, + ); + battle.state = State::Started; + } + _ => return Err(BattleError::WrongState), + } + Ok(Event::BattleStarted) +} + +pub fn automatic_move( + storage: &mut Storage, + player_id: ActorId, + number_of_victories: u8, + round: u8, +) -> Result { + if msg::source() != exec::program_id() { + return Err(BattleError::AccessDenied); + } + let game_id = storage + .players_to_battle_id + .get(&player_id) + .ok_or(BattleError::NoSuchGame)?; + let battle = storage + .battles + .get_mut(game_id) + .ok_or(BattleError::NoSuchGame)?; + + battle.check_state(State::Started)?; + let player = battle + .participants + .get(&player_id) + .ok_or(BattleError::NoSuchPlayer)?; + // check the number of victories, if equal, then the game is not over + if player.number_of_victories == number_of_victories { + let pair_id = battle + .players_to_pairs + .get(&player_id) + .ok_or(BattleError::NoSuchPair)?; + let pair = battle + .pairs + .get_mut(pair_id) + .ok_or(BattleError::NoSuchPair)?; + + // round check + if round == pair.round { + if let Some(opponent_info) = pair.action { + if opponent_info.0 == player_id { + send_delayed_automatic_move( + player_id, + number_of_victories, + pair.round, + storage.config.time_for_move_in_blocks, + ); + return Ok(Event::AutomaticMoveMade); + } + let player_1_ptr = battle + .participants + .get_mut(&opponent_info.0) + .expect("The player must exist") as *mut _; + let player_2_ptr = battle + .participants + .get_mut(&player_id) + .expect("The player must exist") as *mut _; + + let (round_result, player_1, player_2) = unsafe { + let player_1 = &mut *player_1_ptr; + let player_2 = &mut *player_2_ptr; + + ( + pair.recap_round((player_1, opponent_info.1), (player_2, Move::Attack)), + player_1, + player_2, + ) + }; + pair.action = None; + let current_round = pair.round; + if let Some(battle_result) = round_result { + match battle_result { + BattleResult::PlayerWin(winner) => { + let loser = pair.get_opponent(&winner); + let player_loser = battle + .participants + .remove(&loser) + .expect("The player must exist"); + let player_winner = battle + .participants + .get_mut(&winner) + .expect("The player must exist"); + player_winner.player_settings.health = storage.config.health; + player_winner.reflect_reload = 0; + player_winner.ultimate_reload = 0; + player_winner.number_of_victories += 1; + battle.defeated_participants.insert(loser, player_loser); + battle.pairs.remove(pair_id); + battle.players_to_pairs.remove(&winner); + battle.players_to_pairs.remove(&loser); + battle.check_end_game(); + } + BattleResult::Draw(id_1, id_2) => { + let player_1 = battle + .participants + .get_mut(&id_1) + .expect("The player must exist"); + player_1.player_settings.health = storage.config.health; + player_1.reflect_reload = 0; + player_1.ultimate_reload = 0; + let player_2 = battle + .participants + .get_mut(&id_2) + .expect("The player must exist"); + + player_2.player_settings.health = storage.config.health; + player_2.reflect_reload = 0; + player_2.ultimate_reload = 0; + battle.pairs.remove(pair_id); + battle.players_to_pairs.remove(&id_1); + battle.players_to_pairs.remove(&id_2); + battle.check_draw_end_game(); + } + } + } else { + pair.round += 1; + pair.round_start_time = exec::block_timestamp(); + send_delayed_automatic_move( + player_id, + number_of_victories, + pair.round, + storage.config.time_for_move_in_blocks, + ); + } + + return Ok(Event::RoundAction { + round: current_round, + player_1: ( + opponent_info.0, + opponent_info.1, + player_1.player_settings.health, + ), + player_2: (player_id, Move::Attack, player_2.player_settings.health), + }); + } else { + pair.action = Some((player_id, Move::Attack)); + send_delayed_automatic_move( + player_id, + number_of_victories, + pair.round + 1, + storage.config.time_for_move_in_blocks, + ); + } + } else { + // if the round is different, we need to see when it started and calculate the time for the next pending message + let delay = storage.config.time_for_move_in_blocks + - ((exec::block_timestamp() - pair.round_start_time) + / storage.config.block_duration_ms as u64) as u32 + + 1; + + send_delayed_automatic_move(player_id, number_of_victories, pair.round, delay); + } + } + + Ok(Event::AutomaticMoveMade) +} + +pub fn make_move(storage: &mut Storage, warrior_move: Move) -> Result { + let player = msg::source(); + let game_id = storage + .players_to_battle_id + .get(&player) + .ok_or(BattleError::NoSuchGame)?; + let battle = storage + .battles + .get_mut(game_id) + .ok_or(BattleError::NoSuchGame)?; + + battle.check_state(State::Started)?; + + let pair_id = battle + .players_to_pairs + .get(&player) + .ok_or(BattleError::NoSuchPair)?; + let pair = battle + .pairs + .get_mut(pair_id) + .ok_or(BattleError::NoSuchPair)?; + + let timestamp = exec::block_timestamp(); + let time_for_move_ms = + storage.config.block_duration_ms * storage.config.time_for_move_in_blocks; + if timestamp.saturating_sub(pair.round_start_time) >= time_for_move_ms as u64 { + return Err(BattleError::TimeExpired); + } + match warrior_move { + Move::Ultimate => check_reload_ultimate( + battle + .participants + .get(&player) + .expect("The player must exist"), + )?, + Move::Reflect => check_reload_reflect( + battle + .participants + .get(&player) + .expect("The player must exist"), + )?, + Move::Attack => (), + } + + if let Some(opponent_info) = pair.action { + if opponent_info.0 == player { + return Err(BattleError::MoveHasAlreadyBeenMade); + } + + let player_1_ptr = battle + .participants + .get_mut(&opponent_info.0) + .expect("The player must exist") as *mut _; + let player_2_ptr = battle + .participants + .get_mut(&player) + .expect("The player must exist") as *mut _; + + let (round_result, player_1, player_2) = unsafe { + let player_1 = &mut *player_1_ptr; + let player_2 = &mut *player_2_ptr; + + ( + pair.recap_round((player_1, opponent_info.1), (player_2, warrior_move)), + player_1, + player_2, + ) + }; + pair.action = None; + let current_round = pair.round; + if let Some(battle_result) = round_result { + match battle_result { + BattleResult::PlayerWin(winner) => { + let loser = pair.get_opponent(&winner); + let player_loser = battle + .participants + .remove(&loser) + .expect("The player must exist"); + battle.defeated_participants.insert(loser, player_loser); + let player_winner = battle + .participants + .get_mut(&winner) + .expect("The player must exist"); + player_winner.player_settings.health = storage.config.health; + player_winner.reflect_reload = 0; + player_winner.ultimate_reload = 0; + player_winner.number_of_victories += 1; + battle.pairs.remove(pair_id); + battle.players_to_pairs.remove(&winner); + battle.players_to_pairs.remove(&loser); + battle.check_end_game(); + } + BattleResult::Draw(id_1, id_2) => { + let player_1 = battle + .participants + .get_mut(&id_1) + .expect("The player must exist"); + player_1.player_settings.health = storage.config.health; + player_1.reflect_reload = 0; + player_1.ultimate_reload = 0; + let player_2 = battle + .participants + .get_mut(&id_2) + .expect("The player must exist"); + + player_2.player_settings.health = storage.config.health; + player_2.reflect_reload = 0; + player_2.ultimate_reload = 0; + battle.pairs.remove(pair_id); + battle.players_to_pairs.remove(&id_1); + battle.players_to_pairs.remove(&id_2); + battle.check_draw_end_game(); + } + } + } else { + pair.round += 1; + pair.round_start_time = exec::block_timestamp(); + } + Ok(Event::RoundAction { + round: current_round, + player_1: ( + opponent_info.0, + opponent_info.1, + player_1.player_settings.health, + ), + player_2: (player, warrior_move, player_2.player_settings.health), + }) + } else { + pair.action = Some((player, warrior_move)); + Ok(Event::MoveMade) + } +} + +pub fn start_next_fight(storage: &mut Storage) -> Result { + let player_id = msg::source(); + let game_id = storage + .players_to_battle_id + .get(&player_id) + .ok_or(BattleError::NoSuchGame)?; + let battle = storage + .battles + .get_mut(game_id) + .ok_or(BattleError::NoSuchGame)?; + + battle.check_state(State::Started)?; + + if battle.players_to_pairs.contains_key(&player_id) { + return Err(BattleError::AlreadyHaveBattle); + } + + let reservation_id = ReservationId::reserve( + storage.config.reservation_amount, + storage.config.reservation_time, + ) + .expect("Reservation across executions"); + + battle.reservation.insert(player_id, reservation_id); + + let player = battle + .participants + .get(&player_id) + .ok_or(BattleError::NoSuchPlayer)?; + + if let Some((opponent, pair_id)) = battle.waiting_player { + let pair = battle + .pairs + .get_mut(&pair_id) + .expect("The pair must be created"); + pair.player_2 = player.owner; + pair.round_start_time = exec::block_timestamp(); + battle.players_to_pairs.insert(player.owner, pair_id); + battle.waiting_player = None; + send_delayed_message_make_move_from_reservation( + reservation_id, + storage.config.time_for_move_in_blocks, + player_id, + player.number_of_victories, + ); + + let reservation_id = battle + .reservation + .get(&opponent) + .expect("Reservation must be exist"); + let opponent_player = battle + .participants + .get(&opponent) + .expect("Player must be exist"); + send_delayed_message_make_move_from_reservation( + *reservation_id, + storage.config.time_for_move_in_blocks, + opponent_player.owner, + opponent_player.number_of_victories, + ); + Ok(Event::NextBattleStarted) + } else { + let pair = Pair { + player_1: player.owner, + round: 1, + ..Default::default() + }; + battle.pairs.insert(battle.pair_id, pair); + battle.players_to_pairs.insert(player.owner, battle.pair_id); + battle.waiting_player = Some((player.owner, battle.pair_id)); + battle.pair_id += 1; + Ok(Event::EnemyWaiting) + } +} + +pub fn exit_game(storage: &mut Storage) -> Result { + let player_id = msg::source(); + let game_id = storage + .players_to_battle_id + .get(&player_id) + .ok_or(BattleError::NoSuchGame)?; + let battle = storage + .battles + .get_mut(game_id) + .ok_or(BattleError::NoSuchGame)?; + + if battle.defeated_participants.contains_key(&player_id) { + storage.players_to_battle_id.remove(&player_id); + } else { + let player = battle + .participants + .get(&player_id) + .expect("The player must exist"); + if let Some(pair_id) = battle.players_to_pairs.get(&player_id) { + let pair = battle.pairs.remove(pair_id).expect("The pair must exist"); + + battle.players_to_pairs.remove(&player_id); + battle + .defeated_participants + .insert(player_id, player.clone()); + + let opponent_id = pair.get_opponent(&player_id); + battle.players_to_pairs.remove(&opponent_id); + let opponent = battle + .participants + .get_mut(&opponent_id) + .expect("The player must exist"); + + opponent.number_of_victories += 1; + opponent.player_settings.health = storage.config.health; + + battle.participants.remove(&player_id); + storage.players_to_battle_id.remove(&player_id); + battle.check_end_game(); + } else { + if let Some((id, _)) = battle.waiting_player { + if id == player_id { + battle.waiting_player = None; + } + } + battle + .defeated_participants + .insert(player_id, player.clone()); + battle.participants.remove(&player_id); + storage.players_to_battle_id.remove(&player_id); + } + } + Ok(Event::GameLeft) +} + +fn check_reload_ultimate(player: &Player) -> Result<(), BattleError> { + if player.ultimate_reload != 0 { + return Err(BattleError::UltimateReload); + } + Ok(()) +} + +fn check_reload_reflect(player: &Player) -> Result<(), BattleError> { + if player.reflect_reload != 0 { + return Err(BattleError::ReflectReload); + } + Ok(()) +} + +fn check_player_settings( + attack: u16, + defence: u16, + dodge: u16, + config: &Config, +) -> Result<(), BattleError> { + let attack_in_range = config.attack_range.0 <= attack && attack <= config.attack_range.1; + let defence_in_range = config.defence_range.0 <= defence && defence <= config.defence_range.1; + let dodge_in_range = config.dodge_range.0 <= dodge && dodge <= config.dodge_range.1; + + let total_points = attack + defence + dodge + - config.attack_range.0 + - config.defence_range.0 + - config.dodge_range.0; + + if !(attack_in_range + && defence_in_range + && dodge_in_range + && total_points == config.available_points) + { + return Err(BattleError::MisallocationOfPoints); + } + Ok(()) +} + +fn send_delayed_message_make_move_from_reservation( + reservation_id: ReservationId, + time_for_move: u32, + player_id: ActorId, + number_of_victories: u8, +) { + let round: u8 = 1; + let request = [ + "Battle".encode(), + "AutomaticMove".to_string().encode(), + (player_id, number_of_victories, round).encode(), + ] + .concat(); + + msg::send_bytes_delayed_from_reservation( + reservation_id, + exec::program_id(), + request, + 0, + time_for_move, + ) + .expect("Error in sending message"); +} + +fn send_delayed_automatic_move(player_id: ActorId, number_of_victories: u8, round: u8, delay: u32) { + let gas = exec::gas_available() - 5_000_000_000; + let request = [ + "Battle".encode(), + "AutomaticMove".to_string().encode(), + (player_id, number_of_victories, round).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_delayed(exec::program_id(), request, gas, 0, delay) + .expect("Error in sending message"); +} + +fn send_delayed_message_for_cancel_tournament( + game_id: ActorId, + time_creation: u64, + gas_to_cancel_the_battle: u64, + time_to_cancel_the_battle: u32, +) { + let request = [ + "Battle".encode(), + "DelayedCancelTournament".to_string().encode(), + (Some((game_id, time_creation))).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_delayed( + exec::program_id(), + request, + gas_to_cancel_the_battle, + 0, + time_to_cancel_the_battle, + ) + .expect("Error in sending message"); +} diff --git a/contracts/battle/app/src/services/game/mod.rs b/contracts/battle/app/src/services/game/mod.rs new file mode 100644 index 000000000..009f23283 --- /dev/null +++ b/contracts/battle/app/src/services/game/mod.rs @@ -0,0 +1,250 @@ +#![allow(clippy::too_many_arguments)] +#![allow(clippy::new_without_default)] +use crate::services; +use sails_rs::{ + collections::{HashMap, HashSet}, + gstd::{msg, service}, + prelude::*, +}; +mod funcs; +pub mod utils; +use utils::Config; +use utils::*; + +#[derive(Debug, Default, Clone)] +struct Storage { + battles: HashMap, + players_to_battle_id: HashMap, + admins: HashSet, + config: Config, +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + NewBattleCreated { + battle_id: ActorId, + bid: u128, + }, + PlayerRegistered { + admin_id: ActorId, + user_name: String, + bid: u128, + }, + RegisterCanceled { + player_id: ActorId, + }, + BattleCanceled { + game_id: ActorId, + }, + BattleStarted, + MoveMade, + BattleFinished { + winner: ActorId, + }, + PairChecked { + game_id: ActorId, + pair_id: u8, + round: u8, + }, + FirstRoundChecked { + game_id: ActorId, + wave: u8, + }, + NextBattleStarted, + EnemyWaiting, + WarriorGenerated { + address: ActorId, + }, + AdminAdded { + new_admin: ActorId, + }, + ConfigChanged { + config: Config, + }, + GameLeft, + RoundAction { + round: u8, + player_1: (ActorId, Move, u16), + player_2: (ActorId, Move, u16), + }, + AutomaticMoveMade, +} + +#[derive(Clone)] +pub struct BattleService(()); + +impl BattleService { + pub fn init(config: Config) -> Self { + unsafe { + STORAGE = Some(Storage { + admins: HashSet::from([msg::source()]), + config, + ..Default::default() + }); + } + Self(()) + } + fn get_mut(&mut self) -> &'static mut Storage { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + fn get(&self) -> &'static Storage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl BattleService { + pub fn new() -> Self { + Self(()) + } + + pub async fn create_new_battle( + &mut self, + battle_name: String, + user_name: String, + warrior_id: Option, + appearance: Option, + attack: u16, + defence: u16, + dodge: u16, + ) { + let storage = self.get_mut(); + let res = funcs::create_new_battle( + storage, + warrior_id, + appearance, + battle_name, + user_name, + attack, + defence, + dodge, + ) + .await; + let event = match res { + Ok(v) => v, + Err(e) => services::utils::panic(e), + }; + self.notify_on(event.clone()).expect("Notification Error"); + } + pub async fn register( + &mut self, + game_id: ActorId, + warrior_id: Option, + appearance: Option, + user_name: String, + attack: u16, + defence: u16, + dodge: u16, + ) { + let storage = self.get_mut(); + let res = funcs::battle_registration( + storage, game_id, warrior_id, appearance, user_name, attack, defence, dodge, + ) + .await; + let event = match res { + Ok(v) => v, + Err(e) => services::utils::panic(e), + }; + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn cancel_register(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::cancel_register(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn delete_player(&mut self, player_id: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::delete_player(storage, player_id)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn cancel_tournament(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::cancel_tournament(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn delayed_cancel_tournament(&mut self, game_id: ActorId, time_creation: u64) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::delayed_cancel_tournament(storage, game_id, time_creation) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn start_battle(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::start_battle(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn make_move(&mut self, warrior_move: Move) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::make_move(storage, warrior_move)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn automatic_move(&mut self, player_id: ActorId, number_of_victories: u8, round: u8) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::automatic_move(storage, player_id, number_of_victories, round) + }); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn start_next_fight(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::start_next_fight(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn exit_game(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::exit_game(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn add_admin(&mut self, new_admin: ActorId) { + let storage = self.get_mut(); + if !storage.admins.contains(&msg::source()) { + services::utils::panic(BattleError::AccessDenied); + } + storage.admins.insert(new_admin); + self.notify_on(Event::AdminAdded { new_admin }) + .expect("Notification Error"); + } + pub fn change_config(&mut self, config: Config) { + let storage = self.get_mut(); + if !storage.admins.contains(&msg::source()) { + services::utils::panic(BattleError::AccessDenied); + } + storage.config = config.clone(); + self.notify_on(Event::ConfigChanged { config }) + .expect("Notification Error"); + } + + pub fn get_battle(&self, game_id: ActorId) -> Option { + let storage = self.get(); + storage + .battles + .get(&game_id) + .cloned() + .map(|battle| battle.into()) + } + pub fn get_my_battle(&self) -> Option { + let storage = self.get(); + if let Some(game_id) = storage.players_to_battle_id.get(&msg::source()) { + storage + .battles + .get(game_id) + .cloned() + .map(|battle| battle.into()) + } else { + None + } + } + pub fn admins(&self) -> Vec { + let storage = self.get(); + storage.admins.clone().into_iter().collect() + } + pub fn config(&self) -> &'static Config { + let storage = self.get(); + &storage.config + } +} diff --git a/contracts/battle/app/src/services/game/utils.rs b/contracts/battle/app/src/services/game/utils.rs new file mode 100644 index 000000000..41ed18ce3 --- /dev/null +++ b/contracts/battle/app/src/services/game/utils.rs @@ -0,0 +1,575 @@ +use gstd::{exec, msg, ReservationId}; +use sails_rs::{collections::HashMap, prelude::*}; + +pub type PairId = u16; +static mut SEED: u8 = 0; + +pub fn get_random_value(range: u8) -> u8 { + if range == 0 { + return 0; + } + let seed = unsafe { SEED }; + unsafe { SEED = SEED.wrapping_add(1) }; + let random_input: [u8; 32] = [seed; 32]; + let (random, _) = exec::random(random_input).expect("Error in getting random number"); + random[0] % range +} +pub fn get_random_dodge(chance: u8) -> bool { + assert!(chance <= 100, "The chance must be between 0 and 100"); + let random_value = get_random_value(101); + random_value < chance +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum BattleError { + ProgramInitializationFailedWithContext(String), + AlreadyHaveBattle, + NotOwnerOfWarrior, + SendingMessageToWarrior, + GetWarriorOwner, + SeveralRegistrations, + NoSuchGame, + NoSuchPair, + WrongState, + WrongBid, + NoSuchPlayer, + AccessDenied, + BattleFull, + NotEnoughPlayers, + NotAdmin, + PlayerDoesNotExist, + PairDoesNotExist, + GameIsOver, + NotWarriorOwner, + NotPlayerGame, + NoGamesForPlayer, + TimeExpired, + MoveHasAlreadyBeenMade, + NoSuchReservation, + WrongTimeCreation, + UltimateReload, + ReflectReload, + MisallocationOfPoints, + IdAndAppearanceIsNone, +} + +#[derive(Debug, Default, Clone)] +pub struct Battle { + pub admin: ActorId, + pub battle_name: String, + pub time_creation: u64, + pub bid: u128, + pub participants: HashMap, + pub defeated_participants: HashMap, + pub state: State, + pub pairs: HashMap, + pub players_to_pairs: HashMap, + pub reservation: HashMap, + pub waiting_player: Option<(ActorId, PairId)>, + pub pair_id: u16, +} + +impl Battle { + pub fn check_end_game(&mut self) { + if self.participants.len() == 1 { + if let Some((&winner, _)) = self.participants.iter().next() { + if self.bid != 0 { + msg::send_with_gas( + winner, + "", + 10_000, + self.bid * (self.defeated_participants.len() + 1) as u128, + ) + .expect("Error send value"); + // TODO: uncomment and switch https://github.com/gear-tech/gear/pull/4270 + // msg::send_with_gas(winner, "", 0, self.bid * (self.defeated_participants.len() + 1) as u128).expect("Error send value"); + } + self.state = State::GameIsOver { + winners: (winner, None), + }; + } + } + } + + pub fn check_draw_end_game(&mut self) { + if self.participants.len() == 2 { + let mut winners: Vec = Vec::with_capacity(2); + let prize = self.bid * (self.defeated_participants.len() + 2) as u128 / 2; + for id in self.participants.keys() { + if self.bid != 0 { + msg::send_with_gas(*id, "", 10_000, prize).expect("Error send value"); + // TODO: uncomment and switch https://github.com/gear-tech/gear/pull/4270 + // msg::send_with_gas( + // *id, + // "", + // 10_000, + // prize, + // ) + // .expect("Error send value"); + } + winners.push(*id); + } + self.state = State::GameIsOver { + winners: (winners[0], Some(winners[1])), + }; + } + } + + pub fn delete_players(&mut self, loser_1: &ActorId, loser_2: &ActorId, pair_id: u16) { + let player_loser_1 = self + .participants + .remove(loser_1) + .expect("The player must exist"); + let player_loser_2 = self + .participants + .remove(loser_2) + .expect("The player must exist"); + + self.defeated_participants.insert(*loser_1, player_loser_1); + self.defeated_participants.insert(*loser_2, player_loser_2); + + self.pairs.remove(&pair_id); + self.players_to_pairs.remove(loser_1); + self.players_to_pairs.remove(loser_2); + } + + pub fn check_min_player_amount(&self) -> Result<(), BattleError> { + if self.participants.len() <= 1 { + return Err(BattleError::NotEnoughPlayers); + } + Ok(()) + } + + pub fn split_into_pairs(&mut self) -> Result<(), BattleError> { + let round_start_time = exec::block_timestamp(); + self.create_pairs(round_start_time); + Ok(()) + } + + pub fn create_pairs(&mut self, round_start_time: u64) { + self.pairs = HashMap::new(); + self.players_to_pairs = HashMap::new(); + let mut participants_vec: Vec<(ActorId, Player)> = + self.participants.clone().into_iter().collect(); + + while participants_vec.len() > 1 { + let range = participants_vec.len() as u8; + let idx1 = get_random_value(range); + let player1 = participants_vec.swap_remove(idx1 as usize).1; + let idx2 = get_random_value(range - 1); + let player2 = participants_vec.swap_remove(idx2 as usize).1; + let pair = Pair { + player_1: player1.owner, + player_2: player2.owner, + round_start_time, + round: 1, + action: None, + }; + self.pairs.insert(self.pair_id, pair); + self.players_to_pairs.insert(player1.owner, self.pair_id); + self.players_to_pairs.insert(player2.owner, self.pair_id); + self.pair_id += 1; + } + // If there are an odd number of participants left, one goes into standby mode + if participants_vec.len() == 1 { + let player = participants_vec.remove(0).1; + let pair = Pair { + player_1: player.owner, + round: 1, + ..Default::default() + }; + self.pairs.insert(self.pair_id, pair); + self.players_to_pairs.insert(player.owner, self.pair_id); + self.waiting_player = Some((player.owner, self.pair_id)); + } + } + pub fn send_delayed_message_make_move_from_reservation(&mut self, time_for_move: u32) { + let mut new_map_reservation = HashMap::new(); + self.reservation + .iter() + .for_each(|(actor_id, reservation_id)| { + if let Some(waiting_player) = self.waiting_player { + if waiting_player.0 == *actor_id { + new_map_reservation.insert(waiting_player.0, *reservation_id); + return; + } + } + let number_of_victories = self + .participants + .get(actor_id) + .expect("The player must exist") + .number_of_victories; + let round: u8 = 1; + let request = [ + "Battle".encode(), + "AutomaticMove".to_string().encode(), + (*actor_id, number_of_victories, round).encode(), + ] + .concat(); + + msg::send_bytes_delayed_from_reservation( + *reservation_id, + exec::program_id(), + request, + 0, + time_for_move, + ) + .expect("Error in sending message"); + }); + self.reservation = new_map_reservation; + } + + pub fn check_state(&self, state: State) -> Result<(), BattleError> { + if self.state != state { + return Err(BattleError::WrongState); + } + Ok(()) + } +} + +#[derive(Default, Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Player { + pub warrior_id: Option, + pub owner: ActorId, + pub user_name: String, + pub player_settings: PlayerSettings, + pub appearance: Appearance, + pub number_of_victories: u8, + pub ultimate_reload: u8, + pub reflect_reload: u8, +} + +#[derive(Debug, PartialEq, Eq, Encode, Decode, TypeInfo, Default, Clone)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum State { + #[default] + Registration, + Started, + GameIsOver { + winners: (ActorId, Option), + }, +} + +#[derive(Default, Debug, Encode, Decode, TypeInfo, Clone, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Pair { + pub player_1: ActorId, + pub player_2: ActorId, + pub action: Option<(ActorId, Move)>, + pub round: u8, + pub round_start_time: u64, +} + +impl Pair { + pub fn recap_round( + &mut self, + player1_info: (&mut Player, Move), + player2_info: (&mut Player, Move), + ) -> Option { + let dodge_1 = get_random_dodge(player1_info.0.player_settings.dodge as u8); + let dodge_2 = get_random_dodge(player2_info.0.player_settings.dodge as u8); + + // Damage that players will receive (default 0) + let mut damage_1 = 0; + let mut damage_2 = 0; + + // Process the actions of both players + match (player1_info.1, player2_info.1) { + (Move::Attack, Move::Attack) => { + if !dodge_2 { + damage_2 = player1_info.0.player_settings.attack; // Player 2 takes damage from Player 1 + } + if !dodge_1 { + damage_1 = player2_info.0.player_settings.attack; // Player 1 takes damage from Player 2 + } + } + (Move::Attack, Move::Reflect) => { + if !dodge_2 { + // Player 2 reflects the damage, reducing it by the value of his defence + damage_2 = player1_info + .0 + .player_settings + .attack + .saturating_mul(100 - player2_info.0.player_settings.defence) + .saturating_div(100); + } + if !dodge_1 { + // Player 1 takes reflected damage + damage_1 = player1_info + .0 + .player_settings + .attack + .saturating_sub(damage_2); + } + player2_info.0.reflect_reload = 2; + } + (Move::Reflect, Move::Attack) => { + if !dodge_1 { + // Player 1 reflects the damage, reducing it by the value of their defence + damage_1 = player2_info + .0 + .player_settings + .attack + .saturating_mul(100 - player1_info.0.player_settings.defence) + .saturating_div(100); + } + if !dodge_2 { + // Player 2 takes reflected damage + damage_2 = player2_info + .0 + .player_settings + .attack + .saturating_sub(damage_1); + } + player1_info.0.reflect_reload = 2; + } + (Move::Reflect, Move::Reflect) => { + // Both players deflect each other's attacks, no damage done + damage_1 = 0; + damage_2 = 0; + player1_info.0.reflect_reload = 2; + player2_info.0.reflect_reload = 2; + } + (Move::Ultimate, Move::Attack) => { + if !dodge_1 { + // Player 1 receives a normal attack + damage_1 = player2_info.0.player_settings.attack; + } + if !dodge_2 { + // Player 2 takes double the damage from Ultimate + damage_2 = player1_info.0.player_settings.attack * 2; + } + player1_info.0.ultimate_reload = 2; + } + (Move::Attack, Move::Ultimate) => { + if !dodge_1 { + // Player 1 takes double the damage from Ultimate + damage_1 = player2_info.0.player_settings.attack * 2; + } + if !dodge_2 { + // Player 2 receives a normal attack + damage_2 = player1_info.0.player_settings.attack; + } + player2_info.0.ultimate_reload = 2; + } + (Move::Ultimate, Move::Ultimate) => { + if !dodge_1 { + // Player 1 takes double the damage from Ultimate + damage_1 = player2_info.0.player_settings.attack * 2; + } + if !dodge_2 { + // Player 2 takes double the damage from Ultimate + damage_2 = player1_info.0.player_settings.attack * 2; + } + player1_info.0.ultimate_reload = 2; + player2_info.0.ultimate_reload = 2; + } + (Move::Reflect, Move::Ultimate) => { + if !dodge_1 { + // Player 1 takes double damage from Ultimate, but can partially deflect it + damage_1 = (player2_info.0.player_settings.attack * 2) + .saturating_mul(100 - player1_info.0.player_settings.defence) + .saturating_div(100); + } + if !dodge_2 { + // Player 2 takes reflected damage + damage_2 = (player2_info.0.player_settings.attack * 2).saturating_sub(damage_1); + } + player1_info.0.reflect_reload = 2; + player2_info.0.ultimate_reload = 2; + } + (Move::Ultimate, Move::Reflect) => { + if !dodge_2 { + // Player 2 takes double damage from Ultimate, but can partially deflect it + damage_2 = (player1_info.0.player_settings.attack * 2) + .saturating_mul(100 - player2_info.0.player_settings.defence) + .saturating_div(100); + } + if !dodge_1 { + // Player 1 takes reflected damage + damage_1 = (player1_info.0.player_settings.attack * 2).saturating_sub(damage_2); + } + player1_info.0.ultimate_reload = 2; + player2_info.0.reflect_reload = 2; + } + } + + match player2_info.1 { + Move::Attack => { + player2_info.0.reflect_reload = player2_info.0.reflect_reload.saturating_sub(1); + player2_info.0.ultimate_reload = player2_info.0.ultimate_reload.saturating_sub(1); + } + Move::Reflect => { + player2_info.0.ultimate_reload = player2_info.0.ultimate_reload.saturating_sub(1); + } + Move::Ultimate => { + player2_info.0.reflect_reload = player2_info.0.reflect_reload.saturating_sub(1); + } + } + + match player1_info.1 { + Move::Attack => { + player1_info.0.reflect_reload = player1_info.0.reflect_reload.saturating_sub(1); + player1_info.0.ultimate_reload = player1_info.0.ultimate_reload.saturating_sub(1); + } + Move::Reflect => { + player1_info.0.ultimate_reload = player1_info.0.ultimate_reload.saturating_sub(1); + } + Move::Ultimate => { + player1_info.0.reflect_reload = player1_info.0.reflect_reload.saturating_sub(1); + } + } + // Damage application + player1_info.0.player_settings.health = player1_info + .0 + .player_settings + .health + .saturating_sub(damage_1); + player2_info.0.player_settings.health = player2_info + .0 + .player_settings + .health + .saturating_sub(damage_2); + + // Checking to see who won + if player1_info.0.player_settings.health == 0 && player2_info.0.player_settings.health == 0 + { + return Some(BattleResult::Draw( + player1_info.0.owner, + player2_info.0.owner, + )); // Both players lost, a draw + } else if player1_info.0.player_settings.health == 0 { + return Some(BattleResult::PlayerWin(player2_info.0.owner)); // Player 2 wins + } else if player2_info.0.player_settings.health == 0 { + return Some(BattleResult::PlayerWin(player1_info.0.owner)); // Player 1 wins + } + None // Both players are still alive, no one has won + } + + pub fn get_opponent(&self, player: &ActorId) -> ActorId { + if self.player_1 != *player { + self.player_1 + } else { + self.player_2 + } + } +} + +#[derive(Encode, Decode, TypeInfo, PartialEq, Eq, Debug, Clone, Copy)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Move { + Attack, + Reflect, + Ultimate, +} + +#[derive(Default, Encode, Decode, TypeInfo, PartialEq, Eq, Debug, Clone)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct PlayerSettings { + pub health: u16, + pub attack: u16, + pub defence: u16, + pub dodge: u16, +} + +#[derive(Default, Encode, Decode, TypeInfo, PartialEq, Eq, Debug, Clone)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Config { + pub health: u16, + pub max_participants: u8, + pub attack_range: (u16, u16), + pub defence_range: (u16, u16), + pub dodge_range: (u16, u16), + pub available_points: u16, + pub time_for_move_in_blocks: u32, + pub block_duration_ms: u32, + pub gas_for_create_warrior: u64, + pub gas_to_cancel_the_battle: u64, + pub time_to_cancel_the_battle: u32, + pub reservation_amount: u64, + pub reservation_time: u32, +} + +#[derive(Debug, Default, Clone, TypeInfo, Encode, Decode)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct BattleState { + pub admin: ActorId, + pub battle_name: String, + pub time_creation: u64, + pub bid: u128, + pub participants: Vec<(ActorId, Player)>, + pub defeated_participants: Vec<(ActorId, Player)>, + pub state: State, + pub pairs: Vec<(PairId, Pair)>, + pub players_to_pairs: Vec<(ActorId, PairId)>, + pub waiting_player: Option<(ActorId, PairId)>, + pub pair_id: u16, + pub reservation: Vec<(ActorId, ReservationId)>, +} + +impl From for BattleState { + fn from(value: Battle) -> Self { + let Battle { + admin, + battle_name, + time_creation, + bid, + participants, + defeated_participants, + state, + pairs, + players_to_pairs, + waiting_player, + pair_id, + reservation, + } = value; + + let participants = participants.into_iter().collect(); + let defeated_participants = defeated_participants.into_iter().collect(); + let pairs = pairs.into_iter().collect(); + let players_to_pairs = players_to_pairs.into_iter().collect(); + let reservation = reservation.into_iter().collect(); + + Self { + admin, + battle_name, + time_creation, + bid, + participants, + defeated_participants, + state, + pairs, + players_to_pairs, + reservation, + pair_id, + waiting_player, + } + } +} + +pub enum BattleResult { + PlayerWin(ActorId), + Draw(ActorId, ActorId), +} + +#[derive(Default, Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Appearance { + head_index: u16, + hat_index: u16, + body_index: u16, + accessory_index: u16, + body_color: String, + back_color: String, +} diff --git a/contracts/battle/app/src/services/mod.rs b/contracts/battle/app/src/services/mod.rs new file mode 100644 index 000000000..c7f00d70e --- /dev/null +++ b/contracts/battle/app/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod game; +pub mod utils; diff --git a/contracts/battle/app/src/services/utils.rs b/contracts/battle/app/src/services/utils.rs new file mode 100644 index 000000000..9dee576e1 --- /dev/null +++ b/contracts/battle/app/src/services/utils.rs @@ -0,0 +1,13 @@ +use core::fmt::Debug; +use gstd::{ext, format}; + +pub fn panicking Result>(f: F) -> T { + match f() { + Ok(v) => v, + Err(e) => panic(e), + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} diff --git a/contracts/battle/build.rs b/contracts/battle/build.rs new file mode 100644 index 000000000..050e2acf5 --- /dev/null +++ b/contracts/battle/build.rs @@ -0,0 +1,23 @@ +use std::{ + env, + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; + +fn main() { + sails_rs::build_wasm(); + + if env::var("__GEAR_WASM_BUILDER_NO_BUILD").is_ok() { + return; + } + + let bin_path_file = File::open(".binpath").unwrap(); + let mut bin_path_reader = BufReader::new(bin_path_file); + let mut bin_path = String::new(); + bin_path_reader.read_line(&mut bin_path).unwrap(); + + let mut idl_path = PathBuf::from(bin_path); + idl_path.set_extension("idl"); + sails_idl_gen::generate_idl_to_file::(idl_path).unwrap(); +} diff --git a/contracts/battle/client/Cargo.toml b/contracts/battle/client/Cargo.toml new file mode 100644 index 000000000..26df018fc --- /dev/null +++ b/contracts/battle/client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "battle-client" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +mockall = { version = "0.12", optional = true } +sails-rs.workspace = true + +[build-dependencies] +battle-app = { path = "../app" } +sails-client-gen.workspace = true +sails-idl-gen.workspace = true + +[features] +mocks = ["sails-rs/mockall", "dep:mockall"] diff --git a/contracts/battle/client/build.rs b/contracts/battle/client/build.rs new file mode 100644 index 000000000..433266a93 --- /dev/null +++ b/contracts/battle/client/build.rs @@ -0,0 +1,16 @@ +use sails_client_gen::ClientGenerator; +use std::{env, path::PathBuf}; + +fn main() { + let out_dir_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let idl_file_path = out_dir_path.join("battle.idl"); + + // Generate IDL file for the program + sails_idl_gen::generate_idl_to_file::(&idl_file_path).unwrap(); + + // Generate client code from IDL file + ClientGenerator::from_idl_path(&idl_file_path) + .with_mocks("mocks") + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("battle_client.rs")) + .unwrap(); +} diff --git a/contracts/battle/client/src/lib.rs b/contracts/battle/client/src/lib.rs new file mode 100644 index 000000000..47decc3c7 --- /dev/null +++ b/contracts/battle/client/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] +#![allow(clippy::too_many_arguments)] +// Incorporate code generated based on the IDL file +include!(concat!(env!("OUT_DIR"), "/battle_client.rs")); diff --git a/contracts/battle/src/lib.rs b/contracts/battle/src/lib.rs new file mode 100644 index 000000000..f7dc0bbd0 --- /dev/null +++ b/contracts/battle/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] + +#[cfg(target_arch = "wasm32")] +pub use battle_app::wasm::*; + +#[cfg(feature = "wasm-binary")] +#[cfg(not(target_arch = "wasm32"))] +pub use code::WASM_BINARY_OPT as WASM_BINARY; + +#[cfg(feature = "wasm-binary")] +#[cfg(not(target_arch = "wasm32"))] +mod code { + include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +} diff --git a/contracts/battle/tests/gtest.rs b/contracts/battle/tests/gtest.rs new file mode 100644 index 000000000..28f2160ae --- /dev/null +++ b/contracts/battle/tests/gtest.rs @@ -0,0 +1,456 @@ +use battle_client::{traits::*, Appearance, Config, Move}; +use gstd::errors::{ErrorReplyReason, SimpleExecutionError}; +use gtest::{Program, System}; +use sails_rs::errors::{Error, RtlError}; +use sails_rs::{calls::*, gtest::calls::*, ActorId, Encode}; + +const USER_1: u64 = 100; +const USER_2: u64 = 101; +const USER_3: u64 = 102; + +fn init_warrior(system: &System, user: u64) -> ActorId { + let warrior = Program::from_file( + system, + "../target/wasm32-unknown-unknown/release/warrior_wasm.opt.wasm", + ); + let request = ["New".encode(), ("link".to_string()).encode()].concat(); + + let mid = warrior.send_bytes(user, request); + let res = system.run_next_block(); + assert!(res.succeed.contains(&mid)); + warrior.id() +} + +#[tokio::test] +async fn test() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_1, 100_000_000_000_000); + system.mint_to(USER_2, 100_000_000_000_000); + system.mint_to(USER_3, 100_000_000_000_000); + + let remoting = GTestRemoting::new(system, USER_1.into()); + + // Submit program code into the system + let program_code_id = remoting.system().submit_code(battle::WASM_BINARY); + + let program_factory = battle_client::BattleFactory::new(remoting.clone()); + + let program_id = program_factory + .new(Config { + health: 100, + max_participants: 10, + attack_range: (10, 20), + defence_range: (0, 10), + dodge_range: (0, 10), + available_points: 20, + time_for_move_in_blocks: 20, + block_duration_ms: 3_000, + gas_for_create_warrior: 10_000_000_000, + gas_to_cancel_the_battle: 10_000_000_000, + reservation_amount: 500_000_000_000, + reservation_time: 1_000, + time_to_cancel_the_battle: 10_000, + }) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let mut service_client = battle_client::Battle::new(remoting.clone()); + let warrior_id = init_warrior(remoting.system(), USER_1); + + service_client + .create_new_battle( + "Battle".to_string(), + "Warrior_1".to_string(), + Some(warrior_id), + None, + 15, + 10, + 5, + ) + .with_value(10_000_000_000) + .send_recv(program_id) + .await + .unwrap(); + + let warrior_id = init_warrior(remoting.system(), USER_2); + println!("warrior_id {:?}", warrior_id); + service_client + .register( + remoting.actor_id(), + None, + Some(Appearance { + head_index: 1, + hat_index: 2, + body_index: 3, + accessory_index: 4, + body_color: "#008000".to_string(), + back_color: "#0000FF".to_string(), + }), + "Warrior_2".to_string(), + 15, + 10, + 5, + ) + .with_value(10_000_000_000) + .with_args(GTestArgs::new(USER_2.into())) + .send_recv(program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + let warrior_id = init_warrior(remoting.system(), USER_3); + println!("warrior_id {:?}", warrior_id); + service_client + .register( + remoting.actor_id(), + None, + Some(Appearance { + head_index: 1, + hat_index: 2, + body_index: 3, + accessory_index: 4, + body_color: "#008000".to_string(), + back_color: "#0000FF".to_string(), + }), + "Warrior_3".to_string(), + 15, + 10, + 5, + ) + .with_value(10_000_000_000) + .with_args(GTestArgs::new(USER_3.into())) + .send_recv(program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + service_client + .start_battle() + .send_recv(program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + make_move(&mut service_client, Move::Attack, USER_1, program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n block {:?}", remoting.system().block_height()); + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 20); + + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 150); + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + service_client + .start_next_fight() + .with_args(GTestArgs::new(USER_2.into())) + .send_recv(program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 100); + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); +} + +#[tokio::test] +async fn second_test() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_1, 100_000_000_000_000); + system.mint_to(USER_2, 100_000_000_000_000); + system.mint_to(USER_3, 100_000_000_000_000); + + let remoting = GTestRemoting::new(system, USER_1.into()); + + // Submit program code into the system + let program_code_id = remoting.system().submit_code(battle::WASM_BINARY); + + let program_factory = battle_client::BattleFactory::new(remoting.clone()); + + let program_id = program_factory + .new(Config { + health: 100, + max_participants: 10, + attack_range: (10, 20), + defence_range: (0, 10), + dodge_range: (0, 10), + available_points: 20, + time_for_move_in_blocks: 20, + block_duration_ms: 3_000, + gas_for_create_warrior: 10_000_000_000, + gas_to_cancel_the_battle: 10_000_000_000, + reservation_amount: 500_000_000_000, + reservation_time: 1_000, + time_to_cancel_the_battle: 10_000, + }) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let mut service_client = battle_client::Battle::new(remoting.clone()); + + let warrior_id = init_warrior(remoting.system(), USER_1); + service_client + .create_new_battle( + "Battle".to_string(), + "Warrior_1".to_string(), + Some(warrior_id), + None, + 20, + 5, + 5, + ) + .with_value(10_000_000_000) + .send_recv(program_id) + .await + .unwrap(); + + service_client + .register( + remoting.actor_id(), + None, + Some(Appearance { + head_index: 1, + hat_index: 2, + body_index: 3, + accessory_index: 4, + body_color: "#008000".to_string(), + back_color: "#0000FF".to_string(), + }), + "Warrior_2".to_string(), + 15, + 8, + 7, + ) + .with_value(10_000_000_000) + .with_args(GTestArgs::new(USER_2.into())) + .send_recv(program_id) + .await + .unwrap(); + + println!( + "\n start_battle block {:?}", + remoting.system().block_height() + ); + service_client + .start_battle() + .send_recv(program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 5); + + println!("\n make_move block {:?}", remoting.system().block_height()); + + make_move(&mut service_client, Move::Ultimate, USER_1, program_id) + .await + .unwrap(); + make_move(&mut service_client, Move::Reflect, USER_2, program_id) + .await + .unwrap(); + + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 20); + + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 100); + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + + println!("\n RES {:?}", result); + + remoting + .system() + .run_to_block(remoting.system().block_height() + 20); + println!("\n block {:?}", remoting.system().block_height()); + let result = get_battle(&service_client, remoting.actor_id(), program_id).await; + println!("\n RES {:?}", result); +} + +#[tokio::test] +async fn test_error() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_1, 100_000_000_000_000); + system.mint_to(USER_2, 100_000_000_000_000); + system.mint_to(USER_3, 100_000_000_000_000); + + let remoting = GTestRemoting::new(system, USER_1.into()); + + // Submit program code into the system + let program_code_id = remoting.system().submit_code(battle::WASM_BINARY); + + let program_factory = battle_client::BattleFactory::new(remoting.clone()); + + let program_id = program_factory + .new(Config { + health: 100, + max_participants: 10, + attack_range: (10, 20), + defence_range: (0, 10), + dodge_range: (0, 10), + available_points: 20, + time_for_move_in_blocks: 20, + block_duration_ms: 3_000, + gas_for_create_warrior: 10_000_000_000, + gas_to_cancel_the_battle: 10_000_000_000, + reservation_amount: 500_000_000_000, + reservation_time: 1_000, + time_to_cancel_the_battle: 10_000, + }) + .send_recv(program_code_id, b"salt") + .await + .unwrap(); + + let mut service_client = battle_client::Battle::new(remoting.clone()); + + let warrior_id = init_warrior(remoting.system(), USER_1); + service_client + .create_new_battle( + "Battle".to_string(), + "Warrior_1".to_string(), + Some(warrior_id), + None, + 15, + 10, + 5, + ) + .with_value(10_000_000_000) + .send_recv(program_id) + .await + .unwrap(); + + service_client + .register( + remoting.actor_id(), + None, + Some(Appearance { + head_index: 1, + hat_index: 2, + body_index: 3, + accessory_index: 4, + body_color: "#008000".to_string(), + back_color: "#0000FF".to_string(), + }), + "Warrior_2".to_string(), + 15, + 10, + 5, + ) + .with_value(10_000_000_000) + .with_args(GTestArgs::new(USER_2.into())) + .send_recv(program_id) + .await + .unwrap(); + + println!( + "\n start_battle block {:?}", + remoting.system().block_height() + ); + service_client + .start_battle() + .send_recv(program_id) + .await + .unwrap(); + + make_move(&mut service_client, Move::Ultimate, USER_1, program_id) + .await + .unwrap(); + make_move(&mut service_client, Move::Reflect, USER_2, program_id) + .await + .unwrap(); + + let res = make_move(&mut service_client, Move::Ultimate, USER_1, program_id).await; + check_result(res, "Panic occurred: UltimateReload".to_string()); + + let res = make_move(&mut service_client, Move::Reflect, USER_2, program_id).await; + check_result(res, "Panic occurred: ReflectReload".to_string()); +} + +async fn make_move( + service_client: &mut battle_client::Battle, + turn: battle_client::Move, + user: u64, + program_id: ActorId, +) -> Result<(), Error> { + service_client + .make_move(turn) + .with_args(GTestArgs::new(user.into())) + .send_recv(program_id) + .await +} + +async fn get_battle( + service_client: &battle_client::Battle, + game_id: ActorId, + program_id: ActorId, +) -> Option { + service_client + .get_battle(game_id) + .recv(program_id) + .await + .unwrap() +} + +fn check_result(result: Result<(), Error>, error_string: String) { + assert!(matches!( + result, + Err(sails_rs::errors::Error::Rtl(RtlError::ReplyHasError( + ErrorReplyReason::Execution(SimpleExecutionError::UserspacePanic), + message + ))) if message == error_string + )); +} diff --git a/contracts/battle/warrior/README.md b/contracts/battle/warrior/README.md new file mode 100644 index 000000000..52b03f773 --- /dev/null +++ b/contracts/battle/warrior/README.md @@ -0,0 +1,7 @@ +## The **warrior** program + +### 🏗️ Building + +```sh +cargo b -r -p "warrior*" +``` diff --git a/contracts/battle/warrior/app/Cargo.toml b/contracts/battle/warrior/app/Cargo.toml new file mode 100644 index 000000000..933414fba --- /dev/null +++ b/contracts/battle/warrior/app/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "warrior-app" +version = "0.1.0" +edition = "2021" + +[dependencies] +sails-rs = "0.6.0" diff --git a/contracts/battle/warrior/app/src/lib.rs b/contracts/battle/warrior/app/src/lib.rs new file mode 100644 index 000000000..20b9aa825 --- /dev/null +++ b/contracts/battle/warrior/app/src/lib.rs @@ -0,0 +1,86 @@ +#![no_std] +#![allow(clippy::new_without_default)] +use sails_rs::gstd::msg; +use sails_rs::prelude::*; + +#[derive(Debug)] +struct WarriorStorage { + owner: ActorId, + appearance: Appearance, +} + +#[derive(Debug, TypeInfo, Encode)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +struct Appearance { + head_index: u16, + hat_index: u16, + body_index: u16, + accessory_index: u16, + body_color: String, + back_color: String, +} + +static mut STORAGE: Option = None; + +struct WarriorService(()); + +impl WarriorService { + pub fn init() -> Self { + unsafe { + STORAGE = Some(WarriorStorage { + owner: msg::source(), + // YOUR CODE HERE: fill in the remaining fields + // appearance: Appearance { + // head_index: .., + // hat_index: .., + // body_index: .., + // accessory_index: .., + // body_color: .., + // back_color: .., + // } + // + // For example: + appearance: Appearance { + head_index: 1, + hat_index: 2, + body_index: 3, + accessory_index: 4, + body_color: "#008000".to_string(), + back_color: "#0000FF".to_string(), + }, + }); + } + Self(()) + } + pub fn get_warrior_storage(&self) -> &'static WarriorStorage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[sails_rs::service] +impl WarriorService { + pub fn new() -> Self { + Self(()) + } + pub fn get_owner(&self) -> ActorId { + self.get_warrior_storage().owner + } + pub fn get_appearance(&self) -> &'static Appearance { + &self.get_warrior_storage().appearance + } +} + +pub struct WarriorProgram(()); + +#[sails_rs::program] +impl WarriorProgram { + pub fn new() -> Self { + WarriorService::init(); + Self(()) + } + + pub fn warrior(&self) -> WarriorService { + WarriorService::new() + } +} diff --git a/contracts/battle/warrior/wasm/Cargo.toml b/contracts/battle/warrior/wasm/Cargo.toml new file mode 100644 index 000000000..52e45680a --- /dev/null +++ b/contracts/battle/warrior/wasm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "warrior-wasm" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +warrior-app = { path = "../app" } +sails-rs.workspace = true + +[build-dependencies] +gear-wasm-builder.workspace = true +sails-idl-gen.workspace = true +warrior-app = { path = "../app" } + +[lib] +crate-type = ["rlib"] +name = "warrior_wasm" diff --git a/contracts/battle/warrior/wasm/build.rs b/contracts/battle/warrior/wasm/build.rs new file mode 100644 index 000000000..8f09df7f0 --- /dev/null +++ b/contracts/battle/warrior/wasm/build.rs @@ -0,0 +1,15 @@ +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; +use warrior_app::WarriorProgram; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("warrior.idl"); + + let idl_file = File::create(idl_file_path.clone()).unwrap(); + + program::generate_idl::(idl_file).unwrap(); +} diff --git a/contracts/battle/warrior/wasm/src/lib.rs b/contracts/battle/warrior/wasm/src/lib.rs new file mode 100644 index 000000000..318208bf0 --- /dev/null +++ b/contracts/battle/warrior/wasm/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +#[cfg(target_arch = "wasm32")] +pub use warrior_app::wasm::*; diff --git a/contracts/battle/warrior/wasm/warrior.idl b/contracts/battle/warrior/wasm/warrior.idl new file mode 100644 index 000000000..ce8651b09 --- /dev/null +++ b/contracts/battle/warrior/wasm/warrior.idl @@ -0,0 +1,18 @@ +type Appearance = struct { + head_index: u16, + hat_index: u16, + body_index: u16, + accessory_index: u16, + body_color: str, + back_color: str, +}; + +constructor { + New : (); +}; + +service Warrior { + query GetAppearance : () -> Appearance; + query GetOwner : () -> actor_id; +}; + diff --git a/contracts/concert/README.md b/contracts/concert/README.md new file mode 100644 index 000000000..4eb83e150 --- /dev/null +++ b/contracts/concert/README.md @@ -0,0 +1,13 @@ +# Sails Concert + +### 🏗️ Building + +```sh +cargo b -r -p "concert" +``` + +### ✅ Testing + +```sh +cargo t -r -p "concert-app" +``` diff --git a/contracts/concert/app/Cargo.toml b/contracts/concert/app/Cargo.toml new file mode 100644 index 000000000..c665bd78d --- /dev/null +++ b/contracts/concert/app/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "concert-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs = { workspace = true, features = ["gtest"] } + +[dev-dependencies] +gclient.workspace = true +concert = { path = "../wasm" } +tokio = "1" +extended_vmt_wasm = { git = "https://github.com/gear-foundation/standards/"} + diff --git a/contracts/concert/app/src/lib.rs b/contracts/concert/app/src/lib.rs new file mode 100644 index 000000000..9b480b175 --- /dev/null +++ b/contracts/concert/app/src/lib.rs @@ -0,0 +1,352 @@ +#![no_std] + +use core::fmt::Debug; +use gstd::{ext, format}; +use sails_rs::gstd::msg; +use sails_rs::{ + collections::{HashMap, HashSet}, + prelude::*, +}; + +const ZERO_ID: ActorId = ActorId::zero(); +const NFT_COUNT: U256 = U256::one(); + +#[derive(Default, Clone)] +pub struct Storage { + owner_id: ActorId, + contract_id: ActorId, + name: String, + description: String, + ticket_ft_id: U256, + creator: ActorId, + number_of_tickets: U256, + tickets_left: U256, + date: u128, + buyers: HashSet, + id_counter: U256, + concert_id: U256, + running: bool, + metadata: HashMap>>, + token_id: U256, +} + +#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct TokenMetadata { + pub title: Option, + pub description: Option, + pub media: Option, + pub reference: Option, +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + Creation { + creator: ActorId, + concert_id: U256, + number_of_tickets: U256, + date: u128, + }, + Hold { + concert_id: U256, + }, + Purchase { + concert_id: U256, + amount: U256, + }, +} + +#[derive(Debug)] +pub enum ConcertError { + AlreadyRegistered, + ZeroAddress, + LessThanOneTicket, + NotEnoughTickets, + NotEnoughMetadata, + NotCreator, +} + +struct ConcertService(()); + +impl ConcertService { + pub fn init(owner_id: ActorId, vmt_contract: ActorId) -> Self { + let storage = Storage { + owner_id, + contract_id: vmt_contract, + ..Default::default() + }; + unsafe { STORAGE = Some(storage) }; + Self(()) + } + pub fn get_mut(&mut self) -> &'static mut Storage { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn get(&self) -> &'static Storage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl ConcertService { + pub fn new() -> Self { + Self(()) + } + pub fn create( + &mut self, + creator: ActorId, + name: String, + description: String, + number_of_tickets: U256, + date: u128, + token_id: U256, + ) { + let storage = self.get_mut(); + if storage.running { + panic(ConcertError::AlreadyRegistered); + } + storage.creator = creator; + storage.concert_id = storage.id_counter; + storage.ticket_ft_id = storage.concert_id; + storage.name = name; + storage.description = description; + storage.number_of_tickets = number_of_tickets; + storage.date = date; + storage.running = true; + storage.tickets_left = number_of_tickets; + storage.token_id = token_id; + + self.notify_on(Event::Creation { + creator, + concert_id: storage.concert_id, + number_of_tickets, + date, + }) + .expect("Notification Error"); + } + + pub async fn hold_concert(&mut self) { + let storage = self.get_mut(); + let msg_src = msg::source(); + if msg_src != storage.creator { + panic(ConcertError::NotCreator); + } + // get balances from a contract + let accounts: Vec<_> = storage.buyers.clone().into_iter().collect(); + let tokens: Vec = iter::repeat(storage.token_id) + .take(accounts.len()) + .collect(); + + let request = [ + "Vmt".encode(), + "BalanceOfBatch".to_string().encode(), + (accounts.clone(), tokens.clone()).encode(), + ] + .concat(); + + let bytes_reply_balances = msg::send_bytes_for_reply(storage.contract_id, request, 0, 0) + .expect("Error in async message to Mtk contract") + .await + .expect("CONCERT: Error getting balances from the contract"); + + let (_, _, balances) = + <(String, String, Vec)>::decode(&mut bytes_reply_balances.as_ref()) + .expect("Unable to decode reply"); + // we know each user balance now + for (i, balance) in balances.iter().enumerate() { + let request = [ + "Vmt".encode(), + "Burn".to_string().encode(), + (msg_src, tokens[i], balance).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(storage.contract_id, request, 0, 0) + .expect("Error in async message to Mtk contract") + .await + .expect("CONCERT: Error burning balances"); + } + + for actor in &storage.buyers { + let actor_metadata = storage.metadata.get(actor); + if let Some(actor_md) = actor_metadata.cloned() { + let mut ids: Vec = Vec::with_capacity(actor_md.len()); + let amounts: Vec = vec![NFT_COUNT; actor_md.len()]; + let mut meta = vec![]; + for (token, token_meta) in actor_md { + ids.push(token); + meta.push(token_meta); + } + + let request = [ + "Vmt".encode(), + "MintBatch".to_string().encode(), + (actor, ids, amounts, meta).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(storage.contract_id, request, 0, 0) + .expect("Error in async message to Mtk contract") + .await + .expect("CONCERT: Error minting tickets"); + } + } + storage.running = false; + + self.notify_on(Event::Hold { + concert_id: storage.concert_id, + }) + .expect("Notification Error"); + } + + pub async fn buy_tickets(&mut self, amount: U256, mtd: Vec>) { + let storage = self.get_mut(); + let msg_src = msg::source(); + if msg_src == ZERO_ID { + panic(ConcertError::ZeroAddress); + } + + if amount < U256::one() { + panic(ConcertError::LessThanOneTicket); + } + + if storage.tickets_left < amount { + panic(ConcertError::NotEnoughTickets); + } + + if U256::from(mtd.len()) != amount { + panic(ConcertError::NotEnoughMetadata); + } + + for meta in mtd { + storage.id_counter += U256::one(); + storage + .metadata + .entry(msg_src) + .or_default() + .insert(storage.id_counter + U256::one(), meta); + } + + storage.buyers.insert(msg_src); + storage.tickets_left -= amount; + let request = [ + "Vmt".encode(), + "Mint".to_string().encode(), + (msg_src, storage.token_id, amount, None::).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(storage.contract_id, request, 0, 0) + .expect("Error in async message to Mtk contract") + .await + .expect("CONCERT: Error minting concert tokens"); + + self.notify_on(Event::Purchase { + concert_id: storage.concert_id, + amount, + }) + .expect("Notification Error"); + } + + pub fn get_storage(&self) -> State { + self.get().clone().into() + } +} + +pub struct ConcertProgram(()); + +#[sails_rs::program] +impl ConcertProgram { + #[allow(clippy::new_without_default)] + pub fn new(owner_id: ActorId, vmt_contract: ActorId) -> Self { + ConcertService::init(owner_id, vmt_contract); + Self(()) + } + + pub fn concert(&self) -> ConcertService { + ConcertService::new() + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} + +pub type Tickets = Vec<(U256, Option)>; + +#[derive(Debug, Default, PartialEq, Eq, Encode, Decode, TypeInfo)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct State { + pub owner_id: ActorId, + pub contract_id: ActorId, + + pub name: String, + pub description: String, + + pub ticket_ft_id: U256, + pub creator: ActorId, + pub number_of_tickets: U256, + pub tickets_left: U256, + pub date: u128, + + pub buyers: Vec, + + pub id_counter: U256, + pub concert_id: U256, + pub running: bool, + /// user to token id to metadata + pub metadata: Vec<(ActorId, Tickets)>, + pub token_id: U256, +} + +impl From for State { + fn from(value: Storage) -> Self { + let Storage { + owner_id, + contract_id, + name, + description, + ticket_ft_id, + creator, + number_of_tickets, + tickets_left, + date, + buyers, + id_counter, + concert_id, + running, + metadata, + token_id, + } = value; + + let buyers = buyers.into_iter().collect(); + + let metadata = metadata + .into_iter() + .map(|(k, v)| (k, v.into_iter().collect())) + .collect(); + + State { + owner_id, + contract_id, + name, + description, + ticket_ft_id, + creator, + number_of_tickets, + tickets_left, + date, + buyers, + id_counter, + concert_id, + running, + metadata, + token_id, + } + } +} diff --git a/contracts/concert/app/tests/test.rs b/contracts/concert/app/tests/test.rs new file mode 100644 index 000000000..e52260af1 --- /dev/null +++ b/contracts/concert/app/tests/test.rs @@ -0,0 +1,235 @@ +use concert::{ + traits::{Concert, ConcertFactory}, + Concert as ConcertClient, ConcertFactory as Factory, TokenMetadata, +}; +use sails_rs::gtest::{calls::*, System}; +use sails_rs::{calls::*, gtest::Program, ActorId, Decode, Encode, U256}; + +pub const USER_ID: u64 = 10; +pub const TOKEN_ID: U256 = U256::one(); +pub const CONCERT_ID: U256 = U256::zero(); +pub const AMOUNT: U256 = U256::one(); +pub const DATE: u128 = 100000; + +fn init_multitoken(sys: &System) -> (ActorId, Program<'_>) { + let vmt = Program::from_file( + sys, + "../../target/wasm32-unknown-unknown/release/extended_vmt_wasm.opt.wasm", + ); + let payload = ("Name".to_string(), "Symbol".to_string(), 10_u8); + let encoded_request = ["New".encode(), payload.encode()].concat(); + let mid = vmt.send_bytes(USER_ID, encoded_request); + let res = sys.run_next_block(); + assert!(res.succeed.contains(&mid)); + + (vmt.id(), vmt) +} +fn grant_roles(sys: &System, vmt: &Program, concert_id: ActorId) { + let encoded_request = [ + "Vmt".encode(), + "GrantMinterRole".encode(), + (concert_id).encode(), + ] + .concat(); + let mid = vmt.send_bytes(USER_ID, encoded_request); + let res = sys.run_next_block(); + assert!(res.succeed.contains(&mid)); + + let encoded_request = [ + "Vmt".encode(), + "GrantBurnerRole".encode(), + (concert_id).encode(), + ] + .concat(); + let mid = vmt.send_bytes(USER_ID, encoded_request); + let res = sys.run_next_block(); + assert!(res.succeed.contains(&mid)); +} + +fn get_balance(sys: &System, vmt: &Program, account: ActorId, id: U256) -> U256 { + let encoded_request = ["Vmt".encode(), "BalanceOf".encode(), (account, id).encode()].concat(); + let mid = vmt.send_bytes(USER_ID, encoded_request); + let res = sys.run_next_block(); + assert!(res.succeed.contains(&mid)); + + let (_, _, balance) = <(String, String, U256)>::decode(&mut res.log[0].payload()) + .expect("Unable to decode reply"); + balance +} +#[tokio::test] +async fn create_concert() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_ID, 100_000_000_000_000); + let program_space = GTestRemoting::new(system, USER_ID.into()); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/concert.opt.wasm"); + + let concert_factory = Factory::new(program_space.clone()); + let (vmt_id, _vmt_program) = init_multitoken(program_space.system()); + let concert_id = concert_factory + .new(USER_ID.into(), vmt_id) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = ConcertClient::new(program_space); + // create + client + .create( + USER_ID.into(), + String::from("Sum 41"), + String::from("Sum 41 concert in Madrid. 26/08/2022"), + U256::from(100), + DATE, + TOKEN_ID, + ) + .send_recv(concert_id) + .await + .unwrap(); + // check state + let state = client.get_storage().recv(concert_id).await.unwrap(); + + assert_eq!(state.name, "Sum 41".to_string()); + assert_eq!( + state.description, + "Sum 41 concert in Madrid. 26/08/2022".to_string() + ); + assert_eq!(state.date, DATE); + assert_eq!(state.tickets_left, U256::from(100)); +} + +#[tokio::test] +async fn buy_tickets() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_ID, 100_000_000_000_000); + let program_space = GTestRemoting::new(system, USER_ID.into()); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/concert.opt.wasm"); + + let concert_factory = Factory::new(program_space.clone()); + let (vmt_id, vmt_program) = init_multitoken(program_space.system()); + let concert_id = concert_factory + .new(USER_ID.into(), vmt_id) + .send_recv(code_id, "123") + .await + .unwrap(); + grant_roles(program_space.system(), &vmt_program, concert_id); + let mut client = ConcertClient::new(program_space.clone()); + // create + client + .create( + USER_ID.into(), + String::from("Sum 41"), + String::from("Sum 41 concert in Madrid. 26/08/2022"), + U256::from(100), + DATE, + TOKEN_ID, + ) + .send_recv(concert_id) + .await + .unwrap(); + + let metadata = vec![Some(TokenMetadata { + title: Some(String::from("Sum 41 concert in Madrid 26/08/2022")), + description: Some(String::from( + "Sum 41 Madrid 26/08/2022 Ticket. Row 4. Seat 4.", + )), + media: Some(String::from("sum41.com")), + reference: Some(String::from("UNKNOWN")), + })]; + // buy tickets + client + .buy_tickets(AMOUNT, metadata) + .send_recv(concert_id) + .await + .unwrap(); + + // check state + let state = client.get_storage().recv(concert_id).await.unwrap(); + + assert_eq!(state.buyers, vec![USER_ID.into()]); + assert_eq!(state.tickets_left, U256::from(99)); + assert_eq!(state.metadata[0].0, USER_ID.into()); + let balance = get_balance( + program_space.system(), + &vmt_program, + USER_ID.into(), + TOKEN_ID, + ); + assert_eq!(balance, 1.into()); +} + +#[tokio::test] +async fn hold_concert() { + let system = System::new(); + system.init_logger(); + system.mint_to(USER_ID, 100_000_000_000_000); + let program_space = GTestRemoting::new(system, USER_ID.into()); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/concert.opt.wasm"); + + let concert_factory = Factory::new(program_space.clone()); + let (vmt_id, vmt_program) = init_multitoken(program_space.system()); + let concert_id = concert_factory + .new(USER_ID.into(), vmt_id) + .send_recv(code_id, "123") + .await + .unwrap(); + grant_roles(program_space.system(), &vmt_program, concert_id); + let mut client = ConcertClient::new(program_space.clone()); + // create + client + .create( + USER_ID.into(), + String::from("Sum 41"), + String::from("Sum 41 concert in Madrid. 26/08/2022"), + U256::from(100), + DATE, + TOKEN_ID, + ) + .send_recv(concert_id) + .await + .unwrap(); + + let metadata = vec![Some(TokenMetadata { + title: Some(String::from("Sum 41 concert in Madrid 26/08/2022")), + description: Some(String::from( + "Sum 41 Madrid 26/08/2022 Ticket. Row 4. Seat 4.", + )), + media: Some(String::from("sum41.com")), + reference: Some(String::from("UNKNOWN")), + })]; + // buy tickets + client + .buy_tickets(AMOUNT, metadata) + .send_recv(concert_id) + .await + .unwrap(); + + // hold concert + client.hold_concert().send_recv(concert_id).await.unwrap(); + + // check state + let state = client.get_storage().recv(concert_id).await.unwrap(); + + assert!(!state.running); + let balance = get_balance( + program_space.system(), + &vmt_program, + USER_ID.into(), + TOKEN_ID, + ); + assert_eq!(balance, 0.into()); + let balance = get_balance( + program_space.system(), + &vmt_program, + USER_ID.into(), + TOKEN_ID + 1, + ); + assert_eq!(balance, 1.into()); +} diff --git a/contracts/concert/wasm/Cargo.toml b/contracts/concert/wasm/Cargo.toml new file mode 100644 index 000000000..21add8a8e --- /dev/null +++ b/contracts/concert/wasm/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "concert" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +concert-app = { path = "../app" } +sails-rs.workspace = true + +[build-dependencies] +gear-wasm-builder.workspace = true +sails-idl-gen.workspace = true +sails-client-gen.workspace = true +concert-app = { path = "../app" } + +[lib] +crate-type = ["rlib"] +name = "concert" diff --git a/contracts/concert/wasm/build.rs b/contracts/concert/wasm/build.rs new file mode 100644 index 000000000..3a309f9f5 --- /dev/null +++ b/contracts/concert/wasm/build.rs @@ -0,0 +1,20 @@ +use concert_app::ConcertProgram; +use sails_client_gen::ClientGenerator; +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("concert.idl"); + + let idl_file = File::create(idl_file_path.clone()).unwrap(); + + program::generate_idl::(idl_file).unwrap(); + + ClientGenerator::from_idl_path(&idl_file_path) + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("concert_client.rs")) + .unwrap(); +} diff --git a/contracts/concert/wasm/concert.idl b/contracts/concert/wasm/concert.idl new file mode 100644 index 000000000..14f9915c9 --- /dev/null +++ b/contracts/concert/wasm/concert.idl @@ -0,0 +1,43 @@ +type TokenMetadata = struct { + title: opt str, + description: opt str, + media: opt str, + reference: opt str, +}; + +type State = struct { + owner_id: actor_id, + contract_id: actor_id, + name: str, + description: str, + ticket_ft_id: u256, + creator: actor_id, + number_of_tickets: u256, + tickets_left: u256, + date: u128, + buyers: vec actor_id, + id_counter: u256, + concert_id: u256, + running: bool, + /// user to token id to metadata + metadata: vec struct { actor_id, vec struct { u256, opt TokenMetadata } }, + token_id: u256, +}; + +constructor { + New : (owner_id: actor_id, vmt_contract: actor_id); +}; + +service Concert { + BuyTickets : (amount: u256, mtd: vec opt TokenMetadata) -> null; + Create : (creator: actor_id, name: str, description: str, number_of_tickets: u256, date: u128, token_id: u256) -> null; + HoldConcert : () -> null; + query GetStorage : () -> State; + + events { + Creation: struct { creator: actor_id, concert_id: u256, number_of_tickets: u256, date: u128 }; + Hold: struct { concert_id: u256 }; + Purchase: struct { concert_id: u256, amount: u256 }; + } +}; + diff --git a/contracts/concert/wasm/src/lib.rs b/contracts/concert/wasm/src/lib.rs new file mode 100644 index 000000000..04e0c281b --- /dev/null +++ b/contracts/concert/wasm/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] +#![allow(clippy::type_complexity)] + +include!(concat!(env!("OUT_DIR"), "/concert_client.rs")); +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +#[cfg(target_arch = "wasm32")] +pub use concert_app::wasm::*; diff --git a/contracts/syndote/Cargo.toml b/contracts/syndote/Cargo.toml deleted file mode 100644 index 38832fe1f..000000000 --- a/contracts/syndote/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "syndote" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -gstd.workspace = true -syndote-io.workspace = true - -[dev-dependencies] -gtest.workspace = true - -# External binaries - -syndote-player.workspace = true - -[build-dependencies] -gear-wasm-builder.workspace = true -syndote-io.workspace = true diff --git a/contracts/syndote/README.md b/contracts/syndote/README.md deleted file mode 100644 index 29087cb63..000000000 --- a/contracts/syndote/README.md +++ /dev/null @@ -1,16 +0,0 @@ -[![Open in Gitpod](https://img.shields.io/badge/Open_in-Gitpod-white?logo=gitpod)](https://gitpod.io/#FOLDER=syndote/https://github.com/gear-foundation/dapps) -[![Docs](https://img.shields.io/github/actions/workflow/status/gear-foundation/dapps/contracts.yml?logo=rust&label=docs)](https://dapps.gear.rs/syndote_io) - -# [Syndote](https://wiki.gear-tech.io/docs/examples/Gaming/monopoly) - -### 🏗️ Building - -```sh -cargo b -r -p "syndote*" -``` - -### ✅ Testing - -```sh -cargo t -r -p "syndote*" -``` diff --git a/contracts/syndote/app/Cargo.toml b/contracts/syndote/app/Cargo.toml new file mode 100644 index 000000000..fcfc98be1 --- /dev/null +++ b/contracts/syndote/app/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "syndote-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs = { workspace = true, features = ["gtest"] } + +[dev-dependencies] +gclient.workspace = true +syndote = { path = "../wasm" } +tokio = "1" diff --git a/contracts/syndote/app/src/lib.rs b/contracts/syndote/app/src/lib.rs new file mode 100644 index 000000000..c7ce7f880 --- /dev/null +++ b/contracts/syndote/app/src/lib.rs @@ -0,0 +1,19 @@ +#![no_std] +#![allow(clippy::new_without_default)] + +use sails_rs::prelude::*; +pub mod services; +use services::game::GameService; +pub struct Program(()); + +#[program] +impl Program { + pub async fn new(dns_id_and_name: Option<(ActorId, String)>) -> Self { + GameService::init(dns_id_and_name).await; + Self(()) + } + + pub fn syndote(&self) -> GameService { + GameService::new() + } +} diff --git a/contracts/syndote/app/src/services/game/funcs.rs b/contracts/syndote/app/src/services/game/funcs.rs new file mode 100644 index 000000000..618c00e65 --- /dev/null +++ b/contracts/syndote/app/src/services/game/funcs.rs @@ -0,0 +1,505 @@ +use crate::services::game::*; +use gstd::{errors::Error, ReservationId, ReservationIdExt}; +use sails_rs::ActorId; + +pub fn register(storage: &mut Storage, player: &ActorId) -> Result { + storage.check_status(GameStatus::Registration)?; + if storage.players.contains_key(player) { + return Err(GameError::AlreadyRegistered); + } + storage.players.insert( + *player, + PlayerInfo { + balance: INITIAL_BALANCE, + ..Default::default() + }, + ); + storage.players_queue.push(*player); + if storage.players_queue.len() == NUMBER_OF_PLAYERS as usize { + storage.game_status = GameStatus::Play; + } + Ok(Event::Registered) +} + +pub fn reserve_gas(storage: &mut Storage) -> Result { + let reservation_id = + ReservationId::reserve(RESERVATION_AMOUNT, 600).expect("reservation across executions"); + + storage.reservations.push(reservation_id); + Ok(Event::GasReserved) +} + +pub fn start_registration(storage: &mut Storage) -> Result { + storage.check_status(GameStatus::Finished)?; + storage.only_admin()?; + let mut game_storage = Storage { + admin: storage.admin, + ..Default::default() + }; + init_properties(&mut game_storage.properties, &mut game_storage.ownership); + *storage = game_storage; + Ok(Event::StartRegistration) +} + +pub async fn play(storage: &mut Storage) -> Result { + //self.check_status(GameStatus::Play); + let msg_src = msg::source(); + if msg_src != storage.admin && msg_src != exec::program_id() { + return Err(GameError::AccessDenied); + } + while storage.game_status == GameStatus::Play { + if storage.players_queue.len() <= 1 { + storage.winner = storage.players_queue[0]; + storage.game_status = GameStatus::Finished; + + return Ok(Event::GameFinished { + winner: storage.winner, + }); + } + if exec::gas_available() <= GAS_FOR_ROUND { + if let Some(id) = storage.reservations.pop() { + let request = + ["Syndote".encode(), "Play".to_string().encode(), ().encode()].concat(); + + msg::send_bytes_from_reservation(id, exec::program_id(), request, 0) + .expect("Error in sending message"); + + return Ok(Event::NextRoundFromReservation); + } else { + panic!("GIVE ME MORE GAS"); + }; + } + // // check penalty and debt of the players for the previous round + // // if penalty is equal to 5 points we remove the player from the game + // // if a player has a debt and he has not enough balance to pay it + // // he is also removed from the game + // bankrupt_and_penalty( + // &self.admin, + // &mut self.players, + // &mut self.players_queue, + // &mut self.properties, + // &mut self.properties_in_bank, + // &mut self.ownership, + // ); + + // if self.players_queue.len() <= 1 { + // self.winner = self.players_queue[0]; + // self.game_status = GameStatus::Finished; + // msg::reply( + // GameEvent::GameFinished { + // winner: self.winner, + // }, + // 0, + // ) + // .expect("Error in sending a reply `GameEvent::GameFinished`"); + // break; + // } + storage.round = storage.round.wrapping_add(1); + for player in storage.players_queue.clone() { + storage.current_player = player; + storage.current_step += 1; + // we save the state before the player's step in case + // the player's contract does not reply or is executed with a panic. + // Then we roll back all the changes that the player could have made. + let mut state = storage.clone(); + let player_info = storage + .players + .get_mut(&player) + .expect("Cant be None: Get Player"); + + // if a player is in jail we don't throw rolls for him + let position = if player_info.in_jail { + player_info.position + } else { + let (r1, r2) = get_rolls(); + // debug!("ROOLS {:?} {:?}", r1, r2); + let roll_sum = r1 + r2; + (player_info.position + roll_sum) % NUMBER_OF_CELLS + }; + // If a player is on a cell that belongs to another player + // we write down a debt on him in the amount of the rent. + // This is done in order to penalize the participant's contract + // if he misses the rent + let account = storage.ownership[position as usize]; + + if account != player && account != ActorId::zero() { + if let Some((_, _, _, rent)) = storage.properties[position as usize] { + player_info.debt = rent; + } + } + player_info.position = position; + player_info.in_jail = position == JAIL_POSITION; + state.players.insert(player, player_info.clone()); + match position { + 0 => { + player_info.balance += NEW_CIRCLE; + player_info.round = storage.round; + } + // free cells (it can be lottery or penalty): TODO as a task on hackathon + 2 | 4 | 7 | 16 | 20 | 30 | 33 | 36 | 38 => { + player_info.round = storage.round; + } + _ => { + let reply = take_your_turn(&player, &state).await; + + if reply.is_err() { + player_info.penalty = PENALTY; + } + } + } + // check penalty and debt of the players for the previous round + // if penalty is equal to 5 points we remove the player from the game + // if a player has a debt and he has not enough balance to pay it + // he is also removed from the game + bankrupt_and_penalty( + &storage.admin, + &mut storage.players, + &mut storage.players_queue, + &storage.properties, + &mut storage.properties_in_bank, + &mut storage.ownership, + ); + + msg::send( + storage.admin, + Event::Step { + players: storage + .players + .iter() + .map(|(key, value)| (*key, value.clone())) + .collect(), + properties: storage.properties.clone(), + current_player: storage.current_player, + current_step: storage.current_step, + ownership: storage.ownership.clone(), + }, + 0, + ) + .expect("Error in sending a message `GameEvent::Step`"); + } + } + Ok(Event::Played) +} + +async fn take_your_turn(player: &ActorId, storage: &Storage) -> Result, Error> { + let players: Vec<_> = storage.players.clone().into_iter().collect(); + + let request = [ + "Player".encode(), + "YourTurn".to_string().encode(), + (players, storage.properties.clone()).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(*player, request, 0, 0) + .expect("Error on sending `YourTurn` message") + .up_to(Some(WAIT_DURATION)) + .expect("Invalid wait duration.") + .await +} + +pub fn throw_roll( + storage: &mut Storage, + pay_fine: bool, + properties_for_sale: Option>, +) -> Result { + storage.only_player()?; + let player_info = + match get_player_info(&storage.current_player, &mut storage.players, storage.round) { + Ok(player_info) => player_info, + Err(_) => { + return Ok(Event::StrategicError); + } + }; + + // If a player is not in the jail + if !player_info.in_jail { + // debug!("PENALTY: PLAYER IS NOT IN JAIL"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + + if let Some(properties) = properties_for_sale { + if sell_property( + &storage.admin, + &mut storage.ownership, + &properties, + &mut storage.properties_in_bank, + &storage.properties, + player_info, + ) + .is_err() + { + return Ok(Event::StrategicError); + }; + } + + let (r1, r2) = get_rolls(); + if r1 == r2 { + player_info.in_jail = false; + player_info.position = r1 + r2; + } else if pay_fine { + if player_info.balance < FINE { + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + player_info.balance -= FINE; + player_info.in_jail = false; + } + player_info.round = storage.round; + Ok(Event::Jail { + in_jail: player_info.in_jail, + position: player_info.position, + }) +} + +pub fn add_gear( + storage: &mut Storage, + properties_for_sale: Option>, +) -> Result { + storage.only_player()?; + let player_info = + match get_player_info(&storage.current_player, &mut storage.players, storage.round) { + Ok(player_info) => player_info, + Err(_) => { + return Ok(Event::StrategicError); + } + }; + + if let Some(properties) = properties_for_sale { + if sell_property( + &storage.admin, + &mut storage.ownership, + &properties, + &mut storage.properties_in_bank, + &storage.properties, + player_info, + ) + .is_err() + { + return Ok(Event::StrategicError); + }; + } + + // if player did not check his balance itself + if player_info.balance < COST_FOR_UPGRADE { + // debug!("PENALTY: NOT ENOUGH BALANCE FOR UPGRADE"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + + let position = player_info.position; + + let gears = if let Some((account, gears, _, _)) = &mut storage.properties[position as usize] { + if account != &msg::source() { + // debug!("PENALTY: TRY TO UPGRADE NOT OWN CELL"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + gears + } else { + player_info.penalty += 1; + return Ok(Event::StrategicError); + }; + + // maximum amount of gear is reached + if gears.len() == 3 { + // debug!("PENALTY: MAXIMUM AMOUNT OF GEARS ON CELL"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + + gears.push(Gear::Bronze); + player_info.balance -= COST_FOR_UPGRADE; + player_info.round = storage.round; + Ok(Event::StrategicSuccess) +} + +pub fn upgrade( + storage: &mut Storage, + properties_for_sale: Option>, +) -> Result { + storage.only_player()?; + let player_info = + match get_player_info(&storage.current_player, &mut storage.players, storage.round) { + Ok(player_info) => player_info, + Err(_) => { + return Ok(Event::StrategicError); + } + }; + + if let Some(properties) = properties_for_sale { + if sell_property( + &storage.admin, + &mut storage.ownership, + &properties, + &mut storage.properties_in_bank, + &storage.properties, + player_info, + ) + .is_err() + { + return Ok(Event::StrategicError); + }; + } + + // if player did not check his balance itself + if player_info.balance < COST_FOR_UPGRADE { + // debug!("PENALTY: NOT ENOUGH BALANCE FOR UPGRADE"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + + let position = player_info.position; + + if let Some((account, gears, price, rent)) = &mut storage.properties[position as usize] { + if account != &msg::source() { + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + // if nothing to upgrade + if gears.is_empty() { + // debug!("PENALTY: NOTHING TO UPGRADE"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + for gear in gears { + if *gear != Gear::Gold { + *gear = gear.upgrade(); + // add 10 percent to the price of cell + *price += *price / 10; + // add 10 percent to the price of rent + *rent += *rent / 10; + break; + } + } + } else { + player_info.penalty += 1; + return Ok(Event::StrategicError); + }; + + player_info.balance -= COST_FOR_UPGRADE; + player_info.round = storage.round; + Ok(Event::StrategicSuccess) +} + +pub fn buy_cell( + storage: &mut Storage, + properties_for_sale: Option>, +) -> Result { + storage.only_player()?; + let player_info = + match get_player_info(&storage.current_player, &mut storage.players, storage.round) { + Ok(player_info) => player_info, + Err(_) => { + return Ok(Event::StrategicError); + } + }; + let position = player_info.position; + + if let Some(properties) = properties_for_sale { + if sell_property( + &storage.admin, + &mut storage.ownership, + &properties, + &mut storage.properties_in_bank, + &storage.properties, + player_info, + ) + .is_err() + { + return Ok(Event::StrategicError); + }; + } + + // if a player on the field that can't be sold (for example, jail) + if let Some((account, _, price, _)) = storage.properties[position as usize].as_mut() { + if account != &mut ActorId::zero() { + // debug!("PENALTY: THAT CELL IS ALREDY BOUGHT"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + // if a player has not enough balance + if player_info.balance < *price { + player_info.penalty += 1; + // debug!("PENALTY: NOT ENOUGH BALANCE FOR BUYING"); + return Ok(Event::StrategicError); + } + player_info.balance -= *price; + *account = msg::source(); + } else { + player_info.penalty += 1; + // debug!("PENALTY: THAT FIELD CAN'T BE SOLD"); + return Ok(Event::StrategicError); + }; + player_info.cells.push(position); + storage.ownership[position as usize] = msg::source(); + player_info.round = storage.round; + Ok(Event::StrategicSuccess) +} + +pub fn pay_rent( + storage: &mut Storage, + properties_for_sale: Option>, +) -> Result { + storage.only_player()?; + let player_info = + match get_player_info(&storage.current_player, &mut storage.players, storage.round) { + Ok(player_info) => player_info, + Err(_) => { + return Ok(Event::StrategicError); + } + }; + if let Some(properties) = properties_for_sale { + if sell_property( + &storage.admin, + &mut storage.ownership, + &properties, + &mut storage.properties_in_bank, + &storage.properties, + player_info, + ) + .is_err() + { + return Ok(Event::StrategicError); + }; + } + + let position = player_info.position; + let account = storage.ownership[position as usize]; + + if account == msg::source() { + player_info.penalty += 1; + // debug!("PENALTY: PAY RENT TO HIMSELF"); + return Ok(Event::StrategicError); + } + + let (_, _, _, rent) = storage.properties[position as usize] + .clone() + .unwrap_or_default(); + if rent == 0 { + // debug!("PENALTY: CELL WITH NO PROPERTIES"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + }; + + if player_info.balance < rent { + // debug!("PENALTY: NOT ENOUGH BALANCE TO PAY RENT"); + player_info.penalty += 1; + return Ok(Event::StrategicError); + } + player_info.balance -= rent; + player_info.debt = 0; + player_info.round = storage.round; + storage + .players + .entry(account) + .and_modify(|player_info| player_info.balance = player_info.balance.saturating_add(rent)); + Ok(Event::StrategicSuccess) +} + +pub fn change_admin(storage: &mut Storage, admin: ActorId) -> Result { + storage.only_admin()?; + storage.admin = admin; + Ok(Event::AdminChanged) +} diff --git a/contracts/syndote/app/src/services/game/mod.rs b/contracts/syndote/app/src/services/game/mod.rs new file mode 100644 index 000000000..6ee8f1754 --- /dev/null +++ b/contracts/syndote/app/src/services/game/mod.rs @@ -0,0 +1,215 @@ +use gstd::{exec, msg, ReservationId}; +use sails_rs::{ + collections::{HashMap, HashSet}, + gstd::service, + prelude::*, +}; +mod funcs; +pub mod utils; +use crate::services; +use utils::*; + +const RESERVATION_AMOUNT: u64 = 245_000_000_000; +const GAS_FOR_ROUND: u64 = 60_000_000_000; + +pub const NUMBER_OF_CELLS: u8 = 40; +pub const NUMBER_OF_PLAYERS: u8 = 4; +pub const JAIL_POSITION: u8 = 10; +pub const COST_FOR_UPGRADE: u32 = 500; +pub const FINE: u32 = 1_000; +pub const PENALTY: u8 = 5; +pub const INITIAL_BALANCE: u32 = 15_000; +pub const NEW_CIRCLE: u32 = 2_000; +pub const WAIT_DURATION: u32 = 5; + +#[derive(Default, Clone)] +pub struct Storage { + admin: ActorId, + properties_in_bank: HashSet, + round: u128, + players: HashMap, + players_queue: Vec, + current_player: ActorId, + current_step: u64, + // mapping from cells to built properties, + properties: Vec>, + // mapping from cells to accounts who have properties on it + ownership: Vec, + game_status: GameStatus, + winner: ActorId, + reservations: Vec, + dns_info: Option<(ActorId, String)>, +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + Registered, + StartRegistration, + Played, + GameFinished { + winner: ActorId, + }, + StrategicError, + StrategicSuccess, + Step { + players: Vec<(ActorId, PlayerInfo)>, + properties: Vec>, + current_player: ActorId, + ownership: Vec, + current_step: u64, + }, + Jail { + in_jail: bool, + position: u8, + }, + GasReserved, + NextRoundFromReservation, + AdminChanged, + Killed { + inheritor: ActorId, + }, +} + +#[derive(Clone)] +pub struct GameService(()); + +impl GameService { + pub async fn init(dns_id_and_name: Option<(ActorId, String)>) -> Self { + unsafe { + let mut storage = Storage { + admin: msg::source(), + dns_info: dns_id_and_name.clone(), + ..Default::default() + }; + init_properties(&mut storage.properties, &mut storage.ownership); + STORAGE = Some(storage); + } + + if let Some((id, name)) = dns_id_and_name { + let request = [ + "Dns".encode(), + "AddNewProgram".to_string().encode(), + (name, exec::program_id()).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_for_reply(id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + + Self(()) + } + pub fn get_mut(&mut self) -> &'static mut Storage { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn get(&self) -> &'static Storage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl GameService { + pub fn new() -> Self { + Self(()) + } + pub fn register(&mut self, player: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::register(storage, &player)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn reserve_gas(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::reserve_gas(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn start_registration(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::start_registration(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub async fn play(&mut self) { + let storage = self.get_mut(); + let res = funcs::play(storage).await; + let event = match res { + Ok(v) => v, + Err(e) => services::utils::panic(e), + }; + + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn throw_roll(&mut self, pay_fine: bool, properties_for_sale: Option>) -> Event { + let storage = self.get_mut(); + let event = services::utils::panicking(|| { + funcs::throw_roll(storage, pay_fine, properties_for_sale) + }); + self.notify_on(event.clone()).expect("Notification Error"); + event + } + + pub fn add_gear(&mut self, properties_for_sale: Option>) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::add_gear(storage, properties_for_sale)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn upgrade(&mut self, properties_for_sale: Option>) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::upgrade(storage, properties_for_sale)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn buy_cell(&mut self, properties_for_sale: Option>) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::buy_cell(storage, properties_for_sale)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn pay_rent(&mut self, properties_for_sale: Option>) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::pay_rent(storage, properties_for_sale)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub fn change_admin(&mut self, admin: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::change_admin(storage, admin)); + self.notify_on(event.clone()).expect("Notification Error"); + } + + pub async fn kill(&mut self, inheritor: ActorId) { + let storage = self.get(); + if storage.admin != msg::source() { + services::utils::panic(GameError::AccessDenied); + } + if let Some((id, _name)) = &storage.dns_info { + let request = ["Dns".encode(), "DeleteMe".to_string().encode(), ().encode()].concat(); + + msg::send_bytes_with_gas_for_reply(*id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + + self.notify_on(Event::Killed { inheritor }) + .expect("Notification Error"); + exec::exit(inheritor); + } + + pub fn get_storage(&self) -> StorageState { + self.get().clone().into() + } + + pub fn dns_info(&self) -> Option<(ActorId, String)> { + self.get().dns_info.clone() + } +} diff --git a/contracts/syndote/src/utils.rs b/contracts/syndote/app/src/services/game/utils.rs similarity index 67% rename from contracts/syndote/src/utils.rs rename to contracts/syndote/app/src/services/game/utils.rs index d23943372..594a53257 100644 --- a/contracts/syndote/src/utils.rs +++ b/contracts/syndote/app/src/services/game/utils.rs @@ -1,17 +1,90 @@ -use crate::*; -impl Game { - pub fn check_status(&self, game_status: GameStatus) { - assert_eq!(self.game_status, game_status, "Wrong game status"); +use crate::services::game::{Storage, PENALTY}; +use sails_rs::{ + collections::{HashMap, HashSet}, + gstd::{exec, msg}, + prelude::*, + ActorId, +}; + +pub type Price = u32; +pub type Rent = u32; +pub type Gears = Vec; + +#[derive(Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub struct YourTurn { + pub players: Vec<(ActorId, PlayerInfo)>, + pub properties: Vec>, +} + +#[derive(Default, Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub struct PlayerInfo { + pub position: u8, + pub balance: u32, + pub debt: u32, + pub in_jail: bool, + pub round: u128, + pub cells: Vec, + pub penalty: u8, + pub lost: bool, +} + +#[derive(Debug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo, Copy)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Gear { + Bronze, + Silver, + Gold, +} + +impl Gear { + pub fn upgrade(&self) -> Self { + match *self { + Self::Bronze => Self::Silver, + Self::Silver => Self::Gold, + Self::Gold => Self::Gold, + } } +} + +#[derive(Debug, PartialEq, Eq, Clone, TypeInfo, Encode, Decode)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum GameStatus { + Registration, + Play, + Finished, +} - pub fn only_admin(&self) { - assert_eq!(msg::source(), self.admin, "Only admin can start the game"); +impl Default for GameStatus { + fn default() -> Self { + Self::Registration } - pub fn only_player(&self) { - assert!( - self.players.contains_key(&msg::source()), - "You are not in the game" - ); +} + +impl Storage { + pub fn check_status(&self, game_status: GameStatus) -> Result<(), GameError> { + if self.game_status != game_status { + return Err(GameError::WrongStatus); + } + Ok(()) + } + + pub fn only_admin(&self) -> Result<(), GameError> { + if self.admin != msg::source() { + return Err(GameError::AccessDenied); + } + Ok(()) + } + pub fn only_player(&self) -> Result<(), GameError> { + if !self.players.contains_key(&msg::source()) { + return Err(GameError::NotPlayer); + } + Ok(()) } } @@ -54,7 +127,12 @@ pub fn sell_property( for property in properties_for_sale { if let Some((_, _, price, _)) = properties[*property as usize] { - player_info.cells.remove(property); + let index_to_remove = player_info + .cells + .iter() + .position(|cell| cell == property) + .unwrap(); + player_info.cells.remove(index_to_remove); player_info.balance += price / 2; ownership[*property as usize] = *admin; properties_in_bank.insert(*property); @@ -96,7 +174,8 @@ pub fn bankrupt_and_penalty( } if let Some((_, _, price, _)) = &properties[*cell as usize] { player_info.balance += price / 2; - player_info.cells.remove(cell); + let index_to_remove = player_info.cells.iter().position(|c| c == cell).unwrap(); + player_info.cells.remove(index_to_remove); ownership[*cell as usize] = *admin; properties_in_bank.insert(*cell); } @@ -211,13 +290,37 @@ pub fn init_properties( } } +#[derive(Debug)] pub enum GameError { StrategicError, + AlreadyRegistered, + NotPlayer, + AccessDenied, + WrongStatus, +} + +#[derive(Clone, Default, Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub struct StorageState { + pub admin: ActorId, + pub properties_in_bank: Vec, + pub round: u128, + pub players: Vec<(ActorId, PlayerInfo)>, + pub players_queue: Vec, + pub current_player: ActorId, + pub current_step: u64, + // mapping from cells to built properties, + pub properties: Vec>, + // mapping from cells to accounts who have properties on it + pub ownership: Vec, + pub game_status: GameStatus, + pub winner: ActorId, } -impl From for GameState { - fn from(game: Game) -> GameState { - GameState { +impl From for StorageState { + fn from(game: Storage) -> StorageState { + StorageState { admin: game.admin, properties_in_bank: game.properties_in_bank.iter().copied().collect(), round: game.round, diff --git a/contracts/syndote/app/src/services/mod.rs b/contracts/syndote/app/src/services/mod.rs new file mode 100644 index 000000000..c7f00d70e --- /dev/null +++ b/contracts/syndote/app/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod game; +pub mod utils; diff --git a/contracts/syndote/app/src/services/utils.rs b/contracts/syndote/app/src/services/utils.rs new file mode 100644 index 000000000..9dee576e1 --- /dev/null +++ b/contracts/syndote/app/src/services/utils.rs @@ -0,0 +1,13 @@ +use core::fmt::Debug; +use gstd::{ext, format}; + +pub fn panicking Result>(f: F) -> T { + match f() { + Ok(v) => v, + Err(e) => panic(e), + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} diff --git a/contracts/syndote/app/tests/test.rs b/contracts/syndote/app/tests/test.rs new file mode 100644 index 000000000..6877107e4 --- /dev/null +++ b/contracts/syndote/app/tests/test.rs @@ -0,0 +1,114 @@ +use sails_rs::calls::*; +use sails_rs::{ + gtest::{calls::*, Program, System}, + Encode, MessageId, +}; +use syndote::{ + traits::{Syndote, SyndoteFactory}, + GameStatus, Syndote as SyndoteClient, SyndoteFactory as Factory, +}; + +pub const ADMIN_ID: u64 = 10; +pub const USER_ID: u64 = 11; + +#[tokio::test] +async fn test_play_game() { + let system = System::new(); + system.init_logger(); + system.mint_to(ADMIN_ID, 100_000_000_000_000); + system.mint_to(USER_ID, 100_000_000_000_000); + let program_space = GTestRemoting::new(system, ADMIN_ID.into()); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/syndote.opt.wasm"); + + let syndote_factory = Factory::new(program_space.clone()); + + let syndote_id = syndote_factory + .new(None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = SyndoteClient::new(program_space.clone()); + + // upload player program + let player_1 = Program::from_file( + program_space.system(), + "../../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", + ); + let player_2 = Program::from_file( + program_space.system(), + "../../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", + ); + let player_3 = Program::from_file( + program_space.system(), + "../../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", + ); + let player_4 = Program::from_file( + program_space.system(), + "../../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", + ); + let request = ["New".encode(), ().encode()].concat(); + check_send( + program_space.system(), + player_1.send_bytes(USER_ID, request.clone()), + ); + check_send( + program_space.system(), + player_2.send_bytes(USER_ID, request.clone()), + ); + check_send( + program_space.system(), + player_3.send_bytes(USER_ID, request.clone()), + ); + check_send( + program_space.system(), + player_4.send_bytes(USER_ID, request), + ); + + let state = client.get_storage().recv(syndote_id).await.unwrap(); + assert_eq!(state.game_status, GameStatus::Registration); + + // registration + client + .register(player_1.id()) + .send_recv(syndote_id) + .await + .unwrap(); + client + .register(player_2.id()) + .send_recv(syndote_id) + .await + .unwrap(); + client + .register(player_3.id()) + .send_recv(syndote_id) + .await + .unwrap(); + client + .register(player_4.id()) + .send_recv(syndote_id) + .await + .unwrap(); + + // check state + let state = client.get_storage().recv(syndote_id).await.unwrap(); + assert_eq!(state.game_status, GameStatus::Play); + assert_eq!(state.round, 0); + assert_eq!(state.winner, 0.into()); + + // start game + client.play().send_recv(syndote_id).await.unwrap(); + + // check state + let state = client.get_storage().recv(syndote_id).await.unwrap(); + assert_eq!(state.game_status, GameStatus::Finished); + assert_ne!(state.round, 0); + assert_ne!(state.winner, 0.into()); +} + +fn check_send(system: &System, mid: MessageId) { + let res = system.run_next_block(); + assert!(res.succeed.contains(&mid)); +} diff --git a/contracts/syndote/build.rs b/contracts/syndote/build.rs deleted file mode 100644 index 165280026..000000000 --- a/contracts/syndote/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -use syndote_io::SynMetadata; - -fn main() { - gear_wasm_builder::build_with_metadata::(); -} diff --git a/contracts/syndote/io/Cargo.toml b/contracts/syndote/io/Cargo.toml deleted file mode 100644 index 10580f724..000000000 --- a/contracts/syndote/io/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "syndote-io" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -gstd.workspace = true -gmeta.workspace = true diff --git a/contracts/syndote/io/src/lib.rs b/contracts/syndote/io/src/lib.rs deleted file mode 100644 index d1390b1ee..000000000 --- a/contracts/syndote/io/src/lib.rs +++ /dev/null @@ -1,150 +0,0 @@ -#![no_std] - -use gmeta::{InOut, Metadata, Out}; -use gstd::{collections::BTreeSet, prelude::*, ActorId}; - -pub type Price = u32; -pub type Rent = u32; -pub type Gears = Vec; - -#[derive(Encode, Decode, TypeInfo)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub struct YourTurn { - pub players: Vec<(ActorId, PlayerInfo)>, - pub properties: Vec>, -} - -pub struct SynMetadata; - -impl Metadata for SynMetadata { - type Init = (); - type Handle = InOut; - type Reply = (); - type Others = (); - type Signal = (); - type State = Out; -} - -#[derive(Clone, Default, Encode, Decode, TypeInfo)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub struct GameState { - pub admin: ActorId, - pub properties_in_bank: Vec, - pub round: u128, - pub players: Vec<(ActorId, PlayerInfo)>, - pub players_queue: Vec, - pub current_player: ActorId, - pub current_step: u64, - // mapping from cells to built properties, - pub properties: Vec>, - // mapping from cells to accounts who have properties on it - pub ownership: Vec, - pub game_status: GameStatus, - pub winner: ActorId, -} - -#[derive(Encode, Decode, TypeInfo)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub enum GameAction { - StartRegistration, - Register { - player: ActorId, - }, - ReserveGas, - Play, - ThrowRoll { - pay_fine: bool, - properties_for_sale: Option>, - }, - AddGear { - properties_for_sale: Option>, - }, - Upgrade { - properties_for_sale: Option>, - }, - BuyCell { - properties_for_sale: Option>, - }, - PayRent { - properties_for_sale: Option>, - }, - ChangeAdmin(ActorId), -} - -#[derive(Encode, Decode, TypeInfo)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub enum GameEvent { - Registered, - StartRegistration, - GameFinished { - winner: ActorId, - }, - StrategicError, - StrategicSuccess, - Step { - players: Vec<(ActorId, PlayerInfo)>, - properties: Vec>, - current_player: ActorId, - ownership: Vec, - current_step: u64, - }, - Jail { - in_jail: bool, - position: u8, - }, - GasReserved, - NextRoundFromReservation, - AdminChanged, -} - -#[derive(Default, Debug, Clone, Encode, Decode, TypeInfo)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub struct PlayerInfo { - pub position: u8, - pub balance: u32, - pub debt: u32, - pub in_jail: bool, - pub round: u128, - pub cells: BTreeSet, - pub penalty: u8, - pub lost: bool, -} - -#[derive(PartialEq, Eq, Encode, Decode, Clone, TypeInfo, Copy)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub enum Gear { - Bronze, - Silver, - Gold, -} - -impl Gear { - pub fn upgrade(&self) -> Self { - match *self { - Self::Bronze => Self::Silver, - Self::Silver => Self::Gold, - Self::Gold => Self::Gold, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, TypeInfo, Encode, Decode)] -#[codec(crate = gstd::codec)] -#[scale_info(crate = gstd::scale_info)] -pub enum GameStatus { - Registration, - Play, - Finished, -} - -impl Default for GameStatus { - fn default() -> Self { - Self::Registration - } -} diff --git a/contracts/syndote/player/app/Cargo.toml b/contracts/syndote/player/app/Cargo.toml new file mode 100644 index 000000000..3b556c1f0 --- /dev/null +++ b/contracts/syndote/player/app/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "player-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs.workspace = true diff --git a/contracts/syndote/player/app/src/lib.rs b/contracts/syndote/player/app/src/lib.rs new file mode 100644 index 000000000..adf18d9c4 --- /dev/null +++ b/contracts/syndote/player/app/src/lib.rs @@ -0,0 +1,216 @@ +#![no_std] + +use sails_rs::gstd::{exec, msg}; +use sails_rs::prelude::*; + +pub const COST_FOR_UPGRADE: u32 = 500; +pub const FINE: u32 = 1_000; +pub type Price = u32; +pub type Rent = u32; +pub type Gears = Vec; + +#[derive(Default, Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub struct PlayerInfo { + pub position: u8, + pub balance: u32, + pub debt: u32, + pub in_jail: bool, + pub round: u128, + pub cells: Vec, + pub penalty: u8, + pub lost: bool, +} + +#[derive(Debug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo, Copy)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Gear { + Bronze, + Silver, + Gold, +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + Registered, + StartRegistration, + Played, + GameFinished { + winner: ActorId, + }, + StrategicError, + StrategicSuccess, + Step { + players: Vec<(ActorId, PlayerInfo)>, + properties: Vec>, + current_player: ActorId, + ownership: Vec, + current_step: u64, + }, + Jail { + in_jail: bool, + position: u8, + }, + GasReserved, + NextRoundFromReservation, + AdminChanged, + Killed { + inheritor: ActorId, + }, +} + +struct PlayerService(()); + +impl PlayerService { + pub fn init() -> Self { + Self(()) + } +} + +#[sails_rs::service] +impl PlayerService { + pub fn new() -> Self { + Self(()) + } + pub async fn your_turn( + &self, + players: Vec<(ActorId, PlayerInfo)>, + properties: Vec>, + ) -> bool { + let monopoly_id = msg::source(); + let (_, mut player_info) = players + .iter() + .find(|(player, _player_info)| player == &exec::program_id()) + .expect("Can't find my address") + .clone(); + + if player_info.in_jail { + if player_info.balance <= FINE { + let request = [ + "Syndote".encode(), + "ThrowRoll".to_string().encode(), + (false, None::>).encode(), + ] + .concat(); + + let reply: Event = msg::send_bytes_for_reply_as(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + + if let Event::Jail { in_jail, position } = reply { + if !in_jail { + player_info.position = position; + } else { + return true; + } + } + } else { + let request = [ + "Syndote".encode(), + "ThrowRoll".to_string().encode(), + (true, None::>).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + + return true; + } + } + + let position = player_info.position; + + let (my_cell, free_cell, gears) = + if let Some((account, gears, _, _)) = &properties[position as usize] { + let my_cell = account == &exec::program_id(); + let free_cell = account == &ActorId::zero(); + (my_cell, free_cell, gears) + } else { + return true; + }; + + if my_cell { + if gears.len() < 3 { + let request = [ + "Syndote".encode(), + "AddGear".to_string().encode(), + (None::>).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + + return true; + } else { + let request = [ + "Syndote".encode(), + "Upgrade".to_string().encode(), + (None::>).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + + return true; + } + } + if free_cell { + //debug!("BUY CELL"); + + let request = [ + "Syndote".encode(), + "BuyCell".to_string().encode(), + (None::>).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + } else if !my_cell { + //debug!("PAY RENT"); + let request = [ + "Syndote".encode(), + "PayRent".to_string().encode(), + (None::>).encode(), + ] + .concat(); + + msg::send_bytes_for_reply(monopoly_id, request, 0, 0) + .expect("Error in sending a message `ThrowRoll`") + .await + .expect("Unable to decode `Event`"); + } + true + } +} + +pub struct PlayerProgram(()); + +#[sails_rs::program] +impl PlayerProgram { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + PlayerService::init(); + Self(()) + } + + pub fn player(&self) -> PlayerService { + PlayerService::new() + } +} diff --git a/contracts/syndote/player/build.rs b/contracts/syndote/player/build.rs deleted file mode 100644 index b8ba24477..000000000 --- a/contracts/syndote/player/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -use syndote_player_io::PlayerMetadata; - -fn main() { - gear_wasm_builder::build_with_metadata::(); -} diff --git a/contracts/syndote/player/io/Cargo.toml b/contracts/syndote/player/io/Cargo.toml deleted file mode 100644 index 527d39a2a..000000000 --- a/contracts/syndote/player/io/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "syndote-player-io" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -gstd.workspace = true -gmeta.workspace = true -syndote-io.workspace = true diff --git a/contracts/syndote/player/io/src/lib.rs b/contracts/syndote/player/io/src/lib.rs deleted file mode 100644 index 7deed3dcc..000000000 --- a/contracts/syndote/player/io/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -#![no_std] -use gmeta::{InOut, Metadata}; -use gstd::prelude::*; -use syndote_io::*; -pub struct PlayerMetadata; - -impl Metadata for PlayerMetadata { - type Init = (); - type Handle = InOut; - type Reply = (); - type Others = (); - type Signal = (); - type State = (); -} diff --git a/contracts/syndote/player/src/lib.rs b/contracts/syndote/player/src/lib.rs deleted file mode 100644 index 723eb7ae8..000000000 --- a/contracts/syndote/player/src/lib.rs +++ /dev/null @@ -1,145 +0,0 @@ -#![no_std] -use gstd::{exec, msg, prelude::*, ActorId}; -use syndote_io::*; -//static mut MONOPOLY: ActorId = ActorId::zero(); -pub const COST_FOR_UPGRADE: u32 = 500; -pub const FINE: u32 = 1_000; - -#[gstd::async_main] -async fn main() { - //let monopoly_id = unsafe { MONOPOLY }; - let monopoly_id = msg::source(); - // assert_eq!( - // msg::source(), - // monopoly_id, - // "Only monopoly contract can call strategic contract" - // ); - let message: YourTurn = msg::load().expect("Unable to decode struct`YourTurn`"); - let (_, mut player_info) = message - .players - .iter() - .find(|(player, _player_info)| player == &exec::program_id()) - .expect("Can't find my address") - .clone(); - if player_info.in_jail { - if player_info.balance <= FINE { - let reply: GameEvent = msg::send_for_reply_as( - monopoly_id, - GameAction::ThrowRoll { - pay_fine: false, - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::ThrowRoll`") - .await - .expect("Unable to decode `GameEvent"); - - if let GameEvent::Jail { in_jail, position } = reply { - if !in_jail { - player_info.position = position; - } else { - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); - return; - } - } - } else { - msg::send_for_reply_as::<_, GameEvent>( - monopoly_id, - GameAction::ThrowRoll { - pay_fine: true, - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::ThrowRoll`") - .await - .expect("Unable to decode `GameEvent"); - - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); - return; - } - } - - let position = player_info.position; - - // debug!("BALANCE {:?}", my_player.balance); - let (my_cell, free_cell, gears) = - if let Some((account, gears, _, _)) = &message.properties[position as usize] { - let my_cell = account == &exec::program_id(); - let free_cell = account == &ActorId::zero(); - (my_cell, free_cell, gears) - } else { - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); - return; - }; - - if my_cell { - //debug!("ADD GEAR"); - if gears.len() < 3 { - msg::send_for_reply_as::<_, GameEvent>( - monopoly_id, - GameAction::AddGear { - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::AddGear`") - .await - .expect("Unable to decode `GameEvent"); - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); - return; - } else { - //debug!("UPGRADE"); - msg::send_for_reply_as::<_, GameEvent>( - monopoly_id, - GameAction::Upgrade { - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::Upgrade`") - .await - .expect("Unable to decode `GameEvent"); - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); - return; - } - } - if free_cell { - //debug!("BUY CELL"); - msg::send_for_reply_as::<_, GameEvent>( - monopoly_id, - GameAction::BuyCell { - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::BuyCell`") - .await - .expect("Unable to decode `GameEvent"); - } else if !my_cell { - //debug!("PAY RENT"); - msg::send_for_reply_as::<_, GameEvent>( - monopoly_id, - GameAction::PayRent { - properties_for_sale: None, - }, - 0, - 0, - ) - .expect("Error in sending a message `GameAction::PayRent`") - .await - .expect("Unable to decode `GameEvent"); - } - msg::reply("", 0).expect("Error in sending a reply to monopoly contract"); -} - -#[no_mangle] -unsafe extern fn init() { - // MONOPOLY = msg::load::().expect("Unable to decode ActorId"); -} diff --git a/contracts/syndote/player/Cargo.toml b/contracts/syndote/player/wasm/Cargo.toml similarity index 56% rename from contracts/syndote/player/Cargo.toml rename to contracts/syndote/player/wasm/Cargo.toml index cbc9e4db9..60d8ccb9d 100644 --- a/contracts/syndote/player/Cargo.toml +++ b/contracts/syndote/player/wasm/Cargo.toml @@ -2,12 +2,12 @@ name = "syndote-player" version.workspace = true edition.workspace = true -publish.workspace = true +license.workspace = true [dependencies] -gstd.workspace = true -syndote-io.workspace = true +player-app = { path = "../app" } [build-dependencies] gear-wasm-builder.workspace = true -syndote-player-io.workspace = true +sails-idl-gen.workspace = true +player-app = { path = "../app" } diff --git a/contracts/syndote/player/wasm/build.rs b/contracts/syndote/player/wasm/build.rs new file mode 100644 index 000000000..3d144e1dd --- /dev/null +++ b/contracts/syndote/player/wasm/build.rs @@ -0,0 +1,15 @@ +use player_app::PlayerProgram; +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("syndote_player.idl"); + + let idl_file = File::create(idl_file_path).unwrap(); + + program::generate_idl::(idl_file).unwrap(); +} diff --git a/contracts/syndote/player/wasm/src/lib.rs b/contracts/syndote/player/wasm/src/lib.rs new file mode 100644 index 000000000..f60bb299d --- /dev/null +++ b/contracts/syndote/player/wasm/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] + +#[cfg(target_arch = "wasm32")] +pub use player_app::wasm::*; diff --git a/contracts/syndote/player/wasm/syndote_player.idl b/contracts/syndote/player/wasm/syndote_player.idl new file mode 100644 index 000000000..35df17eee --- /dev/null +++ b/contracts/syndote/player/wasm/syndote_player.idl @@ -0,0 +1,25 @@ +type PlayerInfo = struct { + position: u8, + balance: u32, + debt: u32, + in_jail: bool, + round: u128, + cells: vec u8, + penalty: u8, + lost: bool, +}; + +type Gear = enum { + Bronze, + Silver, + Gold, +}; + +constructor { + New : (); +}; + +service Player { + query YourTurn : (players: vec struct { actor_id, PlayerInfo }, properties: vec opt struct { actor_id, vec Gear, u32, u32 }) -> bool; +}; + diff --git a/contracts/syndote/src/lib.rs b/contracts/syndote/src/lib.rs deleted file mode 100644 index e7315dbeb..000000000 --- a/contracts/syndote/src/lib.rs +++ /dev/null @@ -1,292 +0,0 @@ -#![no_std] - -use gstd::{ - collections::{HashMap, HashSet}, - exec, msg, - prelude::*, - ActorId, ReservationId, -}; -use messages::*; -use syndote_io::*; -use utils::*; - -pub mod messages; -pub mod strategic_actions; -pub mod utils; - -const RESERVATION_AMOUNT: u64 = 245_000_000_000; -const GAS_FOR_ROUND: u64 = 60_000_000_000; - -pub const NUMBER_OF_CELLS: u8 = 40; -pub const NUMBER_OF_PLAYERS: u8 = 4; -pub const JAIL_POSITION: u8 = 10; -pub const LOTTERY_POSITION: u8 = 20; -pub const COST_FOR_UPGRADE: u32 = 500; -pub const FINE: u32 = 1_000; -pub const PENALTY: u8 = 5; -pub const INITIAL_BALANCE: u32 = 15_000; -pub const NEW_CIRCLE: u32 = 2_000; -pub const WAIT_DURATION: u32 = 5; - -#[derive(Clone, Default)] -pub struct Game { - admin: ActorId, - properties_in_bank: HashSet, - round: u128, - players: HashMap, - players_queue: Vec, - current_player: ActorId, - current_step: u64, - // mapping from cells to built properties, - properties: Vec>, - // mapping from cells to accounts who have properties on it - ownership: Vec, - game_status: GameStatus, - winner: ActorId, -} - -static mut GAME: Option = None; -static mut RESERVATION: Option> = None; - -impl Game { - fn change_admin(&mut self, admin: &ActorId) { - assert_eq!(msg::source(), self.admin); - self.admin = *admin; - msg::reply(GameEvent::AdminChanged, 0).expect("Error in a reply `GameEvent::AdminChanged`"); - } - fn reserve_gas(&self) { - unsafe { - let reservation_id = ReservationId::reserve(RESERVATION_AMOUNT, 600) - .expect("reservation across executions"); - let reservations = RESERVATION.get_or_insert(Default::default()); - reservations.push(reservation_id); - } - msg::reply(GameEvent::GasReserved, 0).expect(""); - } - fn start_registration(&mut self) { - self.check_status(GameStatus::Finished); - self.only_admin(); - let mut game: Game = Game { - admin: self.admin, - ..Default::default() - }; - init_properties(&mut game.properties, &mut game.ownership); - *self = game; - msg::reply(GameEvent::StartRegistration, 0) - .expect("Error in sending a reply `GameEvent::StartRegistration"); - } - - fn register(&mut self, player: &ActorId) { - self.check_status(GameStatus::Registration); - assert!( - !self.players.contains_key(player), - "You have already registered" - ); - self.players.insert( - *player, - PlayerInfo { - balance: INITIAL_BALANCE, - ..Default::default() - }, - ); - self.players_queue.push(*player); - if self.players_queue.len() == NUMBER_OF_PLAYERS as usize { - self.game_status = GameStatus::Play; - } - msg::reply(GameEvent::Registered, 0) - .expect("Error in sending a reply `GameEvent::Registered`"); - } - - async fn play(&mut self) { - //self.check_status(GameStatus::Play); - assert!( - msg::source() == self.admin || msg::source() == exec::program_id(), - "Only admin or the program can send that message" - ); - - while self.game_status == GameStatus::Play { - if self.players_queue.len() <= 1 { - self.winner = self.players_queue[0]; - self.game_status = GameStatus::Finished; - msg::reply( - GameEvent::GameFinished { - winner: self.winner, - }, - 0, - ) - .expect("Error in sending a reply `GameEvent::GameFinished`"); - break; - } - if exec::gas_available() <= GAS_FOR_ROUND { - unsafe { - let reservations = RESERVATION.get_or_insert(Default::default()); - if let Some(id) = reservations.pop() { - msg::send_from_reservation(id, exec::program_id(), GameAction::Play, 0) - .expect("Failed to send message"); - msg::reply(GameEvent::NextRoundFromReservation, 0).expect(""); - - break; - } else { - panic!("GIVE ME MORE GAS"); - }; - } - } - - // // check penalty and debt of the players for the previous round - // // if penalty is equal to 5 points we remove the player from the game - // // if a player has a debt and he has not enough balance to pay it - // // he is also removed from the game - // bankrupt_and_penalty( - // &self.admin, - // &mut self.players, - // &mut self.players_queue, - // &mut self.properties, - // &mut self.properties_in_bank, - // &mut self.ownership, - // ); - - // if self.players_queue.len() <= 1 { - // self.winner = self.players_queue[0]; - // self.game_status = GameStatus::Finished; - // msg::reply( - // GameEvent::GameFinished { - // winner: self.winner, - // }, - // 0, - // ) - // .expect("Error in sending a reply `GameEvent::GameFinished`"); - // break; - // } - self.round = self.round.wrapping_add(1); - for player in self.players_queue.clone() { - self.current_player = player; - self.current_step += 1; - // we save the state before the player's step in case - // the player's contract does not reply or is executed with a panic. - // Then we roll back all the changes that the player could have made. - let mut state = self.clone(); - let player_info = self - .players - .get_mut(&player) - .expect("Cant be None: Get Player"); - - // if a player is in jail we don't throw rolls for him - let position = if player_info.in_jail { - player_info.position - } else { - let (r1, r2) = get_rolls(); - // debug!("ROOLS {:?} {:?}", r1, r2); - let roll_sum = r1 + r2; - (player_info.position + roll_sum) % NUMBER_OF_CELLS - }; - - // If a player is on a cell that belongs to another player - // we write down a debt on him in the amount of the rent. - // This is done in order to penalize the participant's contract - // if he misses the rent - let account = self.ownership[position as usize]; - if account != player && account != ActorId::zero() { - if let Some((_, _, _, rent)) = self.properties[position as usize] { - player_info.debt = rent; - } - } - player_info.position = position; - player_info.in_jail = position == JAIL_POSITION; - state.players.insert(player, player_info.clone()); - match position { - 0 => { - player_info.balance += NEW_CIRCLE; - player_info.round = self.round; - } - // free cells (it can be lottery or penalty): TODO as a task on hackathon - 2 | 4 | 7 | 16 | 20 | 30 | 33 | 36 | 38 => { - player_info.round = self.round; - } - _ => { - let reply = take_your_turn(&player, &state).await; - - if reply.is_err() { - player_info.penalty = PENALTY; - } - } - } - - // check penalty and debt of the players for the previous round - // if penalty is equal to 5 points we remove the player from the game - // if a player has a debt and he has not enough balance to pay it - // he is also removed from the game - bankrupt_and_penalty( - &self.admin, - &mut self.players, - &mut self.players_queue, - &self.properties, - &mut self.properties_in_bank, - &mut self.ownership, - ); - - msg::send( - self.admin, - GameEvent::Step { - players: self - .players - .iter() - .map(|(key, value)| (*key, value.clone())) - .collect(), - properties: self.properties.clone(), - current_player: self.current_player, - current_step: self.current_step, - ownership: self.ownership.clone(), - }, - 0, - ) - .expect("Error in sending a message `GameEvent::Step`"); - } - } - } -} - -#[gstd::async_main] -async fn main() { - let action: GameAction = msg::load().expect("Could not load `GameAction`"); - let game: &mut Game = unsafe { GAME.get_or_insert(Default::default()) }; - match action { - GameAction::Register { player } => game.register(&player), - GameAction::ReserveGas => game.reserve_gas(), - GameAction::StartRegistration => game.start_registration(), - GameAction::Play => game.play().await, - GameAction::ThrowRoll { - pay_fine, - properties_for_sale, - } => game.throw_roll(pay_fine, properties_for_sale), - GameAction::AddGear { - properties_for_sale, - } => game.add_gear(properties_for_sale), - GameAction::Upgrade { - properties_for_sale, - } => game.upgrade(properties_for_sale), - GameAction::BuyCell { - properties_for_sale, - } => game.buy_cell(properties_for_sale), - GameAction::PayRent { - properties_for_sale, - } => game.pay_rent(properties_for_sale), - GameAction::ChangeAdmin(admin) => game.change_admin(&admin), - } -} - -#[no_mangle] -unsafe extern fn init() { - let mut game = Game { - admin: msg::source(), - ..Default::default() - }; - init_properties(&mut game.properties, &mut game.ownership); - GAME = Some(game); -} - -#[no_mangle] -extern fn state() { - let game = unsafe { GAME.take().expect("Game is not initialized") }; - let game_state: GameState = game.into(); - msg::reply(game_state, 0).expect("Failed to share state"); -} diff --git a/contracts/syndote/src/messages.rs b/contracts/syndote/src/messages.rs deleted file mode 100644 index e85324a8c..000000000 --- a/contracts/syndote/src/messages.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::*; -use gstd::errors::Error; - -pub async fn take_your_turn(player: &ActorId, game: &Game) -> Result, Error> { - let players = game - .players - .iter() - .map(|(key, value)| (*key, value.clone())) - .collect(); - msg::send_for_reply( - *player, - YourTurn { - players, - properties: game.properties.clone(), - }, - 0, - 0, - ) - .expect("Error on sending `YourTurn` message") - .up_to(Some(WAIT_DURATION)) - .expect("Invalid wait duration.") - .await -} diff --git a/contracts/syndote/src/strategic_actions.rs b/contracts/syndote/src/strategic_actions.rs deleted file mode 100644 index 92c2b324a..000000000 --- a/contracts/syndote/src/strategic_actions.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::*; - -impl Game { - // to throw rolls to go out from the prison - // `pay_fine`: to pay fine or not if there is not double - pub fn throw_roll(&mut self, pay_fine: bool, properties_for_sale: Option>) { - self.only_player(); - let player_info = match get_player_info(&self.current_player, &mut self.players, self.round) - { - Ok(player_info) => player_info, - Err(_) => { - reply_strategic_error(); - return; - } - }; - - // If a player is not in the jail - if !player_info.in_jail { - // debug!("PENALTY: PLAYER IS NOT IN JAIL"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - - if let Some(properties) = properties_for_sale { - if sell_property( - &self.admin, - &mut self.ownership, - &properties, - &mut self.properties_in_bank, - &self.properties, - player_info, - ) - .is_err() - { - reply_strategic_error(); - return; - }; - } - - let (r1, r2) = get_rolls(); - if r1 == r2 { - player_info.in_jail = false; - player_info.position = r1 + r2; - } else if pay_fine { - if player_info.balance < FINE { - player_info.penalty += 1; - reply_strategic_error(); - return; - } - player_info.balance -= FINE; - player_info.in_jail = false; - } - player_info.round = self.round; - msg::reply( - GameEvent::Jail { - in_jail: player_info.in_jail, - position: player_info.position, - }, - 0, - ) - .expect("Error in sending a reply `GameEvent::Jail`"); - } - - pub fn add_gear(&mut self, properties_for_sale: Option>) { - self.only_player(); - let player_info = match get_player_info(&self.current_player, &mut self.players, self.round) - { - Ok(player_info) => player_info, - Err(_) => { - reply_strategic_error(); - return; - } - }; - - if let Some(properties) = properties_for_sale { - if sell_property( - &self.admin, - &mut self.ownership, - &properties, - &mut self.properties_in_bank, - &self.properties, - player_info, - ) - .is_err() - { - reply_strategic_error(); - return; - }; - } - - // if player did not check his balance itself - if player_info.balance < COST_FOR_UPGRADE { - // debug!("PENALTY: NOT ENOUGH BALANCE FOR UPGRADE"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - - let position = player_info.position; - - let gears = if let Some((account, gears, _, _)) = &mut self.properties[position as usize] { - if account != &msg::source() { - // debug!("PENALTY: TRY TO UPGRADE NOT OWN CELL"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - gears - } else { - player_info.penalty += 1; - reply_strategic_error(); - return; - }; - - // maximum amount of gear is reached - if gears.len() == 3 { - // debug!("PENALTY: MAXIMUM AMOUNT OF GEARS ON CELL"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - - gears.push(Gear::Bronze); - player_info.balance -= COST_FOR_UPGRADE; - player_info.round = self.round; - reply_strategic_success(); - } - - pub fn upgrade(&mut self, properties_for_sale: Option>) { - self.only_player(); - let player_info = match get_player_info(&self.current_player, &mut self.players, self.round) - { - Ok(player_info) => player_info, - Err(_) => { - reply_strategic_error(); - return; - } - }; - - if let Some(properties) = properties_for_sale { - if sell_property( - &self.admin, - &mut self.ownership, - &properties, - &mut self.properties_in_bank, - &self.properties, - player_info, - ) - .is_err() - { - reply_strategic_error(); - return; - }; - } - - // if player did not check his balance itself - if player_info.balance < COST_FOR_UPGRADE { - // debug!("PENALTY: NOT ENOUGH BALANCE FOR UPGRADE"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - - let position = player_info.position; - - if let Some((account, gears, price, rent)) = &mut self.properties[position as usize] { - if account != &msg::source() { - player_info.penalty += 1; - reply_strategic_error(); - return; - } - // if nothing to upgrade - if gears.is_empty() { - // debug!("PENALTY: NOTHING TO UPGRADE"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - for gear in gears { - if *gear != Gear::Gold { - *gear = gear.upgrade(); - // add 10 percent to the price of cell - *price += *price / 10; - // add 10 percent to the price of rent - *rent += *rent / 10; - break; - } - } - } else { - player_info.penalty += 1; - reply_strategic_error(); - return; - }; - - player_info.balance -= COST_FOR_UPGRADE; - player_info.round = self.round; - reply_strategic_success(); - } - - // if a cell is free, a player can buy it - pub fn buy_cell(&mut self, properties_for_sale: Option>) { - self.only_player(); - let player_info = match get_player_info(&self.current_player, &mut self.players, self.round) - { - Ok(player_info) => player_info, - Err(_) => { - reply_strategic_error(); - return; - } - }; - let position = player_info.position; - - if let Some(properties) = properties_for_sale { - if sell_property( - &self.admin, - &mut self.ownership, - &properties, - &mut self.properties_in_bank, - &self.properties, - player_info, - ) - .is_err() - { - reply_strategic_error(); - return; - }; - } - - // if a player on the field that can't be sold (for example, jail) - if let Some((account, _, price, _)) = self.properties[position as usize].as_mut() { - if account != &mut ActorId::zero() { - // debug!("PENALTY: THAT CELL IS ALREDY BOUGHT"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - // if a player has not enough balance - if player_info.balance < *price { - player_info.penalty += 1; - // debug!("PENALTY: NOT ENOUGH BALANCE FOR BUYING"); - reply_strategic_error(); - return; - } - player_info.balance -= *price; - *account = msg::source(); - } else { - player_info.penalty += 1; - // debug!("PENALTY: THAT FIELD CAN'T BE SOLD"); - reply_strategic_error(); - return; - }; - player_info.cells.insert(position); - self.ownership[position as usize] = msg::source(); - player_info.round = self.round; - reply_strategic_success(); - } - - pub fn pay_rent(&mut self, properties_for_sale: Option>) { - self.only_player(); - let player_info = match get_player_info(&self.current_player, &mut self.players, self.round) - { - Ok(player_info) => player_info, - Err(_) => { - reply_strategic_error(); - return; - } - }; - if let Some(properties) = properties_for_sale { - if sell_property( - &self.admin, - &mut self.ownership, - &properties, - &mut self.properties_in_bank, - &self.properties, - player_info, - ) - .is_err() - { - reply_strategic_error(); - return; - }; - } - - let position = player_info.position; - let account = self.ownership[position as usize]; - - if account == msg::source() { - player_info.penalty += 1; - // debug!("PENALTY: PAY RENT TO HIMSELF"); - reply_strategic_error(); - return; - } - - let (_, _, _, rent) = self.properties[position as usize] - .clone() - .unwrap_or_default(); - if rent == 0 { - // debug!("PENALTY: CELL WITH NO PROPERTIES"); - player_info.penalty += 1; - reply_strategic_error(); - return; - }; - - if player_info.balance < rent { - // debug!("PENALTY: NOT ENOUGH BALANCE TO PAY RENT"); - player_info.penalty += 1; - reply_strategic_error(); - return; - } - player_info.balance -= rent; - player_info.debt = 0; - player_info.round = self.round; - self.players.entry(account).and_modify(|player_info| { - player_info.balance = player_info.balance.saturating_add(rent) - }); - reply_strategic_success(); - } -} - -fn reply_strategic_error() { - msg::reply(GameEvent::StrategicError, 0).expect("Error in a reply `GameEvent::StrategicError`"); -} - -fn reply_strategic_success() { - msg::reply(GameEvent::StrategicSuccess, 0) - .expect("Error in a reply `GameEvent::StrategicSuccess`"); -} diff --git a/contracts/syndote/tests/game_test.rs b/contracts/syndote/tests/game_test.rs deleted file mode 100644 index 1b7c1c59d..000000000 --- a/contracts/syndote/tests/game_test.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gstd::{prelude::*, ActorId, MessageId}; -use gtest::{Program, System}; -use syndote_io::*; - -#[test] -fn game() { - let system = System::new(); - system.init_logger(); - system.mint_to(10, 100_000_000_000_000); - let player_1 = Program::from_file( - &system, - "../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", - ); - let player_2 = Program::from_file( - &system, - "../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", - ); - let player_3 = Program::from_file( - &system, - "../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", - ); - let player_4 = Program::from_file( - &system, - "../target/wasm32-unknown-unknown/release/syndote_player.opt.wasm", - ); - let game = Program::current_opt(&system); - check_send(&system, player_1.send::<_, ActorId>(10, 5.into())); - check_send(&system, player_2.send::<_, ActorId>(10, 5.into())); - check_send(&system, player_3.send::<_, ActorId>(10, 5.into())); - check_send(&system, player_4.send::<_, ActorId>(10, 5.into())); - check_send(&system, game.send(10, 0x00)); - - check_send( - &system, - game.send(10, GameAction::Register { player: 1.into() }), - ); - check_send( - &system, - game.send(10, GameAction::Register { player: 2.into() }), - ); - check_send( - &system, - game.send(10, GameAction::Register { player: 3.into() }), - ); - check_send( - &system, - game.send(10, GameAction::Register { player: 4.into() }), - ); - - game.send(10, GameAction::Play); -} - -fn check_send(system: &System, mid: MessageId) { - let res = system.run_next_block(); - assert!(res.succeed.contains(&mid)); -} diff --git a/contracts/syndote/wasm/Cargo.toml b/contracts/syndote/wasm/Cargo.toml new file mode 100644 index 000000000..50df309d3 --- /dev/null +++ b/contracts/syndote/wasm/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "syndote" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +syndote-app = { path = "../app" } +sails-rs.workspace = true + +[build-dependencies] +gear-wasm-builder.workspace = true +sails-idl-gen.workspace = true +sails-client-gen.workspace = true +syndote-app = { path = "../app" } diff --git a/contracts/syndote/wasm/build.rs b/contracts/syndote/wasm/build.rs new file mode 100644 index 000000000..426740e8a --- /dev/null +++ b/contracts/syndote/wasm/build.rs @@ -0,0 +1,20 @@ +use sails_client_gen::ClientGenerator; +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; +use syndote_app::Program; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("syndote.idl"); + + let idl_file = File::create(idl_file_path.clone()).unwrap(); + + program::generate_idl::(idl_file).unwrap(); + + ClientGenerator::from_idl_path(&idl_file_path) + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("syndote_client.rs")) + .unwrap(); +} diff --git a/contracts/syndote/wasm/src/lib.rs b/contracts/syndote/wasm/src/lib.rs new file mode 100644 index 000000000..e5d3dd77b --- /dev/null +++ b/contracts/syndote/wasm/src/lib.rs @@ -0,0 +1,9 @@ +#![no_std] +#![allow(clippy::type_complexity)] + +include!(concat!(env!("OUT_DIR"), "/syndote_client.rs")); +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +use syndote_app::services::game::Event; +#[cfg(target_arch = "wasm32")] +pub use syndote_app::wasm::*; diff --git a/contracts/syndote/wasm/syndote.idl b/contracts/syndote/wasm/syndote.idl new file mode 100644 index 000000000..6ffc0f50d --- /dev/null +++ b/contracts/syndote/wasm/syndote.idl @@ -0,0 +1,72 @@ +type PlayerInfo = struct { + position: u8, + balance: u32, + debt: u32, + in_jail: bool, + round: u128, + cells: vec u8, + penalty: u8, + lost: bool, +}; + +type Gear = enum { + Bronze, + Silver, + Gold, +}; + +type StorageState = struct { + admin: actor_id, + properties_in_bank: vec u8, + round: u128, + players: vec struct { actor_id, PlayerInfo }, + players_queue: vec actor_id, + current_player: actor_id, + current_step: u64, + properties: vec opt struct { actor_id, vec Gear, u32, u32 }, + ownership: vec actor_id, + game_status: GameStatus, + winner: actor_id, +}; + +type GameStatus = enum { + Registration, + Play, + Finished, +}; + +constructor { + New : (dns_id_and_name: opt struct { actor_id, str }); +}; + +service Syndote { + AddGear : (properties_for_sale: opt vec u8) -> null; + BuyCell : (properties_for_sale: opt vec u8) -> null; + ChangeAdmin : (admin: actor_id) -> null; + Kill : (inheritor: actor_id) -> null; + PayRent : (properties_for_sale: opt vec u8) -> null; + Play : () -> null; + Register : (player: actor_id) -> null; + ReserveGas : () -> null; + StartRegistration : () -> null; + ThrowRoll : (pay_fine: bool, properties_for_sale: opt vec u8) -> Event; + Upgrade : (properties_for_sale: opt vec u8) -> null; + query DnsInfo : () -> opt struct { actor_id, str }; + query GetStorage : () -> StorageState; + + events { + Registered; + StartRegistration; + Played; + GameFinished: struct { winner: actor_id }; + StrategicError; + StrategicSuccess; + Step: struct { players: vec struct { actor_id, PlayerInfo }, properties: vec opt struct { actor_id, vec Gear, u32, u32 }, current_player: actor_id, ownership: vec actor_id, current_step: u64 }; + Jail: struct { in_jail: bool, position: u8 }; + GasReserved; + NextRoundFromReservation; + AdminChanged; + Killed: struct { inheritor: actor_id }; + } +}; + diff --git a/contracts/varatube/README.md b/contracts/varatube/README.md index a57c86c0b..21bc697dd 100644 --- a/contracts/varatube/README.md +++ b/contracts/varatube/README.md @@ -1,12 +1,12 @@ -[![Open in Gitpod](https://img.shields.io/badge/Open_in-Gitpod-white?logo=gitpod)](https://gitpod.io/#FOLDER=varatube/https://github.com/gear-foundation/dapps) -[![Docs](https://img.shields.io/github/actions/workflow/status/gear-foundation/dapps/contracts.yml?logo=rust&label=docs)](https://dapps.gear.rs/varatube_io) +# [VaraTube](https://wiki.vara.network/docs/examples/Infra/varatube) + +⚙️ **Note**: The project code is developed using the [Sails](https://github.com/gear-tech/sails) framework. -# [VaraTube](https://wiki.gear-tech.io/docs/examples/Infra/varatube) ### 🏗️ Building ```sh -cargo b -r -p "varatube-wasm" +cargo b -r -p "varatube" ``` ### ✅ Testing diff --git a/contracts/varatube/wasm/Cargo.toml b/contracts/varatube/wasm/Cargo.toml index 3b4608319..11cd90a17 100644 --- a/contracts/varatube/wasm/Cargo.toml +++ b/contracts/varatube/wasm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "varatube-wasm" +name = "varatube" version.workspace = true edition.workspace = true license.workspace = true