diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6d4a843..0a392db 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,36 +1,145 @@ -name: Testing +name: Linting & Testing -on: [push] +defaults: + run: + shell: bash + +on: + push: + branches: + - 'main' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - 'main' jobs: - testing: - runs-on: ubuntu-latest + debug: + name: Debugging action on ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Installing tree + run: sudo apt-get -y install tree & which tree + + - name: Listing all files + run: tree -I "target*|.git*" + + + fmt: + name: Running cargo fmt on ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + runs-on: ${{ matrix.os }} + env: + CARGO_TERM_COLOR: always + steps: - - uses: actions/checkout@v2 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Update toolchain & add rustfmt + run: | + rustup update + rustup component add rustfmt + + - name: Run rustfmt + run: cargo fmt --all --check + + + check: + name: Running cargo check on ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + runs-on: ${{ matrix.os }} + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Update toolchain + run: rustup update - - name: Setup Node - uses: actions/setup-node@v2.1.2 + - name: Run check + run: cargo check + + + clippy: + name: Running cargo clippy on ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.os }} + env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Update toolchain & add clippy + run: | + rustup update + rustup component add clippy + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 with: - node-version: '12.x' + prefix-key: clippy-v0 + key: clippy-${{ matrix.os }} + + - name: Fetch dependencies + run: cargo fetch --locked - - name: Get yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Run clippy + run: cargo clippy -- --deny warnings - - name: Cache dependencies - uses: actions/cache@v2 + + test: + name: Running cargo test on ${{ matrix.os }} + needs: [fmt, check, clippy] + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.os }} + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Update toolchain + run: rustup update + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Node version - run: node --version - - name: npm version - run: npm --version - - name: Yarn version - run: yarn --version - - - run: yarn install --frozen-lockfile - - run: yarn test + prefix-key: test-v0 + key: test-${{ matrix.os }} + + - name: Build + run: cargo build + + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore index db96533..c1baa31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,20 @@ -.DS_Store +### Rust template +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# system files +.idea/ *.sublime-project *.sublime-workspace -codekit-config.json -*.codekit -node_modules -.sass-cache -.idea -validation-report.json -validation-status.json -npm-debug.log -lerna-debug.log -package-lock.json +.DS_Store + +# These are backup files generated by rustfmt +**/*.rs.bk -dist/* -coverage/* -__tests__/* -*.log +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb -yarn.lock -package-lock.json +# Ignore the main file as it's used for testing +src/main.rs diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index db05e36..0000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - proseWrap: 'preserve', - singleQuote: true, - trailingComma: 'es5', - useTabs: true, - printWidth: 120, -} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7f2bd3a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,491 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfonts" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9c173e616c9d9c3002750274455c39e5df91398044336dd54f496897046727" +dependencies = [ + "enable-ansi-support", + "exitcode", + "rand", + "serde", + "serde_json", + "strum", + "strum_macros", + "supports-color", + "terminal_size", +] + +[[package]] +name = "coup" +version = "1.0.0" +dependencies = [ + "cfonts", + "rand", +] + +[[package]] +name = "enable-ansi-support" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4ff3ae2a9aa54bf7ee0983e59303224de742818c1822d89f07da9856d9bc60" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c629320 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "coup" +version = "1.0.0" +edition = "2021" +authors = ["Dominik Wilkowski "] +license = "GPL-3.0-or-later" +description = "The COUP game implemented in the CLI" +homepage = "https://github.com/dominikwilkowski/coup-cli" +repository = "https://github.com/dominikwilkowski/coup-cli" +documentation = "https://docs.rs/coup-cli/" +keywords = ["game", "cli", "coup", "cards", "cardgame"] +categories = ["command-line-interface", "game-engines", "game-development", "games", "visualization"] +include = ["/assets", "/src", "!.DS_Store", "LICENSE"] + +[dependencies] +cfonts = "1.1.4" +rand = "0.8.5" diff --git a/README.md b/README.md index 5578387..b5c0f83 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,49 @@ # Coup CLI + +

+ > This is a CLI implementation of the game of [COUP](http://gamegrumps.wikia.com/wiki/Coup).

- + crates badge + crates docs tests + build status

This app is designed as a code challenge. It challenges you to write a bot that plays [COUP](http://gamegrumps.wikia.com/wiki/Coup) against other bots. -The idea is you have three rounds of (1,000,000) games to find the winner (sum all scores). +The idea is to have three rounds of (1,000,000) games to find the winner (sum all scores). Between each round you have time to make adjustments to your bot. ## How does this work? -- [RULES](#rules) +- [Rules](#rules) - [Scoring](#scoring) - [How to run the game](#how-to-run-the-game) - [How do I build a bot](#how-do-i-build-a-bot) - [How does the engine work](#how-does-the-engine-work) -- [Development](#development) -## RULES +## Rules -1. NodeJs only -1. No dependencies 1. No changes to engine -1. Name folder appropriately (so you can target specific bots) -1. No data sharing between games -1. No access to other bots +1. Name of bots don't change between rounds (so you can target specific bots) +1. No data sharing between games within a round +1. No file access to other bots 1. No changing other bots -1. No internet -1. No js prototype changing -1. Your code has to stay inside your bots folder -1. Do not output to `stdout` -1. At the beginning of each round you add PRs to the repo (we only merge on the day the round begins) +1. No internet access or calls to OpenAI +1. Do not output to `stdout` or `stderr` ## Scoring Each game is a zero-sum-game in terms of score. -The score is determined by the number of players (can't be more than 6 per game) and winners -(there are instances where the game can stall in a stale-mate with multiple winners). +That means the amount of negative points given to losers + the amount of +positive points given to winners equals to zero. + +The score is determined by the number of players (can't be more than 6 per game) +and winners (there are instances where the game can stall in a stale-mate which +the engine will stop and nominate multiple winners for). Each game will take a max of 6 bots that are randomly elected. Those who win get a positive score, those who lose will get a negative score. @@ -49,269 +52,120 @@ Those who win get a positive score, those who lose will get a negative score. ## How to run the game -

- -

- -The game comes with two simple "dumb" bots that just randomizes it's answers without checking much whether the actions are appropriate. -Each bot lives inside its own folder inside the `bots` folder. -The name of the folder determines the bots name. - -```sh -. -├── bots -│ ├── bot1 -│ │ └── index.js -│ ├── bot1 -│ │ └── index.js -│ └── bot1 -│ └── index.js -│ -├── src -│ ├── constants.js -│ ├── helper.js -│ └── index.js -│ -├── test -│ └── test.js -│ -└── README.md -``` - -To run the game `cd` into the folder. -Install dependencies (`prettier`): - -```sh -yarn -``` - -**Do make sure you run the formatter before each commit** - -Run the formatter via: - -```sh -yarn format -``` - -To play the game run: - -```sh -yarn play -``` - -To run 1000 games: - -```sh -yarn loop -``` +You can run the game in two modes: `play` and `loop`. -To run `n` number of games: +### Play mode -```sh -yarn loop -- -r [n] -``` - -In the loop rounds all output is suppressed so that the games run smoothly on the day. -For development please use the `-d` flag to enable debug mode. It will stop the game loop when it -encounters an error and display the last game with error. - -```sh -yarn loop -r [number] -d -``` - -To run the test suit: - -```sh -yarn test -``` - -## How do I build a bot - -- Create a folder in the `bots` folder (next to the fake bots) -- Pick a name for your bot (You should have a list of names before hand so bots can target specific other bots) -- Include an `index.js` file that exports below class -- Run as many test rounds as you want to -- Create PR on the day of each round - -You get to require 4 functions from the engine at `constants.js` inside your bot: - -- `ALLBOTS()` Returns an array of all players in the game `` -- `CARDS()` Returns an array of all 5 card types `` -- `DECK()` Returns an array of all cards in the deck (3 of each) -- `ACTIONS()` Returns an array of all actions `` - -> TIP: If you console log out the string `STOP` the loop will stop as soon as a game prints this and print everything out from that game. Great for debugging. -> Just make sure you remove the console log before submitting. - -### `` - -- `exampleBot1` -- `exampleBot2` - -### `` - -- `duke` -- `assassin` -- `captain` -- `ambassador` -- `contessa` - -### `` - -- `taking-1` -- `foreign-aid` -- `couping` -- `taking-3` -- `assassination` -- `stealing` -- `swapping` - -### `` - -- `foreign-aid` -> [`duke`, `false`], -- `assassination` -> [`contessa`, `false`], -- `stealing` -> [`captain`, `ambassador`, `false`], -- `taking-3` -> [`duke`, `false`], - -### Class to export - -The class you have to export from your bot needs to include the below methods: - -- `onTurn` - - Called when it is your turn to decide what you may want to do - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards }` - - returns: `{ action: , against: }` -- `onChallengeActionRound` - - Called when another bot made an action and everyone get's to decide whether they want to challenge that action - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom }` - - returns: `` -- `onCounterAction` - - Called when someone does something that can be countered with a card: `foreign-aid`, `stealing` and `assassination` - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom }` - - returns: `` -- `onCounterActionRound` - - Called when a bot did a counter action and everyone get's to decided whether they want to challenge that counter action - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom, card, counterer }` - - returns: `` -- `onSwappingCards` - - Called when you played your ambassador and now need to decide which cards you want to keep - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards, newCards }` - - returns: `Array()` -- `onCardLoss` - - Called when you lose a card to decide which one you want to lose - - parameters: `{ history, myCards, myCoins, otherPlayers, discardedCards }` - - returns: `` - -### The parameters - -Each function is passed one parameter object that can be deconstructed into the below items. - -| parameter | description | -| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `history` | The history array. More below `Array()` | -| `myCards` | An array of your cards `Array()` | -| `myCoins` | The number of coins you have | -| `otherPlayers` | An array of objects of each player, format: `[{ name: , coins: , cards: }, { name: , coins: , cards: }]` | -| `discardedCards` | An array of all cards that have been discarded so far (from penalties, coups or assassinations) | -| `action` | The action that was taken `` | -| `byWhom` | Who did the action `` | -| `toWhom` | To whom is the action directed `` | -| `card` | A string of the counter action taken by the previous bot | -| `newCards` | An array of cards for the ambassador swap `Array()` | -| `counterer` | The player who countered an action | - -### The history array - -Each event is recorded in the history array. See below a list of all events and it's entires: - -An action: +

+ +

-``` -{ - type: 'action', - action: , - from: , - to: , +The `play` mode will play a single game and nominate (a) winner(s) at the end. + +```rust +use coup::{ + bots::{HonestBot, RandomBot, StaticBot}, + Coup, +}; + +fn main() { + let mut coup_game = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(HonestBot), + Box::new(HonestBot), + Box::new(RandomBot), + Box::new(RandomBot), + ]); + + coup_game.play(); } ``` -Lose a card: +### Loop mode -``` -{ - type: 'lost-card', - player: , - lost: , +

+ +

+ +The `loop` mode will play `n` amount of games and sum all score and nominate (a) +winner(s) at the end + +```rust +use coup::{ + bots::{HonestBot, RandomBot, StaticBot}, + Coup, +}; + +fn main() { + let mut coup_game = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(HonestBot), + Box::new(HonestBot), + Box::new(RandomBot), + Box::new(RandomBot), + ]); + + coup_game.looping(1_000_000); } ``` -Challenge outcome: +## How do I build a bot -``` -{ - type: 'challenge-round' || 'counter-round', - challenger: , - challengee: , - player: , - action: , - lying: , -} -``` +Implement the `BotInterface` and override the default implementations of each of +the methods you'd like to take control over. +The default implementation are the methods of the `StaticBot` which only takes +`Income` and is forced to coup by the engine if it accumulated more or equal to +10 coins. It does not challenge, counter or counter challenge. -A Penalty: +### Methods of the bot -``` -{ - type: 'penalty', - from: , -} -``` +The methods of `BotInterface` that will define the behavior of your bot. -An unsuccessful challenge: +- `get_name` – Called only once at the instantiation of the Coup game to identify your bot +- `on_turn` – Called when it's your turn to decide what to do +- `on_auto_coup` – Called when you have equal to or more than 10 coins and must coup. +- `on_challenge_action_round` – Called when another bot played an action and everyone gets to decide whether they want to challenge that action. +- `on_counter` – Called when someone played something that can be countered with a card you may have. +- `on_challenge_counter_round` – Called when a bot played a counter. Now everyone gets to decided whether they want to challenge that counter card. +- `on_swapping_cards` – Called when you played your ambassador and now need to decide which cards you want to keep. +- `on_card_loss` – Called when you lost a card and now must decide which one you want to lose -``` -{ - type: 'unsuccessful-challenge', - action: 'swap-1', - from: , -} -``` +### The context -A counter action: +Each function gets `context` passed in which will contain below infos: -``` -{ - type: 'counter-action', - action: , - from: , - to: , - counter: , - counterer: , -} -``` +| key | description | +| -------------- | ---------------------------------------------------------- | +| `name` | Your bots name after it was deduped by the engine | +| `cards` | Your cards/influences you still have | +| `coins` | Your coins | +| `playing_bots` | A list of all playing bots this round | +| `discard_pile` | A list of all discarded cards so far in the game | +| `history` | A list of each event that has happened in this game so far | +| `score` | The current score of the game | ## How does the engine work -The challenge algorithm: - ``` -if( assassination, stealing, swapping ) - ChallengeRound via all bot.OnChallengeActionRound - ? false = continue - : true = stop - -if( foreign-aid, assassination, stealing ) - CounterAction via bot.OnCounterAction - ? false = continue - : true = CounterChallengeRound via bot.OnCounterActionRound - ? false = continue - : true = stop - -else - do-the-thing +match action + Assassination | Stealing + => + - challenge round + - counter from target + - counter challenge + - action + Coup | Income + => + - action + ForeignAid + => + - counter round from everyone + - counter challenge round + - action + Swapping | Tax + => + - challenge round + - action ``` - -## Development - -The game comes with it's own [test runner](./test/test.js) that runs through all(?) possible moves a bot can make. -You can execute the test runner via `yarn test:code`. diff --git a/assets/coup-cli.png b/assets/coup-cli.png deleted file mode 100644 index bfe47e9..0000000 Binary files a/assets/coup-cli.png and /dev/null differ diff --git a/assets/coup.png b/assets/coup.png new file mode 100644 index 0000000..302ac6a Binary files /dev/null and b/assets/coup.png differ diff --git a/assets/loop.gif b/assets/loop.gif index feb76c3..7c6bfb3 100644 Binary files a/assets/loop.gif and b/assets/loop.gif differ diff --git a/assets/play.png b/assets/play.png new file mode 100644 index 0000000..4ffd89e Binary files /dev/null and b/assets/play.png differ diff --git a/bots/exampleBot1/index.js b/bots/exampleBot1/index.js deleted file mode 100644 index 823c5ac..0000000 --- a/bots/exampleBot1/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const { ALLBOTS, CARDS, DECK, ACTIONS } = require('../../src/constants.js'); - -class BOT { - onTurn({ history, myCards, myCoins, otherPlayers, discardedCards }) { - let action = ACTIONS()[Math.floor(Math.random() * ACTIONS().length)]; - const against = otherPlayers[Math.floor(Math.random() * otherPlayers.length)].name; - - if (myCoins > 10) { - action = 'couping'; - } - - return { - action, - against, - }; - } - - onChallengeActionRound({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom }) { - return [true, false][Math.floor(Math.random() * 2)]; - } - - onCounterAction({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom }) { - if (action === 'assassination') { - return [false, 'contessa'][Math.floor(Math.random() * 2)]; - } else if (action === 'stealing') { - return [false, 'ambassador', 'captain'][Math.floor(Math.random() * 3)]; - } - } - - onCounterActionRound({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom, card }) { - return [true, false][Math.floor(Math.random() * 2)]; - } - - onSwappingCards({ history, myCards, myCoins, otherPlayers, discardedCards, newCards }) { - return newCards; - } - - onCardLoss({ history, myCards, myCoins, otherPlayers, discardedCards }) { - return myCards[0]; - } -} - -module.exports = exports = BOT; diff --git a/bots/exampleBot2/index.js b/bots/exampleBot2/index.js deleted file mode 100644 index 823c5ac..0000000 --- a/bots/exampleBot2/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const { ALLBOTS, CARDS, DECK, ACTIONS } = require('../../src/constants.js'); - -class BOT { - onTurn({ history, myCards, myCoins, otherPlayers, discardedCards }) { - let action = ACTIONS()[Math.floor(Math.random() * ACTIONS().length)]; - const against = otherPlayers[Math.floor(Math.random() * otherPlayers.length)].name; - - if (myCoins > 10) { - action = 'couping'; - } - - return { - action, - against, - }; - } - - onChallengeActionRound({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom }) { - return [true, false][Math.floor(Math.random() * 2)]; - } - - onCounterAction({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom }) { - if (action === 'assassination') { - return [false, 'contessa'][Math.floor(Math.random() * 2)]; - } else if (action === 'stealing') { - return [false, 'ambassador', 'captain'][Math.floor(Math.random() * 3)]; - } - } - - onCounterActionRound({ history, myCards, myCoins, otherPlayers, discardedCards, action, byWhom, toWhom, card }) { - return [true, false][Math.floor(Math.random() * 2)]; - } - - onSwappingCards({ history, myCards, myCoins, otherPlayers, discardedCards, newCards }) { - return newCards; - } - - onCardLoss({ history, myCards, myCoins, otherPlayers, discardedCards }) { - return myCards[0]; - } -} - -module.exports = exports = BOT; diff --git a/package.json b/package.json deleted file mode 100644 index 99ebc95..0000000 --- a/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "coup-cli", - "version": "1.0.0", - "description": "The COUP game implemented in the CLI", - "homepage": "https://github.com/dominikwilkowski/coup-cli", - "scripts": { - "test:format": "prettier --list-different \"**/*.{js,md,mdx,json,html}\"", - "test:code": "node test/test.js", - "test": "npm run test:format && npm run test:code", - "format": "prettier --write \"**/*.{js,md,mdx,json,html}\"", - "play": "node src/bin.js play", - "loop": "node src/bin.js loop" - }, - "main": "./src/index.js", - "bin": { - "coup": "./src/bin.js" - }, - "keywords": [ - "cli", - "coup", - "game" - ], - "author": { - "name": "Dominik Wilkowski", - "email": "Hi@Dominik-Wilkowski.com", - "url": "https://dominik-wilkowski.com/" - }, - "repository": { - "type": "git", - "url": "git://github.com/dominikwilkowski/coup-cli.git" - }, - "bugs": { - "url": "https://github.com/dominikwilkowski/coup-cli/issues" - }, - "licenses": [ - { - "type": "GPL-3.0", - "url": "https://github.com/dominikwilkowski/coup-cli/blob/main/LICENSE" - } - ], - "license": "GPL-3.0", - "devDependencies": { - "prettier": "^2.3.2" - } -} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7de38fb --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,16 @@ +max_width = 80 +newline_style = "Unix" +chain_width = 80 +fn_call_width = 80 +match_block_trailing_comma = true + +hard_tabs = true +tab_spaces = 2 + +# unstable_features = true +# blank_lines_upper_bound = 1 +# blank_lines_lower_bound = 1 +# format_code_in_doc_comments = true +# imports_layout = "HorizontalVertical" +# control_brace_style = "ClosingNextLine" +# imports_granularity = "Crate" diff --git a/src/bin.js b/src/bin.js deleted file mode 100755 index 5d6e2ad..0000000 --- a/src/bin.js +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node - -const { COUP, LOOP } = require('./index.js'); - -if (process.argv.includes('play') || !process.argv.includes('loop')) { - new COUP().play(); -} - -if (process.argv.includes('loop')) { - const loop = new LOOP(); - const debug = process.argv.includes('-d'); - - loop.run(debug); -} diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..71ef512 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,162 @@ +//! The bot trait [BotInterface] and a couple types that help with the bot +//! implementation. +//! +//! ```rust +//! use coup::bot::BotInterface; +//! +//! pub struct MyBot; +//! +//! // The minimal implementation of a bot with the StaticBot default trait methods: +//! impl BotInterface for MyBot { +//! fn get_name(&self) -> String { +//! String::from("Kate") +//! } +//! } +//! ``` + +use crate::{Action, Card, History, Score}; + +/// A bot struct can be used to implement the [BotInterface] trait +#[derive(Debug, Clone, Copy)] +pub struct Bot; + +/// A description of other bots current state who are still in the game. +#[derive(Debug, Clone, PartialEq)] +pub struct OtherBot { + /// The name of the bot used to identify it + pub name: String, + /// The amount of coins this bot has + pub coins: u8, + /// The amount of [Card] this bot still have + pub cards: u8, +} + +/// The context struct is what is passed into each of the [BotInterface] methods +/// as arguments so the bot knows the context of the current move. +/// This is where your game state is stored including your current cards and +/// coins but also what other bots are still in the game, the discard pile etc. +#[derive(Debug, Clone, PartialEq)] +pub struct Context { + /// Your bots name after it was deduped by the engine as identifier + pub name: String, + /// Your cards/influences you still have + pub cards: Vec, + /// Your coins + pub coins: u8, + /// A list of all playing bots this round + pub playing_bots: Vec, + /// A list of all discarded [Card] so far in the game + pub discard_pile: Vec, + /// A list of each event that has happened in this game so far + pub history: Vec, + /// The current score of the game + pub score: Score, +} + +/// The BotInterface trait is what drives your bot. +/// Implementing each method below will define your bots behavior. +/// The default implementation is a static implementation of a bot like the +/// pre-build [crate::bots::StaticBot]. +pub trait BotInterface { + /// Called only once at the instantiation of the Coup game to identify your bot. + /// The name might get a number appended if there is another bot with the same name. + fn get_name(&self) -> String; + + /// Called when it's your turn to decide what to do. + /// + /// The static implementation always plays [Action::Income]. + fn on_turn(&self, _context: &Context) -> Action { + Action::Income + } + + /// Called when you have equal to or more than 10 coins and must coup. + /// You can use this method internally as well when you decide to coup on + /// your own. + /// + /// The static implementation coups the first bot it finds that isn't itself. + fn on_auto_coup(&self, context: &Context) -> String { + context + .playing_bots + .iter() + .find(|bot| bot.name != context.name) + .unwrap() + .name + .clone() + } + + /// Called when another bot played an action and everyone gets to decide + /// whether they want to challenge that action. + /// + /// Called for: + /// - [Action::Assassination] + /// - [Action::Swapping] + /// - [Action::Stealing] + /// - [Action::Tax] + /// + /// The static implementation never challenges. + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + false + } + + /// Called when someone played something that can be countered with a card + /// you may have. + /// + /// Called for: + /// - [Action::Assassination] + /// - [Action::ForeignAid] + /// - [Action::Stealing] + /// + /// The static implementation never counters. + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + false + } + + /// Called when a bot played a counter. Now everyone gets to decided whether + /// they want to challenge that counter card. + /// + /// Called for: + /// - [Action::Assassination] + /// - [Action::ForeignAid] + /// - [Action::Stealing] + /// + /// The static implementation never counter-challenges. + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + false + } + + /// Called when you played your ambassador and now need to decide which cards + /// you want to keep. + /// Return the cards you don't want anymore. They will be shuffled back into + /// the deck. + /// + /// The static implementation gives back the cards it got from the deck. + fn on_swapping_cards( + &self, + new_cards: [Card; 2], + _context: &Context, + ) -> [Card; 2] { + new_cards + } + + /// Called when you lost a card and now must decide which one you want to lose. + /// + /// The static implementation discards the first card it finds. + fn on_card_loss(&self, context: &Context) -> Card { + context.cards.clone().pop().unwrap() + } +} diff --git a/src/bots/honest_bot.rs b/src/bots/honest_bot.rs new file mode 100644 index 0000000..8529157 --- /dev/null +++ b/src/bots/honest_bot.rs @@ -0,0 +1,158 @@ +//! An honest bot implementation for you to use to test your own bot with. + +use crate::{ + bot::{BotInterface, Context}, + Action, Card, +}; + +/// The honest bot will try to take all actions it should take without being too +/// smart. It will act on it's own cards, counter other bots if they do +/// something that it can counter based on its cards and will never bluff. +pub struct HonestBot; + +impl BotInterface for HonestBot { + /// HonestBot is the name + fn get_name(&self) -> String { + String::from("HonestBot") + } + + /// Acts on cards it has and falls back to [Action::Income]. + /// Never plays [Action::ForeignAid] or [Action::Swapping]. + fn on_turn(&self, context: &Context) -> Action { + let target = context + .playing_bots + .iter() + .filter(|bot| bot.name != context.name) + .min_by_key(|bot| bot.cards) + .unwrap(); + + if context.cards.contains(&Card::Assassin) && context.coins >= 3 { + Action::Assassination(target.name.clone()) + } else if context.cards.contains(&Card::Captain) { + Action::Stealing(target.name.clone()) + } else if context.cards.contains(&Card::Duke) { + Action::Tax + } else { + Action::Income + } + } + + /// Looks for the bot with the least cards + fn on_auto_coup(&self, context: &Context) -> String { + let target = context + .playing_bots + .iter() + .filter(|bot| bot.name != context.name) + .min_by_key(|bot| bot.cards) + .unwrap(); + target.name.clone() + } + + /// Challenges only if it can see all three cards associated with the current + /// action in either the discard pile or its own hand. + fn on_challenge_action_round( + &self, + action: &Action, + _by: String, + context: &Context, + ) -> bool { + let mut all_visible_cards = context.cards.clone(); + all_visible_cards.extend(context.discard_pile.clone()); + + match action { + Action::Assassination(_) => { + all_visible_cards.iter().filter(|card| **card == Card::Assassin).count() + == 3 + }, + Action::Swapping => { + all_visible_cards + .iter() + .filter(|card| **card == Card::Ambassador) + .count() == 3 + }, + Action::Stealing(_) => { + all_visible_cards.iter().filter(|card| **card == Card::Captain).count() + == 3 + }, + Action::Tax => { + all_visible_cards.iter().filter(|card| **card == Card::Duke).count() + == 3 + }, + Action::Coup(_) | Action::ForeignAid | Action::Income => { + unreachable!("Can't challenge couping or Income") + }, + } + } + + /// Counters only if it has the card to counter + fn on_counter( + &self, + action: &Action, + _by: String, + context: &Context, + ) -> bool { + match action { + Action::Assassination(_) => context.cards.contains(&Card::Contessa), + Action::ForeignAid => context.cards.contains(&Card::Duke), + Action::Stealing(_) => { + context.cards.contains(&Card::Captain) + || context.cards.contains(&Card::Ambassador) + }, + Action::Coup(_) | Action::Swapping | Action::Income | Action::Tax => { + unreachable!("Can't challenge couping or Income") + }, + } + } + + /// Counter-challenges only if it can see all three cards associated with the + /// current action in either the discard pile or its own hand. + fn on_challenge_counter_round( + &self, + action: &Action, + _by: String, + context: &Context, + ) -> bool { + let mut all_visible_cards = context.cards.clone(); + all_visible_cards.extend(context.discard_pile.clone()); + + match action { + Action::Assassination(_) => { + all_visible_cards.iter().filter(|card| **card == Card::Contessa).count() + == 3 + }, + Action::ForeignAid => context.cards.contains(&Card::Duke), + Action::Stealing(_) => { + all_visible_cards.iter().filter(|card| **card == Card::Captain).count() + == 3 && all_visible_cards + .iter() + .filter(|card| **card == Card::Ambassador) + .count() == 3 + }, + Action::Coup(_) | Action::Income | Action::Swapping | Action::Tax => { + unreachable!("Can't challenge couping or Income") + }, + } + } + + /// Swaps duplicate cards + fn on_swapping_cards( + &self, + new_cards: [Card; 2], + context: &Context, + ) -> [Card; 2] { + let mut discard_cards = Vec::new(); + if context.cards[0] == context.cards[1] { + discard_cards.push(context.cards[0]) + } else { + discard_cards.push(new_cards[0]); + } + discard_cards.push(new_cards[1]); + + [discard_cards[0], discard_cards[1]] + } + + /// Takes the first card to discard + fn on_card_loss(&self, context: &Context) -> Card { + context.cards.clone().pop().unwrap() + } +} diff --git a/src/bots/mod.rs b/src/bots/mod.rs new file mode 100644 index 0000000..e69ae70 --- /dev/null +++ b/src/bots/mod.rs @@ -0,0 +1,9 @@ +//! A collection of pre-built bots to test with. + +pub mod honest_bot; +pub mod random_bot; +pub mod static_bot; + +pub use honest_bot::HonestBot; +pub use random_bot::RandomBot; +pub use static_bot::StaticBot; diff --git a/src/bots/random_bot.rs b/src/bots/random_bot.rs new file mode 100644 index 0000000..44b6dd7 --- /dev/null +++ b/src/bots/random_bot.rs @@ -0,0 +1,111 @@ +//! A random bot implementation for you to use to test your own bot with. + +use rand::{seq::SliceRandom, thread_rng}; + +use crate::{ + bot::{BotInterface, Context, OtherBot}, + Action, Card, +}; + +/// The random bot will not think about anything but will, just like monkey +/// testing, throw some randomness into your tests with your own bot and helps +/// the engine test its robustness. +pub struct RandomBot; + +impl BotInterface for RandomBot { + /// RandomBot is the name + fn get_name(&self) -> String { + String::from("RandomBot") + } + + /// Randomizes all possible [Action] + fn on_turn(&self, context: &Context) -> Action { + let mut targets = context.playing_bots.clone(); + targets = targets + .iter() + .filter(|bot| bot.name != context.name) + .cloned() + .collect::>(); + targets.shuffle(&mut thread_rng()); + + let mut actions = [ + Action::Assassination(targets[0].name.clone()), + Action::Coup(targets[0].name.clone()), + Action::ForeignAid, + Action::Swapping, + Action::Income, + Action::Stealing(targets[0].name.clone()), + Action::Tax, + ]; + actions.shuffle(&mut thread_rng()); + actions[0].clone() + } + + /// Randomizes who it coups + fn on_auto_coup(&self, context: &Context) -> String { + let mut targets = context.playing_bots.clone(); + targets = targets + .iter() + .filter(|bot| bot.name != context.name) + .cloned() + .collect::>(); + targets.shuffle(&mut thread_rng()); + targets[0].name.clone() + } + + /// Randomizes if it challenges or not + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + let mut challange = [true, false]; + challange.shuffle(&mut thread_rng()); + challange[0] + } + + /// Randomizes if it counters or not + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + let mut counter = [true, false]; + counter.shuffle(&mut thread_rng()); + counter[0] + } + + /// Randomizes if it counter-challenges or not + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + let mut challange = [true, false]; + challange.shuffle(&mut thread_rng()); + challange[0] + } + + /// Randomizes what card it swaps + fn on_swapping_cards( + &self, + new_cards: [Card; 2], + context: &Context, + ) -> [Card; 2] { + let mut all_visible_cards = context.cards.clone(); + all_visible_cards.extend(new_cards); + all_visible_cards.shuffle(&mut thread_rng()); + + [all_visible_cards[0], all_visible_cards[1]] + } + + /// Randomizes what card it discards + fn on_card_loss(&self, context: &Context) -> Card { + let mut cards = context.cards.clone(); + cards.shuffle(&mut thread_rng()); + cards[0] + } +} diff --git a/src/bots/static_bot.rs b/src/bots/static_bot.rs new file mode 100644 index 0000000..38cb05e --- /dev/null +++ b/src/bots/static_bot.rs @@ -0,0 +1,15 @@ +//! A static bot implementation for you to use to test your own bot with. + +use crate::bot::BotInterface; + +/// The static bot only takes [crate::Action::Income] on turns and is eventually +/// forced by the engine to coup another bot. +/// It won't challenge, counter or act on its own cards at all. +pub struct StaticBot; + +impl BotInterface for StaticBot { + /// StaticBot is the name + fn get_name(&self) -> String { + String::from("StaticBot") + } +} diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index d1c76e4..0000000 --- a/src/constants.js +++ /dev/null @@ -1,60 +0,0 @@ -const path = require('path'); -const fs = require('fs'); - -const getPlayer = (thisPath = './bots/') => { - const allPlayer = fs - .readdirSync(thisPath) - .map((name) => path.join(process.cwd(), thisPath, name)) - .filter( - (folder) => - fs.lstatSync(folder).isDirectory() && - !folder.endsWith('assets') && - !folder.startsWith('.') && - folder !== 'node_modules' - ) - .map((name) => name.split('/').slice(-1)[0]); - - if (allPlayer.length < 2) { - console.error(`\n🛑 We need at least two player to play this game!\n`); - process.exit(1); - } else { - return allPlayer; - } -}; - -const ALLBOTS = getPlayer; - -// prettier-ignore -const CARDS = () => [ - 'duke', - 'assassin', - 'captain', - 'ambassador', - 'contessa' -]; - -const getStack = (cards = CARDS()) => { - let STACK = []; - cards.forEach((card) => (STACK = [...STACK, ...new Array(3).fill(card)])); - return STACK; -}; - -const DECK = getStack; - -// prettier-ignore -const ACTIONS = () => [ - 'taking-1', - 'foreign-aid', - 'couping', - 'taking-3', - 'assassination', - 'stealing', - 'swapping' -]; - -module.exports = exports = { - ALLBOTS, - CARDS, - DECK, - ACTIONS, -}; diff --git a/src/helper.js b/src/helper.js deleted file mode 100644 index aa9c8b3..0000000 --- a/src/helper.js +++ /dev/null @@ -1,23 +0,0 @@ -const style = { - parse: (text, start, end = '39m') => { - if (text !== undefined) { - return `\u001B[${start}${text}\u001b[${end}`; - } else { - return ``; - } - }, - black: (text) => style.parse(text, '30m'), - red: (text) => style.parse(text, '31m'), - green: (text) => style.parse(text, '32m'), - yellow: (text) => style.parse(text, '33m'), - blue: (text) => style.parse(text, '34m'), - magenta: (text) => style.parse(text, '35m'), - cyan: (text) => style.parse(text, '36m'), - white: (text) => style.parse(text, '37m'), - gray: (text) => style.parse(text, '90m'), - bold: (text) => style.parse(text, '1m', '22m'), -}; - -module.exports = exports = { - style, -}; diff --git a/src/index.js b/src/index.js deleted file mode 100755 index bcaa053..0000000 --- a/src/index.js +++ /dev/null @@ -1,915 +0,0 @@ -const path = require('path'); - -let { ALLBOTS, CARDS, DECK, ACTIONS } = require('./constants.js'); -const { version } = require('../package.json'); -const { style } = require('./helper.js'); - -// making clones so the bots don't break them -CARDS = CARDS(); -DECK = DECK(); -ACTIONS = ACTIONS(); - -class COUP { - constructor() { - // yes globals(sorta); sue me! - this.HISTORY = []; - this.DISCARDPILE = []; - this.BOTS = {}; - this.PLAYER = {}; - this.DECK = DECK.slice(0); - this.TURN = 0; - this.ROUNDS = 0; - this.ALLPLAYER = []; - } - - play() { - console.log( - `\n\n` + - ` ██████${style.yellow('╗')} ██████${style.yellow('╗')} ██${style.yellow('╗')} ██${style.yellow( - '╗' - )} ██████${style.yellow('╗')}\n` + - ` ██${style.yellow('╔════╝')} ██${style.yellow('╔═══')}██${style.yellow('╗')} ██${style.yellow( - '║' - )} ██${style.yellow('║')} ██${style.yellow('╔══')}██${style.yellow('╗')}\n` + - ` ██${style.yellow('║')} ██${style.yellow('║')} ██${style.yellow('║')} ██${style.yellow( - '║' - )} ██${style.yellow('║')} ██████${style.yellow('╔╝')}\n` + - ` ██${style.yellow('║')} ██${style.yellow('║')} ██${style.yellow('║')} ██${style.yellow( - '║' - )} ██${style.yellow('║')} ██${style.yellow('╔═══╝')}\n` + - ` ${style.yellow('╚')}██████${style.yellow('╗')} ${style.yellow('╚')}██████${style.yellow( - '╔╝' - )} ${style.yellow('╚')}██████${style.yellow('╔╝')} ██${style.yellow('║')}\n` + - ` ${style.yellow('╚═════╝ ╚═════╝ ╚═════╝ ╚═╝')} v${version}\n` + - `\n` - ); - - this.ALLPLAYER = this.getPlayer(); - this.getBots(); - this.makePlayers(); - this.handOutCards(); - this.electStarter(); - - // this is the game loop - return this.turn(); - } - - //////////////////////////////////////////////////////////////////////////////| Init methods - - // We need to make sure we don't play with more than 6 bots at a time - // So here we make sure we shuffle all bots randomly and take 6 for this round - getPlayer() { - return ALLBOTS() - .filter((item) => item !== undefined) - .map((item) => [Math.random(), item]) - .sort((a, b) => a[0] - b[0]) - .map((item) => item[1]) - .slice(0, 6); - } - - // we collect all players of this round and require them into memory - // We also make sure each bot has all methods at least defined - getBots(player) { - try { - this.ALLPLAYER.forEach((player) => { - const botPath = path.normalize(`${__dirname}/../bots/${player}/index.js`); - const bot = require(botPath); - this.BOTS[player] = new bot({ name: player }); - - if ( - !this.BOTS[player].onTurn || - !this.BOTS[player].onChallengeActionRound || - !this.BOTS[player].onCounterAction || - !this.BOTS[player].onCounterActionRound || - !this.BOTS[player].onSwappingCards || - !this.BOTS[player].onCardLoss - ) { - const missing = [ - 'onTurn', - 'onChallengeActionRound', - 'onCounterAction', - 'onCounterActionRound', - 'onSwappingCards', - 'onCardLoss', - ].filter((method) => !Object.keys(this.BOTS[player]).includes(method)); - - throw `🚨 ${style.red('The bot ')}${style.yellow(player)}${style.red( - ` is missing ${missing.length > 1 ? 'methods' : 'a method'}: ` - )}${style.yellow(missing.join(', '))}!\n`; - } - }); - } catch (error) { - console.error(`Error in bot ${player} at ${botPath}`); - console.error(error); - process.exit(1); - } - } - - // giving each player the right object so we can use them - makePlayers(players) { - players = this.shufflePlayer(this.ALLPLAYER); - - players.forEach((player) => { - this.PLAYER[player] = { - card1: undefined, - card2: undefined, - coins: 0, - }; - }); - } - - shuffleCards() { - this.DECK = this.DECK.filter((item) => item !== undefined) - .map((item) => [Math.random(), item]) - .sort((a, b) => a[0] - b[0]) - .map((item) => item[1]); - } - - // this shuffles all play of this round only - shufflePlayer(player) { - return player - .filter((item) => item !== undefined) - .map((item) => [Math.random(), item]) - .sort((a, b) => a[0] - b[0]) - .map((item) => item[1]); - } - - handOutCards() { - this.shuffleCards(); - - Object.entries(this.PLAYER).forEach(([key, value]) => { - this.PLAYER[key].card1 = this.DECK.pop(); - this.PLAYER[key].card2 = this.DECK.pop(); - }); - } - - electStarter() { - this.TURN = Math.floor(Math.random() * Object.keys(this.PLAYER).length); - } - - //////////////////////////////////////////////////////////////////////////////| Play methods - - turn() { - const player = Object.keys(this.PLAYER)[this.goToNextPlayer()]; - - let botAnswer; - try { - botAnswer = this.BOTS[player].onTurn(this.getGameState(player)); - } catch (error) { - this.penalty(player, `the bot crashed`); - console.error(`Error in bot ${player}`); - console.error(error); - } - - if (botAnswer && this.isValidTarget(botAnswer.action, botAnswer.against, player)) { - const { action, against } = botAnswer; - const playerAvatar = this.getAvatar(player); - const targetAvatar = this.getAvatar(against); - - let skipAction = false; - - switch (action) { - case 'taking-1': - this.HISTORY.push({ - type: 'action', - action: 'taking-1', - from: player, - }); - console.log(`🃏 ${playerAvatar} takes ${style.yellow('a coin')}`); - break; - case 'foreign-aid': - this.HISTORY.push({ - type: 'action', - action: 'foreign-aid', - from: player, - }); - console.log(`🃏 ${playerAvatar} takes 2 coins ${style.yellow('foreign aid')}`); - break; - case 'couping': - this.HISTORY.push({ - type: 'action', - action: 'couping', - from: player, - to: against, - }); - console.log(`🃏 ${playerAvatar} coups ${targetAvatar}`); - - if (this.PLAYER[player].coins < 7) { - this.penalty(player, `did't having enough coins for a coup`); - skipAction = true; - } - - if (!this.stillAlive(against)) { - this.penalty(player, `tried to coup a dead player`); - skipAction = true; - } - break; - case 'taking-3': - this.HISTORY.push({ - type: 'action', - action: 'taking-3', - from: player, - }); - console.log(`🃏 ${playerAvatar} takes 3 coins with the ${style.yellow('duke')}`); - break; - case 'assassination': - this.HISTORY.push({ - type: 'action', - action: 'assassination', - from: player, - to: against, - }); - console.log(`🃏 ${playerAvatar} assassinates ${targetAvatar}`); - - if (this.PLAYER[player].coins < 3) { - this.penalty(player, `did't have enough coins for an assassination`); - skipAction = true; - } else if (!this.stillAlive(against)) { - this.penalty(player, `tried to assassinat a dead player`); - skipAction = true; - } else { - this.PLAYER[player].coins -= 3; - } - break; - case 'stealing': - this.HISTORY.push({ - type: 'action', - action: 'stealing', - from: player, - to: against, - }); - - if (!this.stillAlive(against)) { - this.penalty(player, `tried to steal from a dead player`); - skipAction = true; - } - - console.log(`🃏 ${playerAvatar} steals from ${targetAvatar}`); - break; - case 'swapping': - this.HISTORY.push({ - type: 'action', - action: 'swapping', - from: player, - }); - console.log(`🃏 ${playerAvatar} swaps two cards with the ${style.yellow('ambassador')}`); - break; - default: - this.HISTORY.push({ - type: 'penalty', - from: player, - }); - this.penalty( - player, - `of issuing an invalid action: "${style.yellow(action)}", allowed: ${style.yellow(ACTIONS.join(', '))}` - ); - skipAction = true; - } - - if (!skipAction) this.runChallenges({ player, action, target: against }); - } - - if (this.whoIsLeft().length > 1 && this.ROUNDS < 1000) { - this.ROUNDS++; - return this.turn(); - } else if (this.ROUNDS >= 1000) { - console.log('The game was stopped because of an infinite loop'); - return this.whoIsLeft(); - } else { - const winner = this.whoIsLeft()[0]; - console.log(`\nThe winner is ${this.getAvatar(winner)}\n`); - return [winner]; - } - } - - goToNextPlayer() { - this.TURN++; - - if (this.TURN > Object.keys(this.PLAYER).length - 1) { - this.TURN = 0; - } - - if ( - this.PLAYER[Object.keys(this.PLAYER)[this.TURN]].card1 || - this.PLAYER[Object.keys(this.PLAYER)[this.TURN]].card2 - ) { - return this.TURN; - } else { - return this.goToNextPlayer(); - } - } - - getCardFromDeck() { - const newCard = this.DECK.pop(); - - if (!newCard && this.DECK.length > 0) { - return this.getCardFromDeck(); - } else { - return newCard; - } - } - - exchangeCard(card) { - this.DECK.push(card); - this.shuffleCards(); - - return this.getCardFromDeck(); - } - - swapCards({ chosenCards = [], newCards, player }) { - let oldCards = []; - if (this.PLAYER[player].card1) oldCards.push(this.PLAYER[player].card1); - if (this.PLAYER[player].card2) oldCards.push(this.PLAYER[player].card2); - - let allCards = oldCards.slice(0); - if (newCards[0]) allCards.push(newCards[0]); - if (newCards[1]) allCards.push(newCards[1]); - - chosenCards = chosenCards.filter((card) => allCards.includes(card)).slice(0, oldCards.length); - - this.PLAYER[player].card1 = chosenCards[0]; - this.PLAYER[player].card2 = chosenCards[1]; - - allCards - .filter((card) => { - if (card && card === chosenCards[0]) { - chosenCards[0] = undefined; - return false; - } - if (card && card === chosenCards[1]) { - chosenCards[1] = undefined; - return false; - } - return true; - }) - .map((card) => this.DECK.push(card)); - - this.shuffleCards(); - } - - stillAlive(player) { - let cards = 0; - if (this.PLAYER[player].card1) cards++; - if (this.PLAYER[player].card2) cards++; - - return cards > 0; - } - - whoIsLeft() { - return Object.keys(this.PLAYER).filter((player) => this.PLAYER[player].card1 || this.PLAYER[player].card2); - } - - getPlayerObjects(players, filter = '') { - return players - .filter((user) => user !== filter) - .map((player) => { - let cards = 0; - if (this.PLAYER[player].card1) cards++; - if (this.PLAYER[player].card2) cards++; - - return { - name: player, - coins: this.PLAYER[player].coins, - cards, - }; - }); - } - - getGameState(player) { - return { - history: this.HISTORY.slice(0), - myCards: this.getPlayerCards(player), - myCoins: this.PLAYER[player].coins, - otherPlayers: this.getPlayerObjects(this.whoIsLeft(), player), - discardedCards: this.DISCARDPILE.slice(0), - }; - } - - isValidTarget(action, target, player) { - const doesExist = Object.keys(this.PLAYER).includes(target); - const isRequired = ['couping', 'assassination', 'stealing'].includes(action); - const isValid = (isRequired && doesExist) || !isRequired; - - if (!isValid) { - this.penalty(player, `the bot gave invalid target "${target}"`); - } - - return isValid; - } - - // We leave this here for debugging our code - wait(time) { - return new Promise((resolve) => setTimeout(resolve, time)); - } - - getAvatar(player) { - if (!player) { - return player; - } else if (!this.ALLPLAYER.includes(player)) { - return `[${style.yellow(`${player}`)} -not found-]`; - } else { - return ( - style.yellow(`[${player} `) + - // `${ this.PLAYER[ player ].card1 ? `${ style.red( this.PLAYER[ player ].card1.substring( 0, 2 ) ) } ` : '' }` + - // `${ this.PLAYER[ player ].card2 ? `${ style.red( this.PLAYER[ player ].card2.substring( 0, 2 ) ) } ` : '' }` + - `${this.PLAYER[player].card1 ? style.red('♥') : ''}` + - `${this.PLAYER[player].card2 ? style.red('♥') : ''}` + - ` ${style.yellow(`💰 ${this.PLAYER[player].coins}]`)}` - ); - } - } - - getPlayerCards(player) { - const myCards = []; - if (this.PLAYER[player].card1) myCards.push(this.PLAYER[player].card1); - if (this.PLAYER[player].card2) myCards.push(this.PLAYER[player].card2); - return myCards; - } - - losePlayerCard(player, card) { - let lost = ''; - - if (this.PLAYER[player].card1 === card) { - lost = this.PLAYER[player].card1; - this.PLAYER[player].card1 = undefined; - } else if (this.PLAYER[player].card2 === card) { - lost = this.PLAYER[player].card2; - this.PLAYER[player].card2 = undefined; - } - - this.HISTORY.push({ - type: 'lost-card', - player, - lost, - }); - - this.DISCARDPILE.push(lost); - - let lives = 0; - if (this.PLAYER[player].card1) lives++; - if (this.PLAYER[player].card2) lives++; - - console.log(`${lives > 0 ? '💔' : '☠️ '} ${this.getAvatar(player)} has lost the ${style.yellow(lost)}`); - } - - penalty(player, reason) { - let penalty = ''; - - let lostCard; - try { - lostCard = this.BOTS[player].onCardLoss(this.getGameState(player)); - } catch (error) { - this.PLAYER[player].card1 = undefined; - this.PLAYER[player].card2 = undefined; - console.error(`Error in bot ${player}`); - console.error(error); - } - - const _validCard = [this.PLAYER[player].card1, this.PLAYER[player].card2].includes(lostCard) && lostCard; - - if ((_validCard && this.PLAYER[player].card1 === lostCard) || (!_validCard && this.PLAYER[player].card1)) { - penalty = this.PLAYER[player].card1; - } else if ((_validCard && this.PLAYER[player].card2 === lostCard) || (!_validCard && this.PLAYER[player].card2)) { - penalty = this.PLAYER[player].card2; - } - - console.log(`🚨 ${this.getAvatar(player)} was penalised because ${style.yellow(reason)}`); - this.losePlayerCard(player, penalty); - } - - resolveChallenge({ challenger, byWhom, card, action, type, target, counterer, challengee }) { - const challengeTypes = { - 'challenge-round': 'onChallengeActionRound', - 'counter-round': 'onCounterActionRound', - }; - - let botAnswer; - try { - botAnswer = this.BOTS[challenger][challengeTypes[type]]({ - ...this.getGameState(challenger), - action, - byWhom, - toWhom: target, - counterer, - card, - }); - } catch (error) { - this.penalty(challenger, `the bot crashed`); - console.error(`Error in bot ${challenger}`); - console.error(error); - } - - if (botAnswer) { - const lying = this.PLAYER[challengee].card1 !== card && this.PLAYER[challengee].card2 !== card; - - this.HISTORY.push({ - type, - challenger, - challengee, - action, - lying, - }); - - console.log(`❓ ${this.getAvatar(challengee)} was challenged by ${this.getAvatar(challenger)}`); - - if (lying) { - this.HISTORY.push({ - type: 'penalty', - player: challengee, - }); - - this.penalty(challengee, 'of lying'); - - return true; - } else { - this.HISTORY.push({ - type: 'penalty', - from: challenger, - }); - - this.penalty(challenger, `of challenging ${this.getAvatar(challengee)} unsuccessfully`); - const newCard = this.exchangeCard(card); - - if (this.PLAYER[challengee].card1 === card) this.PLAYER[challengee].card1 = newCard; - else if (this.PLAYER[challengee].card2 === card) this.PLAYER[challengee].card2 = newCard; - - this.HISTORY.push({ - type: 'unsuccessful-challenge', - action: 'swap-1', - from: challengee, - card: card, - }); - console.log( - `↬ ${this.getAvatar(challengee)} put the ${style.yellow(card)} back in the deck and drew a new card` - ); - - return 'done'; - } - } - - return false; - } - - challengeRound({ player, target, card, action, type, counterer }) { - let _hasBeenChallenged = false; - - const challengee = type === 'counter-round' ? counterer : player; - - Object.keys(this.PLAYER) - .filter( - (challenger) => challenger !== challengee && (this.PLAYER[challenger].card1 || this.PLAYER[challenger].card2) - ) - .some((challenger) => { - _hasBeenChallenged = this.resolveChallenge({ - challenger, - byWhom: player, - card, - action, - type, - target, - counterer, - challengee, - }); - return _hasBeenChallenged === 'done' ? true : _hasBeenChallenged; - }); - - return _hasBeenChallenged; - } - - counterAction({ player, action, target }) { - const actions = { - 'foreign-aid': ['duke', false], - assassination: ['contessa', false], - stealing: ['captain', 'ambassador', false], - }; - const counter = {}; - if (action !== 'foreign-aid') { - try { - counter.counterAction = this.BOTS[target].onCounterAction({ - ...this.getGameState(target), - action, - byWhom: player, - toWhom: target, - }); - counter.counterer = target; - } catch (error) { - this.penalty(target, `the bot crashed`); - console.error(`Error in bot ${target}`); - console.error(error); - } - } else { - // Foreign aid. everyone gets a go! - Object.keys(this.PLAYER) - .filter((counterer) => counterer !== player && (this.PLAYER[counterer].card1 || this.PLAYER[counterer].card2)) - .some((counterer) => { - let _hasBeenChallenged; - try { - _hasBeenChallenged = this.BOTS[counterer].onCounterAction({ - ...this.getGameState(counterer), - action, - byWhom: player, - toWhom: undefined, - }); - } catch (error) { - this.penalty(counterer, `the bot crashed`); - console.error(`Error in bot ${counterer}`); - console.error(error); - } - - if (_hasBeenChallenged) { - counter.counterAction = _hasBeenChallenged; - counter.counterer = counterer; - return true; - } - }); - } - - if (counter.counterAction) { - if (!actions[action].includes(counter.counterAction)) { - this.penalty( - counter.counterer, - `did't give a valid counter action ${style.yellow(counter.counterAction)} for ${style.yellow(action)}` - ); - return true; - } - - this.HISTORY.push({ - type: 'counter-action', - action, - from: player, - to: target, - counter: counter.counterAction, - counterer: counter.counterer, - }); - console.log( - `❓ ${this.getAvatar(player)} was counter actioned by ${this.getAvatar(counter.counterer)} with ${style.yellow( - counter.counterAction - )}` - ); - const _hasBeenChallenged = this.challengeRound({ - player, - target, - card: counter.counterAction, - action, - type: 'counter-round', - counterer: counter.counterer, - }); - return _hasBeenChallenged === 'done' ? true : !_hasBeenChallenged; - } - - return false; - } - - runChallenges({ action, player, target }) { - if (action === 'taking-3' || action === 'assassination' || action === 'stealing' || action === 'swapping') { - const card = { - 'taking-3': 'duke', - assassination: 'assassin', - stealing: 'captain', - swapping: 'ambassador', - }[action]; - - const _hasBeenChallenged = this.challengeRound({ - player, - card, - action, - type: 'challenge-round', - target, - }); - if (_hasBeenChallenged && _hasBeenChallenged !== 'done') { - return; - } - } - - if (action === 'foreign-aid' || action === 'assassination' || action === 'stealing') { - const _hasBeenChallenged = this.counterAction({ player, action, target }); - if (_hasBeenChallenged && _hasBeenChallenged !== 'done') { - return; - } - } - - this.runActions({ player, action, target }); - } - - runActions({ player, action, target }) { - if (!this.PLAYER[target] && !['taking-1', 'taking-3', 'swapping', 'foreign-aid'].includes(action)) { - this.penalty(player, `did't give a valid (${target}) player`); - return true; - } - - if (!ACTIONS.includes(action)) { - this.penalty(player, `did't give a valid (${action}) action`); - return true; - } - - if (this.PLAYER[player].coins > 10 && action !== 'couping') { - this.penalty(player, `had too much coins and needed to coup`); - return; - } - - let disgarded; - - switch (action) { - case 'taking-1': - this.PLAYER[player].coins++; - break; - - case 'foreign-aid': - this.PLAYER[player].coins += 2; - break; - - case 'couping': - this.PLAYER[player].coins -= 7; - try { - disgarded = this.BOTS[target].onCardLoss(this.getGameState(target)); - } catch (error) { - this.PLAYER[target].card1 = undefined; - this.PLAYER[target].card2 = undefined; - console.error(`Error in bot ${target}`); - console.error(error); - } - - if (this.PLAYER[target].card1 === disgarded && disgarded) { - this.losePlayerCard(target, disgarded); - } else if (this.PLAYER[target].card2 === disgarded && disgarded) { - this.losePlayerCard(target, disgarded); - } else { - this.penalty(target, `did't give up a valid card "${disgarded}"`); - } - break; - - case 'taking-3': - this.PLAYER[player].coins += 3; - break; - - case 'assassination': - try { - disgarded = this.BOTS[target].onCardLoss(this.getGameState(target)); - } catch (error) { - this.PLAYER[target].card1 = undefined; - this.PLAYER[target].card2 = undefined; - console.error(`Error in bot ${target}`); - console.error(error); - } - - if (this.PLAYER[target].card1 === disgarded && disgarded) { - this.losePlayerCard(target, disgarded); - } else if (this.PLAYER[target].card2 === disgarded && disgarded) { - this.losePlayerCard(target, disgarded); - } else { - this.penalty(target, `did't give up a valid card "${disgarded}"`); - } - break; - - case 'stealing': - if (this.PLAYER[target].coins < 2) { - this.PLAYER[player].coins += this.PLAYER[target].coins; - this.PLAYER[target].coins = 0; - } else { - this.PLAYER[player].coins += 2; - this.PLAYER[target].coins -= 2; - } - break; - - case 'swapping': - const newCards = [this.getCardFromDeck(), this.getCardFromDeck()]; - let chosenCards; - try { - chosenCards = this.BOTS[player].onSwappingCards({ - ...this.getGameState(player), - newCards: newCards.slice(0), - }); - } catch (error) { - this.penalty(player, `the bot crashed`); - console.error(`Error in bot ${player}`); - console.error(error); - } - - this.swapCards({ chosenCards, player, newCards }); - break; - } - } -} - -class LOOP { - constructor() { - this.DEBUG = false; - this.WINNERS = {}; - this.LOG = ''; - this.ERROR = false; - this.SCORE = {}; - this.ROUND = 0; - this.ROUNDS = this.getRounds(); - - ALLBOTS().forEach((player) => { - this.WINNERS[player] = 0; - this.SCORE[player] = 0; - }); - } - - getScore(winners, allPlayer) { - const winnerCount = winners.length; - const loserCount = allPlayer.length - winnerCount; - const loserScore = -1 / (allPlayer.length - 1); - const winnerScore = ((loserScore * loserCount) / winnerCount) * -1; - - allPlayer.forEach((player) => { - if (winners.includes(player)) { - this.SCORE[player] += winnerScore; - } else { - this.SCORE[player] += loserScore; - } - }); - - winners.forEach((player) => { - this.WINNERS[player]++; - }); - } - - displayScore(clear = false) { - if (this.ROUND % 40 === 0 || this.ROUND === this.ROUNDS) { - if (clear) process.stdout.write(`\u001b[${Object.keys(this.SCORE).length + 1}A\u001b[2K`); - - const done = String(Math.floor((this.ROUND / this.ROUNDS) * 100)); - const scoreWidth = Math.round(Math.log10(this.ROUNDS) + 8); - process.stdout.write(`\u001b[2K${done.padEnd(3)}% done\n`); - - Object.keys(this.SCORE) - .sort((a, b) => this.SCORE[b] - this.SCORE[a]) - .forEach((player) => { - const percentage = this.ROUND > 0 ? `${((this.WINNERS[player] * 100) / this.ROUND).toFixed(3)}%` : '-'; - - process.stdout.write( - `\u001b[2K${style.gray(percentage.padStart(8))} ` + - `${style.red( - String(this.SCORE[player].toFixed(2)) - .padStart(scoreWidth - 2) - .padEnd(scoreWidth) - )} ` + - `${style.yellow(player)}\n` - ); - }); - } - } - - getRounds() { - const rIdx = process.argv.indexOf('-r'); - if (rIdx > 0 && process.argv.length > rIdx && Number.parseInt(process.argv[rIdx + 1]) > 0) { - return Number.parseInt(process.argv[rIdx + 1]); - } - return 1000; - } - - play() { - let game = new COUP(); - const winners = game.play(); - this.ROUND++; - - this.displayScore(true); - - if (!winners || this.ERROR || this.LOG.includes(`STOP`)) { - console.info(this.LOG); - // console.info( JSON.stringify( game.HISTORY, null, 2 ) ); - this.ROUND = this.ROUNDS; - } - - this.getScore(winners, game.ALLPLAYER); - - this.LOG = ''; - - if (this.ROUND < this.ROUNDS) { - // We run on next tick so the GC can get to work. - // Otherwise it will work up a large memory footprint - // when running over 100,000 games - // (cause loops won't let the GC run efficiently) - process.nextTick(() => this.play()); - } else { - console.info(); - } - } - - run(debug = false) { - this.DEBUG = debug; - - console.log = (text) => { - this.LOG += `${text}\n`; - }; - console.error = (text) => { - if (this.DEBUG) { - this.ERROR = true; - this.LOG += style.red(`🛑 ${text}\n`); - } - }; - console.info(`\n${style.gray(`Game started with`)} ${style.cyan(this.ROUNDS)} ${style.gray('rounds')}`); - console.info(`\n🎉 ${style.bold('Board')} 🎉\n`); - - this.displayScore(false); - - this.play(); - } -} - -module.exports = exports = { - COUP, - LOOP, -}; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6d4a9e1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4170 @@ +//! # Coup +//! This is an engine for the popular card game [coup](http://gamegrumps.wikia.com/wiki/Coup). +//! +//! ```rust +//! use coup::{ +//! bots::{HonestBot, RandomBot, StaticBot}, +//! Coup, +//! }; +//! +//! let mut coup_game = Coup::new(vec![ +//! Box::new(StaticBot), +//! Box::new(HonestBot), +//! Box::new(RandomBot), +//! Box::new(StaticBot), +//! Box::new(RandomBot), +//! Box::new(HonestBot), +//! ]); +//! +//! // You can play a single game +//! coup_game.play(); +//! +//! // Or you can play 5 games (or more) +//! coup_game.looping(5); +//! ``` + +extern crate cfonts; + +use cfonts::{render, Colors, Options}; +use rand::{seq::SliceRandom, thread_rng}; +use std::fmt; + +pub mod bot; +pub mod bots; + +use crate::bot::{BotInterface, Context, OtherBot}; + +/// One of the five cards you get in the game of Coup. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Card { + /// - [Action::Swapping] – Draw two character cards from the deck, choose which (if any) to exchange with your cards, then return two
+ /// - [Counter::Stealing] – Block someone from stealing coins from you + Ambassador, + /// - [Action::Assassination] – Pay three coins and try to assassinate another player's character + Assassin, + /// - [Action::Stealing] – Take two coins from another player + /// - [Counter::Stealing] – Block someone from stealing coins from you + Captain, + /// - [Counter::Assassination] – Block an assassination attempt against yourself + Contessa, + /// - [Action::Tax] – Take three coins from the treasury
+ /// - [Counter::ForeignAid] – Block someone from taking foreign aid + Duke, +} + +/// Actions that can we taken with a [Card] you have. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Action { + /// Take this action with your [Card::Assassin]. + Assassination(String), + /// This standard action can be taken at any time as long as you have at least + /// 7 coin. + Coup(String), + /// This standard action can be taken at any time. + ForeignAid, + /// Take this action with your [Card::Ambassador]. + Swapping, + /// This standard action can be taken at any time. + Income, + /// Take this action with your [Card::Captain]. + Stealing(String), + /// Take this action with your [Card::Duke]. + Tax, +} + +/// Counters are played if something happens that can be countered with a +/// [Card] you have. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Counter { + /// Block an assassination with your [Card::Contessa]. + Assassination, + /// Block foreign aid with your [Card::Duke]. + ForeignAid, + /// Block stealing with your [Card::Captain] or your [Card::Ambassador]. + Stealing, +} + +enum ChallengeRound { + Action, + Counter, +} + +/// A collection on all possible moves in the game for bots to analyze. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum History { + /// A bot played an Assassin to assassinate another bot for 3 coins. + ActionAssassination { by: String, target: String }, + /// A bot played to coup another bot with 10 coins. + ActionCoup { by: String, target: String }, + /// A bot takes 2 coins from the treasury. + ActionForeignAid { by: String }, + /// A bot played an Ambassador. + ActionSwapping { by: String }, + /// A bot took 1 coin of income from the treasury. + ActionIncome { by: String }, + /// A bot played a Captain to steal 2 coins from another bot. + ActionStealing { by: String, target: String }, + /// A bot played a Duke to take 3 coins of tax from the treasury. + ActionTax { by: String }, + + /// A bot challenged another bot for having the Assassin. + ChallengeAssassin { by: String, target: String }, + /// A bot challenged another bot for having the Ambassador. + ChallengeAmbassador { by: String, target: String }, + /// A bot challenged another bot for having the Captain. + ChallengeCaptain { by: String, target: String }, + /// A bot challenged another bot for having the Duke. + ChallengeDuke { by: String, target: String }, + + /// Another bot was trying to assassinated so this bot played the Contessa to counter. + CounterAssassination { by: String, target: String }, + /// Another bot was trying to take foreign aid from the treasury so this bot played the Duke to counter. + CounterForeignAid { by: String, target: String }, + /// Another bot was trying to stealing from this bot so it played the Captain or Ambassador to counter. + CounterStealing { by: String, target: String }, + + /// Another bot countered with the Contessa and this bot challenged it for having that card. + CounterChallengeContessa { by: String, target: String }, + /// Another bot countered with the Duke and this bot challenged it for having that card. + CounterChallengeDuke { by: String, target: String }, + /// Another bot countered with the Captain or Ambassador and this bot challenged it for having that card. + CounterChallengeCaptainAmbassedor { by: String, target: String }, +} + +/// The score of the game for all bots. +pub type Score = Vec<(String, f64)>; + +struct Bot { + name: String, + coins: u8, + cards: Vec, + interface: Box, +} + +impl fmt::Debug for Bot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + writeln!(f, "Bot {{")?; + writeln!(f, " name: {:?}", self.name)?; + writeln!(f, " coins: {:?}", self.coins)?; + writeln!(f, " cards: {:?}", self.cards)?; + write!(f, "}}") + } else { + write!( + f, + "Bot {{ name: {:?}, coins: {:?}, cards: {:?} }}", + self.name, self.coins, self.cards + ) + } + } +} + +impl fmt::Display for Bot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "\x1b[33m[\x1b[1m{}\x1b[0m \x1b[31m{}{}\x1b[33m 💰{}]\x1b[39m", + self.name, + "♥".repeat(self.cards.len()), + "♡".repeat(2 - self.cards.len()), + self.coins + ) + } +} + +/// The Coup game engine. +pub struct Coup { + bots: Vec, + playing_bots: Vec, + deck: Vec, + discard_pile: Vec, + history: Vec, + score: Score, + turn: usize, + moves: usize, + log: bool, + rounds: u64, + round: u64, +} + +impl Coup { + /// Start a new Coup game by passing in all your bots in a Vec. + pub fn new(user_bots: Vec>) -> Self { + let mut bots: Vec = Vec::new(); + let mut existing_names: Vec = Vec::new(); + let mut score: Vec<(String, f64)> = Vec::new(); + + for bot in user_bots.into_iter() { + let base_name = bot.get_name(); + + // Generating a unique name for the bot + let mut unique_name = base_name.clone(); + let mut suffix = 2; + while existing_names.contains(&unique_name) { + unique_name = format!("{} {}", base_name, suffix); + suffix += 1; + } + + existing_names.push(unique_name.clone()); + + let bot = Bot { + name: unique_name.clone(), + coins: 2, + cards: Vec::new(), + interface: bot, + }; + + bots.push(bot); + score.push((unique_name, 0.0)); + } + + Self { + bots, + playing_bots: vec![], + deck: vec![], + discard_pile: vec![], + history: vec![], + score, + turn: 0, + moves: 0, + log: true, + round: 0, + rounds: 0, + } + } + + /// A public method to get a new deck. + /// This can be used by bots to make sure you get the same amount of cards as + /// the engine does. + pub fn new_deck() -> Vec { + let mut deck = vec![ + Card::Ambassador, + Card::Ambassador, + Card::Ambassador, + Card::Assassin, + Card::Assassin, + Card::Assassin, + Card::Captain, + Card::Captain, + Card::Captain, + Card::Contessa, + Card::Contessa, + Card::Contessa, + Card::Duke, + Card::Duke, + Card::Duke, + ]; + deck.shuffle(&mut thread_rng()); + deck + } + + fn setup(&mut self) { + // A fresh deck + let mut deck = Coup::new_deck(); + + // Put the index of all bots into play so we can shuffle them later + self.playing_bots.clear(); + for index in 0..self.bots.len() { + self.playing_bots.push(index); + } + + // Shuffle all bots each round and limit them to the max players per game + self.playing_bots.shuffle(&mut thread_rng()); + self.playing_bots.truncate(6); + + // Give all playing bots cards and coins + for bot in self.playing_bots.iter() { + let new_cards = vec![deck.pop().unwrap(), deck.pop().unwrap()]; + self.bots[*bot].cards = new_cards; + self.bots[*bot].coins = 2; + } + self.deck = deck; + + self.discard_pile = vec![]; + self.history = vec![]; + self.turn = 0; + self.moves = 0; + } + + fn log(message: std::fmt::Arguments, logging: bool) { + if logging { + println!(" {:?}", message); + } + } + + fn get_bot_by_name(&self, name: String) -> &Bot { + self.bots.iter().find(|bot| bot.name == name).unwrap() + } + + fn get_other_bots(&self) -> Vec { + self + .playing_bots + .iter() + .map(|bot_index| { + let bot = &self.bots[*bot_index]; + OtherBot { + name: bot.name.clone(), + coins: bot.coins, + cards: bot.cards.len() as u8, + } + }) + .filter(|bot| bot.cards != 0) + .collect() + } + + fn get_context(&self, name: String) -> Context { + Context { + name: name.clone(), + coins: self.get_bot_by_name(name.clone()).coins, + cards: self.get_bot_by_name(name.clone()).cards.clone(), + playing_bots: self.get_other_bots(), + discard_pile: self.discard_pile.clone(), + history: self.history.clone(), + score: self.score.clone(), + } + } + + fn card_loss(&mut self, name: String) { + if self.get_bot_by_name(name.clone()).cards.is_empty() { + // This bot is already dead + return; + } + let context = self.get_context(name.clone()); + self.bots.iter_mut().enumerate().for_each(|(index, bot)| { + let context = Context { + coins: bot.coins, + cards: bot.cards.clone(), + ..context.clone() + }; + if !self.playing_bots.contains(&index) { + } else if bot.name == name { + let lost_card = bot.interface.on_card_loss(&context); + if !bot.cards.contains(&lost_card) { + Self::log(format_args!("🚨 {} is being penalized because \x1b[33mit discarded a card({:?}) it didn't have\x1b[39m", bot, lost_card), self.log); + + let card = bot.cards.pop().unwrap(); + let mut lost_cards = format!("{:?}", card); + self.discard_pile.push(card); + + if !bot.cards.is_empty() { + let card = bot.cards.pop().unwrap(); + lost_cards = + format!("{} and {:?}", lost_cards, card); + self.discard_pile.push(card); + } + + bot.cards = vec![]; + Self::log(format_args!("☠️ {} has lost the \x1b[33m{:?}\x1b[39m", bot, lost_cards), self.log); + } else { + if let Some(index) = bot.cards.iter().position(|&c| c == lost_card) { + bot.cards.remove(index); + } + self.discard_pile.push(lost_card); + + Self::log(format_args!( + "{} {} has lost the \x1b[33m{:?}\x1b[39m", + if bot.cards.is_empty() { + "☠️ " + } else { + "💔" + }, + bot, + lost_card + ), self.log); + } + } + }); + } + + fn penalize_bot(&mut self, name: String, reason: &str) { + Self::log( + format_args!( + "🚨 {} is being penalized because \x1b[33m{}\x1b[39m", + self.get_bot_by_name(name.clone()), + reason + ), + self.log, + ); + self.card_loss(name); + } + + fn target_not_found(&self, target: String) -> bool { + self.bots.iter().filter(|bot| bot.name == target).count() != 1 + } + + fn set_score(&mut self, winners: Vec) { + let winner_count = winners.len() as f64; + let loser_count = if self.bots.len() > 6 { + 6.0 + } else { + self.bots.len() as f64 + } - winner_count; + let loser_score = -1.0 / loser_count; + let winner_score = -((loser_score * loser_count) / winner_count); + + self.score = self + .score + .iter() + .map(|(name, score)| { + if winners.contains(name) { + (name.clone(), score + winner_score) + } else { + (name.clone(), score + loser_score) + } + }) + .collect::(); + } + + // We take a card from a bot and replace it with a new one from the deck + fn swap_card(&mut self, card: Card, swopee: String) { + Self::log( + format_args!( + "🔄 {} is swapping its card for a new card from the deck", + self.get_bot_by_name(swopee.clone()) + ), + self.log, + ); + for bot in self.bots.iter_mut() { + if bot.name == swopee.clone() { + if let Some(index) = bot.cards.iter().position(|&c| c == card) { + bot.cards.remove(index); + } + self.deck.push(card); + self.deck.shuffle(&mut thread_rng()); + + let mut new_cards = bot.cards.clone(); + new_cards.push(self.deck.pop().unwrap()); + bot.cards = new_cards; + } + } + } + + /// Playing a game which means we setup the table, give each bots their cards + /// and coins and start the game loop. + pub fn play(&mut self) { + self.setup(); + + // Logo + let output = render(Options { + text: String::from("Coup"), + colors: vec![Colors::White, Colors::Yellow], + spaceless: true, + ..Options::default() + }); + Self::log( + format_args!( + "\n\n{}\x1b[4Dv{}\n\n", + output.text, + env!("CARGO_PKG_VERSION") + ), + self.log, + ); + + let bots = self + .playing_bots + .iter() + .map(|bot_index| format!("{}", self.bots[*bot_index])) + .collect::>(); + Self::log( + format_args!("🤺 This rounds player:\n {}\n", bots.join("\n "),), + self.log, + ); + + // Let's play + while self.playing_bots.len() > 1 { + self.game_loop(); + + if self.moves >= 1000 { + break; + } + } + + let winners = self + .playing_bots + .iter() + .map(|bot_index| self.bots[*bot_index].name.clone()) + .collect::>(); + + self.set_score(winners.clone()); + + Self::log( + format_args!( + "\n 🎉🎉🎉 The winner{} \x1b[1m{}\x1b[0m \x1b[90min {} moves\x1b[39m\n", + if winners.len() > 1 { "s are" } else { " is" }, + winners.join(" and "), + self.moves + ), + self.log, + ); + } + + fn game_loop(&mut self) { + self.moves += 1; + + let context = + self.get_context(self.bots[self.playing_bots[self.turn]].name.clone()); + + // If you have 10 or more coins you must coup + let action = if self.bots[self.playing_bots[self.turn]].coins >= 10 { + let target = self.bots[self.playing_bots[self.turn]] + .interface + .on_auto_coup(&context); + Action::Coup(target) + } else { + self.bots[self.playing_bots[self.turn]].interface.on_turn(&context) + }; + + match action { + Action::Assassination(target_name) => { + if self.target_not_found(target_name.clone()) { + self.penalize_bot( + context.name.clone(), + "it tried to assassinate an unknown bot", + ); + } else { + self.history.push(History::ActionAssassination { + by: context.name.clone(), + target: target_name.clone(), + }); + Self::log( + format_args!( + "🃏 {} assassinates {} with the \x1b[33mAssassin\x1b[39m", + self.bots[self.playing_bots[self.turn]], + self.get_bot_by_name(target_name.clone()) + ), + self.log, + ); + self.challenge_and_counter_round( + Action::Assassination(target_name.clone()), + target_name, + ); + } + }, + Action::Coup(target_name) => { + if self.target_not_found(target_name.clone()) { + self.penalize_bot( + context.name.clone(), + "it tried to coup an unknown bot", + ); + } else { + self.history.push(History::ActionCoup { + by: context.name.clone(), + target: target_name.clone(), + }); + Self::log( + format_args!( + "🃏 {} \x1b[33mcoups\x1b[39m {}", + self.bots[self.playing_bots[self.turn]], + self.get_bot_by_name(target_name.clone()) + ), + self.log, + ); + self.action_couping(target_name.clone()); + } + }, + Action::ForeignAid => { + self.history.push(History::ActionForeignAid { + by: context.name.clone(), + }); + Self::log( + format_args!( + "🃏 {} takes \x1b[33mforeign aid\x1b[39m", + self.bots[self.playing_bots[self.turn]], + ), + self.log, + ); + self.counter_round_only(); + }, + Action::Swapping => { + self.history.push(History::ActionSwapping { + by: context.name.clone(), + }); + Self::log( + format_args!( + "🃏 {} swaps cards with \x1b[33mthe Ambassador\x1b[39m", + self.bots[self.playing_bots[self.turn]] + ), + self.log, + ); + self.challenge_round_only(Action::Swapping); + }, + Action::Income => { + self.history.push(History::ActionIncome { + by: context.name.clone(), + }); + Self::log( + format_args!( + "🃏 {} takes \x1b[33ma coin\x1b[39m", + self.bots[self.playing_bots[self.turn]] + ), + self.log, + ); + self.action_income(); + }, + Action::Stealing(target_name) => { + if self.target_not_found(target_name.clone()) { + self.penalize_bot( + context.name.clone(), + "it tried to steal from an unknown bot", + ); + } else { + self.history.push(History::ActionStealing { + by: context.name.clone(), + target: target_name.clone(), + }); + Self::log( + format_args!( + "🃏 {} \x1b[33msteals 2 coins\x1b[39m from {}", + self.bots[self.playing_bots[self.turn]], + self.get_bot_by_name(target_name.clone()), + ), + self.log, + ); + self.challenge_and_counter_round( + Action::Stealing(target_name.clone()), + target_name, + ); + } + }, + Action::Tax => { + self.history.push(History::ActionTax { + by: context.name.clone(), + }); + Self::log( + format_args!( + "🃏 {} takes tax with the \x1b[33mDuke\x1b[39m", + self.bots[self.playing_bots[self.turn]], + ), + self.log, + ); + self.challenge_round_only(Action::Tax); + }, + } + + // Let's filter out all dead bots + self.playing_bots = self + .playing_bots + .iter() + .filter(|bot_index| !self.bots[**bot_index].cards.is_empty()) + .copied() + .collect::>(); + + // We move to the next turn + self.turn = if self.playing_bots.is_empty() + || self.turn >= self.playing_bots.len() - 1 + { + 0 + } else { + self.turn + 1 + }; + } + + fn challenge_and_counter_round( + &mut self, + action: Action, + target_name: String, + ) { + // THE CHALLENGE ROUND + let playing_bot_name = self.bots[self.playing_bots[self.turn]].name.clone(); + // On Action::Assassination and Action::Stealing + // Does anyone want to challenge this action? + if let Some(challenger) = self.challenge_round( + ChallengeRound::Action, + &action, + playing_bot_name.clone(), + ) { + // The bot "challenger" is challenging this action + let success = self.resolve_challenge( + action.clone(), + playing_bot_name.clone(), + challenger.clone(), + ); + if !success { + // The challenge was unsuccessful + // Discard the card and pick up a new card from the deck + let discard_card = match action { + Action::Assassination(_) => Card::Assassin, + Action::Stealing(_) => Card::Captain, + Action::Coup(_) + | Action::ForeignAid + | Action::Swapping + | Action::Income + | Action::Tax => { + unreachable!("Challenge and counter not called on other actions") + }, + }; + self.swap_card(discard_card, playing_bot_name.clone()); + } else { + // The challenge was successful so we stop a counter round + return; + } + } + + // At this point it's possible this bot is dead already and can't + // play any counters. + // Scenario: + // - Bot1(1 card) gets assassinated by Bot2 + // - Bot1(1 card) challenges this assassination unsuccessfully + // - Bot1(0 card) is now dead and can't counter + if !self.get_bot_by_name(target_name.clone()).cards.is_empty() { + // THE COUNTER CHALLENGE ROUND + // Does the target want to counter this action? + let counter = + self.get_bot_by_name(target_name.clone()).interface.on_counter( + &action, + playing_bot_name.clone(), + &self.get_context(target_name.clone()), + ); + + if counter { + // The bot target_name is countering the action so we now ask the + // table if anyone would like to challenge this counter + match action { + Action::Assassination(_) => { + self.history.push(History::CounterAssassination { + by: target_name.clone(), + target: playing_bot_name.clone(), + }) + }, + Action::Stealing(_) => self.history.push(History::CounterStealing { + by: target_name.clone(), + target: playing_bot_name.clone(), + }), + Action::Coup(_) + | Action::ForeignAid + | Action::Swapping + | Action::Income + | Action::Tax => { + unreachable!("Challenge and counter not called on other actions") + }, + }; + Self::log( + format_args!( + "🛑 {} was countered by {}", + self.get_bot_by_name(playing_bot_name.clone()), + self.get_bot_by_name(target_name.clone()), + ), + self.log, + ); + + if let Some(counter_challenge) = self.challenge_round( + ChallengeRound::Counter, + &action, + target_name.clone(), + ) { + let counter_card = match action { + Action::Assassination(_) => Counter::Assassination, + Action::Stealing(_) => Counter::Stealing, + Action::Coup(_) + | Action::ForeignAid + | Action::Swapping + | Action::Income + | Action::Tax => { + unreachable!("Challenge and counter not called on other actions") + }, + }; + // The bot counter_challenge.by is challenging this action + let success = self.resolve_counter_challenge( + counter_card, + target_name.clone(), + counter_challenge.clone(), + ); + if success { + // The challenge was successful so the player who countered gets a + // penalty but the action is still performed + match action { + Action::Assassination(_) => { + self.action_assassination(target_name.clone()) + }, + Action::Stealing(_) => self.action_stealing(target_name.clone()), + Action::Coup(_) + | Action::ForeignAid + | Action::Swapping + | Action::Income + | Action::Tax => unreachable!( + "Challenge and counter not called on other actions" + ), + } + } + } else { + // There was no challenge to the counter played so the action is + // not performed (because it is countered). + } + } else { + // No counter was played so the action is performed + match action { + Action::Assassination(_) => { + self.action_assassination(target_name.clone()) + }, + Action::Stealing(_) => self.action_stealing(target_name.clone()), + Action::Coup(_) + | Action::ForeignAid + | Action::Swapping + | Action::Income + | Action::Tax => { + unreachable!("Challenge and counter not called on other actions") + }, + } + } + } + } + + fn challenge_round_only(&mut self, action: Action) { + // THE CHALLENGE ROUND + let playing_bot_name = self.bots[self.playing_bots[self.turn]].name.clone(); + // On Action::Swapping and Action::Tax + // Does anyone want to challenge this action? + if let Some(challenger) = self.challenge_round( + ChallengeRound::Action, + &action, + playing_bot_name.clone(), + ) { + // The bot "challenger" is challenging this action + let success = self.resolve_challenge( + action.clone(), + playing_bot_name.clone(), + challenger.clone(), + ); + if !success { + // The challenge was unsuccessful + // Discard the card and pick up a new card from the deck + let discard_card = match action { + Action::Swapping => Card::Ambassador, + Action::Tax => Card::Duke, + Action::Coup(_) + | Action::Assassination(_) + | Action::ForeignAid + | Action::Income + | Action::Stealing(_) => { + unreachable!("Challenge only not called on other actions") + }, + }; + self.swap_card(discard_card, playing_bot_name.clone()); + + // The challenge was unsuccessful so let's do the thing + match action { + Action::Swapping => self.action_swapping(), + Action::Tax => self.action_tax(), + Action::Coup(_) + | Action::Assassination(_) + | Action::ForeignAid + | Action::Income + | Action::Stealing(_) => { + unreachable!("Challenge only not called on other actions") + }, + } + } + } else { + // No challenge was played so the action is performed + match action { + Action::Swapping => self.action_swapping(), + Action::Tax => self.action_tax(), + Action::Coup(_) + | Action::Assassination(_) + | Action::ForeignAid + | Action::Income + | Action::Stealing(_) => { + unreachable!("Challenge only not called on other actions") + }, + } + } + } + + fn counter_round_only(&mut self) { + let playing_bot_name = self.bots[self.playing_bots[self.turn]].name.clone(); + // THE COUNTER CHALLENGE ROUND + // On Action::ForeignAid + // Does anyone want to counter this action? + let mut counterer_name = String::new(); + for bot_index in self.playing_bots.iter() { + let bot = &self.bots[*bot_index]; + // Skipping the challenger + if bot.name.clone() == playing_bot_name.clone() { + continue; + } + + let countering = bot.interface.on_counter( + &Action::ForeignAid, + playing_bot_name.clone(), + &self.get_context(playing_bot_name.clone()), + ); + + if countering { + counterer_name = bot.name.clone(); + break; + } + } + + if !counterer_name.is_empty() { + self.history.push(History::CounterForeignAid { + by: counterer_name.clone(), + target: playing_bot_name.clone(), + }); + Self::log( + format_args!( + "🛑 {} was countered by {}", + self.get_bot_by_name(playing_bot_name.clone()), + self.get_bot_by_name(counterer_name.clone()), + ), + self.log, + ); + + // The bot counterer_name is countering the action so we now ask the table + // if anyone would like to challenge this counter + if let Some(counter_challenge) = self.challenge_round( + ChallengeRound::Counter, + &Action::ForeignAid, + counterer_name.clone(), + ) { + // The bot counter_challenge.by is challenging this action + let success = self.resolve_counter_challenge( + Counter::ForeignAid, + counterer_name.clone(), + counter_challenge.clone(), + ); + if success { + self.action_foraign_aid(); + } + } + } else { + // No counter was played so the action is performed + self.action_foraign_aid(); + } + } + + // All bots (minus the playing bot) are asked if they want to challenge a play + fn challenge_round( + &mut self, + challenge_type: ChallengeRound, + action: &Action, + by: String, + ) -> Option { + for bot_index in self.playing_bots.iter() { + let bot = &self.bots[*bot_index]; + // skipping the challenger + if bot.name.clone() == by.clone() { + continue; + } + + let context = self.get_context(bot.name.clone()); + + let challenging = match challenge_type { + ChallengeRound::Action => { + bot.interface.on_challenge_action_round(action, by.clone(), &context) + }, + ChallengeRound::Counter => { + bot.interface.on_challenge_counter_round(action, by.clone(), &context) + }, + }; + + if challenging { + Self::log( + format_args!( + "❓ {} was challenged by {}", + self.get_bot_by_name(by), + bot + ), + self.log, + ); + return Some(bot.name.clone()); + } + } + None + } + + // Someone challenged another bot for playing a card they believe is a bluff + fn resolve_challenge( + &mut self, + action: Action, + player: String, + challenger: String, + ) -> bool { + self.history.push(match action { + Action::Assassination(_) => History::ChallengeAssassin { + by: challenger.clone(), + target: player.clone(), + }, + Action::Swapping => History::ChallengeAmbassador { + by: challenger.clone(), + target: player.clone(), + }, + Action::Stealing(_) => History::ChallengeCaptain { + by: challenger.clone(), + target: player.clone(), + }, + Action::Tax => History::ChallengeDuke { + by: challenger.clone(), + target: player.clone(), + }, + Action::Coup(_) | Action::Income | Action::ForeignAid => { + unreachable!("Can't challenge Coup, Income or ForeignAid") + }, + }); + + let player = self.get_bot_by_name(player.clone()); + let challenger = self.get_bot_by_name(challenger.clone()); + + let card = match action { + Action::Assassination(_) => Card::Assassin, + Action::Swapping => Card::Ambassador, + Action::Stealing(_) => Card::Captain, + Action::Tax => Card::Duke, + Action::Coup(_) | Action::Income | Action::ForeignAid => { + unreachable!("Can't challenge Coup, Income or ForeignAid") + }, + }; + + if player.cards.contains(&card) { + Self::log( + format_args!( + "👎 The challenge was unsuccessful because {} \x1b[33mdid have the {:?}\x1b[39m", + player, card + ), + self.log, + ); + self.card_loss(challenger.name.clone()); + false + } else { + Self::log( + format_args!( + "👍 The challenge was successful because {} \x1b[33mdidn't have the {:?}\x1b[39m", + player, card + ), + self.log, + ); + self.card_loss(player.name.clone()); + true + } + } + + // A bot is countering another bots action against them + fn resolve_counter_challenge( + &mut self, + counter: Counter, + counterer: String, + challenger: String, + ) -> bool { + self.history.push(match counter { + Counter::Assassination => History::CounterChallengeContessa { + by: challenger.clone(), + target: counterer.clone(), + }, + Counter::ForeignAid => History::CounterChallengeDuke { + by: challenger.clone(), + target: counterer.clone(), + }, + Counter::Stealing => History::CounterChallengeCaptainAmbassedor { + by: challenger.clone(), + target: counterer.clone(), + }, + }); + + let counterer = self.get_bot_by_name(counterer.clone()); + let challenger = self.get_bot_by_name(challenger.clone()); + + let cards = match counter { + Counter::Assassination => vec![Card::Contessa], + Counter::ForeignAid => vec![Card::Duke], + Counter::Stealing => vec![Card::Captain, Card::Ambassador], + }; + let card_string = cards + .iter() + .map(|card| format!("{:?}", card)) + .collect::>() + .join(" or the "); + + if cards.iter().any(|&card| counterer.cards.contains(&card)) { + Self::log( + format_args!( + "👎 The counter was unsuccessful because {} \x1b[33mdid have the {}\x1b[39m", + counterer, card_string + ), + self.log, + ); + self.card_loss(challenger.name.clone()); + false + } else { + Self::log( + format_args!( + "👍 The counter was successful because {} \x1b[33mdidn't have the {}\x1b[39m", + counterer, card_string + ), + self.log, + ); + self.card_loss(counterer.name.clone()); + true + } + } + + fn display_score(&mut self) { + let fps = (self.rounds as f64 / 1000.0).max(1.0) as u64; + if self.round == 0 || self.round % fps == 0 || self.round + 1 == self.rounds + { + if self.round > 0 { + print!("\x1b[{}A\x1b[2K", self.score.len() + 1); + } + + let done = + (((self.round + 1) as f64 / self.rounds as f64) * 100.0).round(); + println!("\x1b[2K {:>3}% done", done); + self.score.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap()); + self.score.iter().for_each(|(name, score)| { + let percentage = if self.round > 0 { + format!("{:.3}", (score * 100.0) / self.round as f64) + } else { String::from("0") }; + println!("\x1b[2K\x1b[90m {:>8}%\x1b[39m \x1b[31m{:>15.5}\x1b[39m \x1b[33m{}\x1b[39m", percentage, score, name); + }); + } + } + + fn format_number_with_separator(mut number: u64) -> String { + if number == 0 { + return String::from("0"); + } + let mut result = String::new(); + let mut count = 0; + + while number != 0 { + if count % 3 == 0 && count != 0 { + result.insert(0, ','); + } + count += 1; + result.insert(0, (b'0' + (number % 10) as u8) as char); + number /= 10; + } + + result + } + + /// Play n number of rounds and tally up the score in the CLI. + pub fn looping(&mut self, rounds: u64) { + self.setup(); + self.log = false; + self.rounds = rounds; + + // Logo + let output = render(Options { + text: String::from("Coup"), + colors: vec![Colors::White, Colors::Yellow], + spaceless: true, + ..Options::default() + }); + println!("\n\n{}\x1b[4Dv{}\n\n", output.text, env!("CARGO_PKG_VERSION")); + + println!( + " Starting \x1b[36m{}\x1b[39m rounds", + Self::format_number_with_separator(rounds) + ); + + println!(" ╔═════════════════╗\n ║ 🎲🎲 \x1b[1mBOARD\x1b[0m 🎲🎲 ║\n ╚═════════════════╝\x1b[?25l"); + self.display_score(); + for round in 0..rounds { + self.setup(); + self.play(); + // TODO: detect "stop" and record log in debug mode + self.round = round + 1; + self.display_score(); + } + + println!( + "\x1b[?25h\n 🎉🎉🎉 The winner is: \x1b[1m{}\x1b[0m\n", + self + .score + .iter() + .max_by(|(_, a), (_, b)| a + .partial_cmp(b) + .unwrap_or(std::cmp::Ordering::Equal)) + .unwrap() + .0 + ); + } + + // *******************************| Actions |****************************** // + fn action_assassination(&mut self, target: String) { + let playing_bot_coins = self.bots[self.playing_bots[self.turn]].coins; + let playing_bot_name = self.bots[self.playing_bots[self.turn]].name.clone(); + if playing_bot_coins < 3 { + self.penalize_bot( + playing_bot_name.clone(), + "it tried to assassinate someone with insufficient funds", + ); + } else if self.target_not_found(target.clone()) { + self.penalize_bot( + playing_bot_name.clone(), + "it tried to assassinate an unknown bot", + ); + } else { + // Paying the fee + self.bots[self.playing_bots[self.turn]].coins = playing_bot_coins - 3; + + // Taking a card from the target bot + self.card_loss(target); + } + } + + fn action_couping(&mut self, target: String) { + let playing_bot_coins = self.bots[self.playing_bots[self.turn]].coins; + let playing_bot_name = self.bots[self.playing_bots[self.turn]].name.clone(); + if playing_bot_coins < 7 { + self.penalize_bot( + playing_bot_name.clone(), + "it tried to coup someone with insufficient funds", + ); + } else if self.target_not_found(target.clone()) { + self.penalize_bot( + playing_bot_name.clone(), + "it tried to coup an unknown bot", + ); + } else { + // Paying the fee + self.bots[self.playing_bots[self.turn]].coins = playing_bot_coins - 7; + + // Taking a card from the target bot + self.card_loss(target); + } + } + + fn action_foraign_aid(&mut self) { + let coins = self.bots[self.playing_bots[self.turn]].coins; + self.bots[self.playing_bots[self.turn]].coins = coins + 2; + } + + fn action_swapping(&mut self) { + let mut all_available_cards = + self.bots[self.playing_bots[self.turn]].cards.clone(); + let card1 = self.deck.pop().unwrap(); + let card2 = self.deck.pop().unwrap(); + let cards_from_deck = [card1, card2]; + let swapped_cards = + self.bots[self.playing_bots[self.turn]].interface.on_swapping_cards( + cards_from_deck, + &self.get_context(self.bots[self.playing_bots[self.turn]].name.clone()), + ); + all_available_cards.push(card1); + all_available_cards.push(card2); + + if !(all_available_cards.contains(&swapped_cards[0]) + && all_available_cards.contains(&swapped_cards[1])) + { + self.penalize_bot( + self.bots[self.playing_bots[self.turn]].name.clone(), + "it tried to swap cards it didn't have", + ); + } else { + self.deck.push(swapped_cards[0]); + self.deck.push(swapped_cards[1]); + self.deck.shuffle(&mut thread_rng()); + + // removing the discarded cards from the pool and giving it to the bot + if let Some(index) = + all_available_cards.iter().position(|&c| c == swapped_cards[0]) + { + all_available_cards.remove(index); + } + if let Some(index) = + all_available_cards.iter().position(|&c| c == swapped_cards[1]) + { + all_available_cards.remove(index); + } + self.bots[self.playing_bots[self.turn]].cards = all_available_cards; + } + } + + fn action_income(&mut self) { + let playing_bot_coins = self.bots[self.playing_bots[self.turn]].coins; + self.bots[self.playing_bots[self.turn]].coins = playing_bot_coins + 1; + } + + fn action_stealing(&mut self, target: String) { + let coins = self.bots[self.playing_bots[self.turn]].coins; + let target_coins = self.get_bot_by_name(target.clone()).coins; + let booty = std::cmp::min(target_coins, 2); + self.bots[self.playing_bots[self.turn]].coins = coins + booty; + self + .bots + .iter_mut() + .find(|bot| bot.name.clone() == target) + .unwrap() + .coins = target_coins - booty; + } + + fn action_tax(&mut self) { + let coins = self.bots[self.playing_bots[self.turn]].coins; + self.bots[self.playing_bots[self.turn]].coins = coins + 3; + } +} + +/// The debug trait has been implemented to support both format and alternate +/// format which means you can print a game instance with: +/// ```rust +/// # use coup::Coup; +/// let mut my_coup = Coup::new(vec![]); +/// println!("{:?}", my_coup); +/// ``` +/// and +/// ```rust +/// # use coup::Coup; +/// let mut my_coup = Coup::new(vec![]); +/// println!("{:#?}", my_coup); +/// ``` +impl fmt::Debug for Coup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + writeln!(f, "Coup {{")?; + writeln!(f, " bots: {:#?}", self.bots)?; + writeln!(f, " playing_bots: {:#?}", self.playing_bots)?; + writeln!(f, " deck: {:#?}", self.deck)?; + writeln!(f, " discard_pile: {:#?}", self.discard_pile)?; + writeln!(f, " history: {:#?}", self.history)?; + writeln!(f, " score: {:#?}", self.score)?; + write!(f, "}}") + } else { + write!( + f, + "Coup {{ bots: {:?}, playing_bots: {:?}, deck: {:?}, discard_pile: {:?}, history: {:?}, score: {:?} }}", + self.bots, self.playing_bots, self.deck, self.discard_pile, self.history, self.score + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bots::StaticBot; + + #[test] + fn test_new() { + let coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + + assert_eq!(coup.bots[0].cards, vec![]); + assert_eq!(coup.bots[1].cards, vec![]); + assert_eq!(coup.playing_bots, Vec::::new()); + assert_eq!(coup.deck, vec![]); + assert_eq!(coup.discard_pile, vec![]); + assert_eq!(coup.history, vec![]); + assert_eq!( + coup.score, + vec![ + (String::from("StaticBot"), 0.0), + (String::from("StaticBot 2"), 0.0) + ] + ); + assert_eq!(coup.turn, 0); + assert_eq!(coup.moves, 0); + } + + #[test] + fn test_new_deck() { + let deck = Coup::new_deck(); + assert_eq!(deck.len(), 15); + assert_eq!( + deck.iter().filter(|&card| card == &Card::Ambassador).count(), + 3 + ); + assert_eq!(deck.iter().filter(|&card| card == &Card::Assassin).count(), 3); + assert_eq!(deck.iter().filter(|&card| card == &Card::Captain).count(), 3); + assert_eq!(deck.iter().filter(|&card| card == &Card::Contessa).count(), 3); + assert_eq!(deck.iter().filter(|&card| card == &Card::Duke).count(), 3); + } + + #[test] + fn test_setup() { + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + + assert_eq!(coup.bots[coup.playing_bots[0]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[0]].coins, 2); + assert_eq!(coup.bots[coup.playing_bots[1]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[1]].coins, 2); + assert_eq!(coup.bots[coup.playing_bots[2]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[2]].coins, 2); + assert_eq!(coup.bots[coup.playing_bots[3]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[3]].coins, 2); + assert_eq!(coup.bots[coup.playing_bots[4]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[4]].coins, 2); + assert_eq!(coup.bots[coup.playing_bots[5]].cards.len(), 2); + assert_eq!(coup.bots[coup.playing_bots[5]].coins, 2); + assert_eq!(coup.playing_bots.len(), 6); + assert_eq!(coup.deck.len(), 3); + assert_eq!(coup.discard_pile, vec![]); + assert_eq!(coup.turn, 0); + assert_eq!(coup.moves, 0); + } + + // TODO: test_log + + #[test] + fn test_get_bot_by_name() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + assert_eq!( + coup.get_bot_by_name(String::from("StaticBot")).name, + String::from("StaticBot") + ); + assert_eq!( + coup.get_bot_by_name(String::from("StaticBot 2")).name, + String::from("StaticBot 2") + ); + } + + #[test] + fn test_get_other_bots() { + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 3"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 4"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 5"), + coins: 2, + cards: 2 + })); + + coup.playing_bots = vec![4, 3, 2, 1, 0]; + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 3"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 4"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 5"), + coins: 2, + cards: 2 + })); + + coup.turn = 2; + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 3"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 4"), + coins: 2, + cards: 2 + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 5"), + coins: 2, + cards: 2 + })); + + coup.bots[0].cards = vec![]; + coup.bots[1].cards = vec![Card::Duke]; + coup.bots[3].cards = vec![]; + coup.playing_bots = vec![1, 2, 4]; + coup.turn = 2; + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 1, + })); + assert!(coup.get_other_bots().contains(&OtherBot { + name: String::from("StaticBot 3"), + coins: 2, + cards: 2, + })); + } + + #[test] + fn test_get_context() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Captain, Card::Captain]; + coup.playing_bots = vec![0, 1]; + + assert_eq!( + coup.get_context(String::from("StaticBot")), + Context { + name: String::from("StaticBot"), + coins: 2, + cards: vec![Card::Ambassador, Card::Duke], + playing_bots: vec![ + OtherBot { + name: String::from("StaticBot"), + coins: 2, + cards: 2 + }, + OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 2 + } + ], + discard_pile: vec![], + history: vec![], + score: vec![ + (String::from("StaticBot"), 0.0), + (String::from("StaticBot 2"), 0.0) + ], + } + ); + + coup.turn = 1; + assert_eq!( + coup.get_context(String::from("StaticBot 2")), + Context { + name: String::from("StaticBot 2"), + coins: 2, + cards: vec![Card::Captain, Card::Captain], + playing_bots: vec![ + OtherBot { + name: String::from("StaticBot"), + coins: 2, + cards: 2 + }, + OtherBot { + name: String::from("StaticBot 2"), + coins: 2, + cards: 2 + } + ], + discard_pile: vec![], + history: vec![], + score: vec![ + (String::from("StaticBot"), 0.0), + (String::from("StaticBot 2"), 0.0) + ], + } + ); + } + + #[test] + fn test_card_loss() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Captain, Card::Captain]; + + coup.card_loss(String::from("StaticBot 2")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Captain]); + } + + #[test] + fn test_card_loss_faulty_bot() { + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_card_loss(&self, _context: &Context) -> Card { + Card::Duke + } + } + + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(TestBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + + coup.card_loss(String::from("TestBot")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![]); + assert_eq!(coup.discard_pile, vec![Card::Captain, Card::Assassin]); + } + + // TODO: test_penalize_bot + + #[test] + fn test_target_not_found() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + assert_eq!(coup.target_not_found(String::from("StaticBot")), false); + assert_eq!(coup.target_not_found(String::from("StaticBot 3")), true); + assert_eq!(coup.target_not_found(String::from("StaticBot 2")), false); + } + + #[test] + fn test_set_score() { + // Two players, one winner + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.set_score(vec![String::from("StaticBot")]); + + assert_eq!( + coup.score, + vec![ + (String::from("StaticBot"), 1.0), + (String::from("StaticBot 2"), -1.0) + ] + ); + + // Five players, one winner + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + + coup.set_score(vec![String::from("StaticBot")]); + + assert_eq!( + coup.score, + vec![ + (String::from("StaticBot"), 1.0), + (String::from("StaticBot 2"), -0.25), + (String::from("StaticBot 3"), -0.25), + (String::from("StaticBot 4"), -0.25), + (String::from("StaticBot 5"), -0.25), + ] + ); + + // Five players, two winner + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + + coup + .set_score(vec![String::from("StaticBot"), String::from("StaticBot 2")]); + + assert_eq!( + coup.score, + vec![ + (String::from("StaticBot"), 0.5), + (String::from("StaticBot 2"), 0.5), + (String::from("StaticBot 3"), -0.3333333333333333), + (String::from("StaticBot 4"), -0.3333333333333333), + (String::from("StaticBot 5"), -0.3333333333333333), + ] + ); + } + + #[test] + fn test_swap_card() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + coup.playing_bots = vec![0, 1]; + + coup.bots[0].cards = vec![Card::Ambassador, Card::Ambassador]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![]); + + coup.swap_card(Card::Ambassador, String::from("StaticBot")); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.deck.len(), 2); + assert_eq!(coup.discard_pile, vec![]); + } + + // TODO: test_play + + #[test] + fn test_game_loop() { + struct ActionChallengeBot; + impl BotInterface for ActionChallengeBot { + fn get_name(&self) -> String { + String::from("ActionChallengeBot") + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct ChallengeCounterBot; + impl BotInterface for ChallengeCounterBot { + fn get_name(&self) -> String { + String::from("ChallengeCounterBot") + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct CounterBot; + impl BotInterface for CounterBot { + fn get_name(&self) -> String { + String::from("CounterBot") + } + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct AssassinationBot; + impl BotInterface for AssassinationBot { + fn get_name(&self) -> String { + String::from("AssassinationBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Assassination(String::from("StaticBot")) + } + } + struct CoupBot; + impl BotInterface for CoupBot { + fn get_name(&self) -> String { + String::from("CoupBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Coup(String::from("StaticBot")) + } + } + struct ForeignAidBot; + impl BotInterface for ForeignAidBot { + fn get_name(&self) -> String { + String::from("ForeignAidBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::ForeignAid + } + } + struct SwappingBot; + impl BotInterface for SwappingBot { + fn get_name(&self) -> String { + String::from("SwappingBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Swapping + } + } + struct IncomeBot; + impl BotInterface for IncomeBot { + fn get_name(&self) -> String { + String::from("IncomeBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Income + } + } + struct StealingBot; + impl BotInterface for StealingBot { + fn get_name(&self) -> String { + String::from("StealingBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Stealing(String::from("StaticBot")) + } + } + struct TaxBot; + impl BotInterface for TaxBot { + fn get_name(&self) -> String { + String::from("TaxBot") + } + fn on_turn(&self, _context: &Context) -> Action { + Action::Tax + } + } + + // Challenge successful + let mut coup = Coup::new(vec![ + Box::new(AssassinationBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::ActionAssassination { + by: String::from("AssassinationBot"), + target: String::from("StaticBot"), + }, + History::ChallengeAssassin { + by: String::from("ActionChallengeBot"), + target: String::from("AssassinationBot"), + } + ] + ); + + // Challenge unsuccessful + let mut coup = Coup::new(vec![ + Box::new(StealingBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards.len(), 1); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[2].coins, 0); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::ActionStealing { + by: String::from("StealingBot"), + target: String::from("StaticBot"), + }, + History::ChallengeCaptain { + by: String::from("ActionChallengeBot"), + target: String::from("StealingBot"), + } + ] + ); + + // Counter successful + let mut coup = Coup::new(vec![ + Box::new(ForeignAidBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards, vec![Card::Ambassador, Card::Assassin]); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::ActionForeignAid { + by: String::from("ForeignAidBot"), + }, + History::CounterForeignAid { + by: String::from("CounterBot"), + target: String::from("ForeignAidBot"), + } + ] + ); + + // Counter challenge successful + let mut coup = Coup::new(vec![ + Box::new(ForeignAidBot), + Box::new(CounterBot), + Box::new(ChallengeCounterBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[1].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::ActionForeignAid { + by: String::from("ForeignAidBot"), + }, + History::CounterForeignAid { + by: String::from("CounterBot"), + target: String::from("ForeignAidBot"), + }, + History::CounterChallengeDuke { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + }, + ] + ); + + // Counter challenge unsuccessful + let mut coup = Coup::new(vec![ + Box::new(ForeignAidBot), + Box::new(CounterBot), + Box::new(ChallengeCounterBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 1); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::ActionForeignAid { + by: String::from("ForeignAidBot"), + }, + History::CounterForeignAid { + by: String::from("CounterBot"), + target: String::from("ForeignAidBot"), + }, + History::CounterChallengeDuke { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + }, + ] + ); + + // Assassination + let mut coup = Coup::new(vec![ + Box::new(AssassinationBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.bots[1].cards, vec![Card::Duke]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionAssassination { + by: String::from("AssassinationBot"), + target: String::from("StaticBot"), + },] + ); + + // Coup + let mut coup = Coup::new(vec![ + Box::new(CoupBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 9; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 2); + assert_eq!(coup.bots[1].cards, vec![Card::Duke]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionCoup { + by: String::from("CoupBot"), + target: String::from("StaticBot"), + },] + ); + + // ForeignAid + let mut coup = Coup::new(vec![ + Box::new(ForeignAidBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 3; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 5); + assert_eq!(coup.bots[1].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionForeignAid { + by: String::from("ForeignAidBot"), + },] + ); + + // Swapping + let mut coup = Coup::new(vec![ + Box::new(SwappingBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 3; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[0].coins, 3); + assert_eq!(coup.bots[1].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionSwapping { + by: String::from("SwappingBot"), + },] + ); + + // Income + let mut coup = Coup::new(vec![ + Box::new(IncomeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 3; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionIncome { + by: String::from("IncomeBot"), + },] + ); + + // Stealing + let mut coup = Coup::new(vec![ + Box::new(StealingBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 3; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 5); + assert_eq!(coup.bots[1].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[1].coins, 0); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionStealing { + by: String::from("StealingBot"), + target: String::from("StaticBot"), + },] + ); + + // Tax + let mut coup = Coup::new(vec![ + Box::new(TaxBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 3; + coup.bots[1].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.game_loop(); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ActionTax { + by: String::from("TaxBot"), + },] + ); + } + + #[test] + fn test_challenge_and_counter_round_assassination() { + struct ActionChallengeBot; + impl BotInterface for ActionChallengeBot { + fn get_name(&self) -> String { + String::from("ActionChallengeBot") + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct ChallengeCounterBot; + impl BotInterface for ChallengeCounterBot { + fn get_name(&self) -> String { + String::from("ChallengeCounterBot") + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct CounterBot; + impl BotInterface for CounterBot { + fn get_name(&self) -> String { + String::from("CounterBot") + } + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + // Successful challenge + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("StaticBot 2")), + String::from("StaticBot 2"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ChallengeAssassin { + by: String::from("ActionChallengeBot"), + target: String::from("StaticBot"), + }] + ); + + // Successful counter + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Ambassador, Card::Assassin]); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::CounterAssassination { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }] + ); + + // Successful counter challenge + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(ChallengeCounterBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Captain, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 0); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::CounterAssassination { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }, + History::CounterChallengeContessa { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + } + ] + ); + + // Successful action + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("StaticBot 4")), + String::from("StaticBot 4"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + + // Unsuccessful challenge + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("StaticBot 3")), + String::from("StaticBot 3"), + ); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.bots[1].cards.len(), 1); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ChallengeAssassin { + by: String::from("ActionChallengeBot"), + target: String::from("StaticBot"), + }] + ); + + // Unsuccessful counter challenge + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(ChallengeCounterBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Contessa, Card::Assassin]; + coup.bots[5].cards = vec![Card::Duke, Card::Ambassador]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Assassination(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards, vec![Card::Duke]); + assert_eq!( + coup.history, + vec![ + History::CounterAssassination { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }, + History::CounterChallengeContessa { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + } + ] + ); + } + + #[test] + fn test_challenge_and_counter_round_stealing() { + struct ActionChallengeBot; + impl BotInterface for ActionChallengeBot { + fn get_name(&self) -> String { + String::from("ActionChallengeBot") + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct ChallengeCounterBot; + impl BotInterface for ChallengeCounterBot { + fn get_name(&self) -> String { + String::from("ChallengeCounterBot") + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct CounterBot; + impl BotInterface for CounterBot { + fn get_name(&self) -> String { + String::from("CounterBot") + } + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + // Successful challenge + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("StaticBot 2")), + String::from("StaticBot 2"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[3].coins, 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ChallengeCaptain { + by: String::from("ActionChallengeBot"), + target: String::from("StaticBot"), + }] + ); + + // Successful counter + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[3].coins, 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::CounterStealing { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }] + ); + + // Successful counter challenge + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(ChallengeCounterBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Duke]); + assert_eq!(coup.bots[3].coins, 0); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![ + History::CounterStealing { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }, + History::CounterChallengeCaptainAmbassedor { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + } + ] + ); + + // Successful action + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("StaticBot 4")), + String::from("StaticBot 4"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[3].coins, 0); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + + // Unsuccessful challenge + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(ActionChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("StaticBot 3")), + String::from("StaticBot 3"), + ); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.bots[1].cards.len(), 1); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[3].coins, 0); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards.len(), 2); + assert_eq!( + coup.history, + vec![History::ChallengeCaptain { + by: String::from("ActionChallengeBot"), + target: String::from("StaticBot"), + }] + ); + + // Unsuccessful counter challenge with Captain + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(ChallengeCounterBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Captain, Card::Assassin]; + coup.bots[5].cards = vec![Card::Duke, Card::Ambassador]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[3].coins, 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards, vec![Card::Duke]); + assert_eq!( + coup.history, + vec![ + History::CounterStealing { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }, + History::CounterChallengeCaptainAmbassedor { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + } + ] + ); + + // Unsuccessful counter challenge with Ambassador + coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(CounterBot), + Box::new(StaticBot), + Box::new(ChallengeCounterBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[0].coins = 4; + coup.bots[3].cards = vec![Card::Ambassador, Card::Assassin]; + coup.bots[5].cards = vec![Card::Duke, Card::Ambassador]; + coup.playing_bots = vec![0, 1, 2, 3, 4, 5]; + coup.turn = 0; + coup.history = vec![]; + + coup.challenge_and_counter_round( + Action::Stealing(String::from("CounterBot")), + String::from("CounterBot"), + ); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].cards.len(), 2); + assert_eq!(coup.bots[2].cards.len(), 2); + assert_eq!(coup.bots[3].cards.len(), 2); + assert_eq!(coup.bots[3].coins, 2); + assert_eq!(coup.bots[4].cards.len(), 2); + assert_eq!(coup.bots[5].cards, vec![Card::Duke]); + assert_eq!( + coup.history, + vec![ + History::CounterStealing { + by: String::from("CounterBot"), + target: String::from("StaticBot"), + }, + History::CounterChallengeCaptainAmbassedor { + by: String::from("ChallengeCounterBot"), + target: String::from("CounterBot"), + } + ] + ); + } + + #[test] + fn test_challenge_round_only_successful() { + // Swapping + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(TestBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.challenge_round_only(Action::Swapping); + + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[2].cards, vec![Card::Ambassador, Card::Assassin]); + + // Action::Tax + coup.setup(); + coup.bots[0].cards = vec![Card::Ambassador, Card::Captain]; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.challenge_round_only(Action::Tax); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[2].cards, vec![Card::Ambassador, Card::Assassin]); + } + + #[test] + fn test_challenge_round_only_unsuccessful() { + // Swapping + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(TestBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Ambassador, Card::Captain]; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.challenge_round_only(Action::Swapping); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[2].cards, vec![Card::Ambassador]); + + // Action::Tax + coup.setup(); + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[2].cards = vec![Card::Ambassador, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.challenge_round_only(Action::Tax); + + assert_eq!(coup.bots[0].cards.len(), 2); + assert_eq!(coup.bots[2].cards, vec![Card::Ambassador]); + } + + #[test] + fn test_counter_round_only_successful() { + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct ChallengeBot; + impl BotInterface for ChallengeBot { + fn get_name(&self) -> String { + String::from("ChallengeBot") + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(TestBot), + Box::new(ChallengeBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[3].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[4].cards = vec![Card::Captain, Card::Duke]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.counter_round_only(); + + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 2); + assert_eq!(coup.bots[3].cards, vec![Card::Duke, Card::Assassin]); + assert_eq!(coup.bots[4].cards, vec![Card::Captain]); + } + + #[test] + fn test_counter_round_only_unsuccessful() { + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_counter( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + struct ChallengeBot; + impl BotInterface for ChallengeBot { + fn get_name(&self) -> String { + String::from("ChallengeBot") + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(ChallengeBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(TestBot), + ]); + coup.setup(); + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[2].cards = vec![Card::Duke, Card::Duke]; + coup.bots[4].cards = vec![Card::Captain, Card::Assassin]; + coup.playing_bots = vec![0, 1, 2, 3, 4]; + coup.turn = 0; + + coup.counter_round_only(); + + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[2].cards, vec![Card::Duke, Card::Duke]); + assert_eq!(coup.bots[4].cards, vec![Card::Captain]); + } + + #[test] + fn test_challenge_round_action_no_challenge() { + struct TestBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for TestBot { + fn get_name(&self) -> String { + format!("TestBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self.calls.borrow_mut().push(String::from("on_challenge_action_round")); + false + } + } + + let mut coup = Coup::new(vec![ + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + ]); + coup.setup(); + + coup.challenge_round( + ChallengeRound::Action, + &Action::Swapping, + String::from("TestBot"), + ); + + assert_eq!(coup.bots[0].interface.get_name(), String::from("TestBot")); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[4].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!(coup.history, vec![]); + + coup.challenge_round( + ChallengeRound::Action, + &Action::Swapping, + String::from("TestBot 3"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from( + "TestBoton_challenge_action_round,on_challenge_action_round" + ) + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from( + "TestBoton_challenge_action_round,on_challenge_action_round" + ) + ); + assert_eq!( + coup.bots[4].interface.get_name(), + String::from( + "TestBoton_challenge_action_round,on_challenge_action_round" + ) + ); + assert_eq!(coup.history, vec![]); + } + + #[test] + fn test_challenge_round_action() { + struct TestBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for TestBot { + fn get_name(&self) -> String { + format!("TestBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self.calls.borrow_mut().push(String::from("on_challenge_action_round")); + false + } + } + pub struct ChallengeBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for ChallengeBot { + fn get_name(&self) -> String { + format!("ChallengeBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_action_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self.calls.borrow_mut().push(String::from("on_challenge_action_round")); + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(ChallengeBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + ]); + coup.setup(); + coup.playing_bots = vec![0, 1, 2, 3, 4]; + + coup.challenge_round( + ChallengeRound::Action, + &Action::Swapping, + String::from("TestBot"), + ); + + assert_eq!(coup.bots[0].interface.get_name(), String::from("TestBot")); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from("ChallengeBoton_challenge_action_round") + ); + assert_eq!(coup.bots[3].interface.get_name(), String::from("TestBot")); + assert_eq!(coup.bots[4].interface.get_name(), String::from("TestBot")); + + coup.challenge_round( + ChallengeRound::Action, + &Action::Swapping, + String::from("TestBot 4"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from("TestBoton_challenge_action_round") + ); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from( + "TestBoton_challenge_action_round,on_challenge_action_round" + ) + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from( + "ChallengeBoton_challenge_action_round,on_challenge_action_round" + ) + ); + assert_eq!(coup.bots[3].interface.get_name(), String::from("TestBot")); + assert_eq!(coup.bots[4].interface.get_name(), String::from("TestBot")); + } + + #[test] + fn test_challenge_round_counter_no_challenge() { + struct TestBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for TestBot { + fn get_name(&self) -> String { + format!("TestBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self + .calls + .borrow_mut() + .push(String::from("on_challenge_counter_round")); + false + } + } + + let mut coup = Coup::new(vec![ + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + ]); + coup.setup(); + + coup.challenge_round( + ChallengeRound::Counter, + &Action::ForeignAid, + String::from("TestBot 2"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!(coup.bots[1].interface.get_name(), String::from("TestBot")); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!( + coup.bots[4].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!(coup.history, vec![]); + + coup.challenge_round( + ChallengeRound::Counter, + &Action::ForeignAid, + String::from("TestBot 5"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from( + "TestBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from( + "TestBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from( + "TestBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!( + coup.bots[4].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!(coup.history, vec![]); + } + + #[test] + fn test_challenge_round_counter() { + struct TestBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for TestBot { + fn get_name(&self) -> String { + format!("TestBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self + .calls + .borrow_mut() + .push(String::from("on_challenge_counter_round")); + false + } + } + pub struct ChallengeBot { + pub calls: std::cell::RefCell>, + } + impl BotInterface for ChallengeBot { + fn get_name(&self) -> String { + format!("ChallengeBot{}", self.calls.borrow().join(",")) + } + fn on_challenge_counter_round( + &self, + _action: &Action, + _by: String, + _context: &Context, + ) -> bool { + self + .calls + .borrow_mut() + .push(String::from("on_challenge_counter_round")); + true + } + } + + let mut coup = Coup::new(vec![ + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(ChallengeBot { + calls: std::cell::RefCell::new(vec![]), + }), + Box::new(TestBot { + calls: std::cell::RefCell::new(vec![]), + }), + ]); + coup.setup(); + coup.playing_bots = vec![0, 1, 2, 3, 4]; + + coup.challenge_round( + ChallengeRound::Counter, + &Action::ForeignAid, + String::from("TestBot 2"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!(coup.bots[1].interface.get_name(), String::from("TestBot")); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from("ChallengeBoton_challenge_counter_round") + ); + assert_eq!(coup.bots[4].interface.get_name(), String::from("TestBot")); + + coup.challenge_round( + ChallengeRound::Counter, + &Action::ForeignAid, + String::from("TestBot 4"), + ); + + assert_eq!( + coup.bots[0].interface.get_name(), + String::from( + "TestBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!( + coup.bots[1].interface.get_name(), + String::from("TestBoton_challenge_counter_round") + ); + assert_eq!( + coup.bots[2].interface.get_name(), + String::from( + "TestBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!( + coup.bots[3].interface.get_name(), + String::from( + "ChallengeBoton_challenge_counter_round,on_challenge_counter_round" + ) + ); + assert_eq!(coup.bots[4].interface.get_name(), String::from("TestBot")); + } + + #[test] + fn test_resolve_challenge_successful() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + // Assassination + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Assassination(String::from("StaticBot 2")), + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeAssassin { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Swapping + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Swapping, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeAmbassador { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Stealing + coup.bots[0].cards = vec![Card::Duke, Card::Assassin]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Stealing(String::from("StaticBot 2")), + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeCaptain { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Tax + coup.bots[0].cards = vec![Card::Captain, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Tax, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeDuke { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + } + + #[test] + fn test_resolve_challenge_unsuccessful() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + // Assassination + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Assassination(String::from("StaticBot 2")), + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeAssassin { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Swapping + coup.bots[0].cards = vec![Card::Assassin, Card::Ambassador]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Swapping, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeAmbassador { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Stealing + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Stealing(String::from("StaticBot 2")), + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeCaptain { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Tax + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_challenge( + Action::Tax, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::ChallengeDuke { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + } + + #[test] + fn test_resolve_counter_challenge_successful() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + // Assassination + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::Assassination, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeContessa { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Foreign Aid + coup.bots[0].cards = vec![Card::Assassin, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::ForeignAid, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeDuke { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Stealing + coup.bots[0].cards = vec![Card::Assassin, Card::Contessa]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::Stealing, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, true); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador, Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeCaptainAmbassedor { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + } + + #[test] + fn test_resolve_counter_challenge_unsuccessful() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + // Assassination + coup.bots[0].cards = vec![Card::Assassin, Card::Contessa]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::Assassination, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Assassin, Card::Contessa]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeContessa { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Foreign Aid + coup.bots[0].cards = vec![Card::Duke, Card::Contessa]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::ForeignAid, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Contessa]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeDuke { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Stealing with Captain + coup.bots[0].cards = vec![Card::Duke, Card::Captain]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::Stealing, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeCaptainAmbassedor { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + coup.history = vec![]; + + // Stealing with Ambassador + coup.bots[0].cards = vec![Card::Duke, Card::Ambassador]; + coup.bots[1].cards = vec![Card::Ambassador, Card::Ambassador]; + + let result = coup.resolve_counter_challenge( + Counter::Stealing, + String::from("StaticBot"), + String::from("StaticBot 2"), + ); + + assert_eq!(result, false); + assert_eq!(coup.bots[0].cards, vec![Card::Duke, Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Ambassador]); + assert_eq!( + coup.history, + vec![History::CounterChallengeCaptainAmbassedor { + by: String::from("StaticBot 2"), + target: String::from("StaticBot") + }] + ); + } + + // TODO: test_display_score + + #[test] + fn test_format_number_with_separator() { + assert_eq!(Coup::format_number_with_separator(0), String::from("0")); + assert_eq!(Coup::format_number_with_separator(00000), String::from("0")); + assert_eq!(Coup::format_number_with_separator(1), String::from("1")); + assert_eq!(Coup::format_number_with_separator(99), String::from("99")); + assert_eq!(Coup::format_number_with_separator(999), String::from("999")); + assert_eq!(Coup::format_number_with_separator(1234), String::from("1,234")); + assert_eq!( + Coup::format_number_with_separator(9876543210), + String::from("9,876,543,210") + ); + } + + // TODO: test_looping + + // *******************************| Actions |****************************** // + #[test] + fn test_action_assassination() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 4; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_assassination(String::from("StaticBot 2")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin]); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Captain]); + } + + #[test] + fn test_action_assassination_unknown_bot() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 4; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_assassination(String::from("Unknown bot")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Duke]); + } + + #[test] + fn test_action_assassination_insufficient_funds() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 2; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_assassination(String::from("StaticBot 2")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 2); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Duke]); + } + + #[test] + fn test_action_couping() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 8; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_couping(String::from("StaticBot 2")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin]); + assert_eq!(coup.bots[0].coins, 1); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Captain]); + } + + #[test] + fn test_action_couping_unknown_bot() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 8; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_couping(String::from("Unknown bot")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 8); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Duke]); + } + + #[test] + fn test_action_couping_insufficient_funds() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.bots[0].coins = 6; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_couping(String::from("StaticBot 2")); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.bots[0].coins, 6); + assert_eq!(coup.deck, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.discard_pile, vec![Card::Duke]); + } + + #[test] + fn test_action_foraign_aid() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + coup.playing_bots = vec![0, 1]; + + coup.action_foraign_aid(); + + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].coins, 2); + } + + #[test] + fn test_action_swapping() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.deck = vec![Card::Captain, Card::Assassin]; + + coup.action_swapping(); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Duke]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.deck.len(), 2); + } + + #[test] + fn test_action_swapping_custom_bot() { + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_swapping_cards( + &self, + new_cards: [Card; 2], + context: &Context, + ) -> [Card; 2] { + [new_cards[1], context.cards[1]] + } + } + + let mut coup = Coup::new(vec![Box::new(TestBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.deck = vec![Card::Assassin, Card::Captain]; + + coup.action_swapping(); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador, Card::Captain]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.deck.len(), 2); + } + + #[test] + fn test_action_swapping_faulty_bot() { + struct TestBot; + impl BotInterface for TestBot { + fn get_name(&self) -> String { + String::from("TestBot") + } + fn on_swapping_cards( + &self, + _new_cards: [Card; 2], + _context: &Context, + ) -> [Card; 2] { + [Card::Assassin, Card::Duke] + } + } + + let mut coup = Coup::new(vec![Box::new(TestBot), Box::new(StaticBot)]); + coup.setup(); + + coup.bots[0].cards = vec![Card::Ambassador, Card::Duke]; + coup.bots[1].cards = vec![Card::Assassin, Card::Captain]; + coup.playing_bots = vec![0, 1]; + coup.deck = vec![Card::Ambassador, Card::Captain]; + + coup.action_swapping(); + + assert_eq!(coup.bots[0].cards, vec![Card::Ambassador]); + assert_eq!(coup.bots[1].cards, vec![Card::Assassin, Card::Captain]); + assert_eq!(coup.deck.len(), 0); + } + + #[test] + fn test_action_income() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + coup.playing_bots = vec![0, 1]; + + coup.action_income(); + + assert_eq!(coup.bots[0].coins, 3); + assert_eq!(coup.bots[1].coins, 2); + } + + #[test] + fn test_action_stealing() { + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.playing_bots = vec![0, 1, 2, 3]; + + coup.action_stealing(String::from("StaticBot 3")); + + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].coins, 2); + assert_eq!(coup.bots[2].coins, 0); + assert_eq!(coup.bots[3].coins, 2); + } + + #[test] + fn test_action_stealing_min() { + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.playing_bots = vec![0, 1, 2, 3]; + coup.bots[2].coins = 1; + + coup.action_stealing(String::from("StaticBot 3")); + + assert_eq!(coup.bots[0].coins, 3); + assert_eq!(coup.bots[1].coins, 2); + assert_eq!(coup.bots[2].coins, 0); + assert_eq!(coup.bots[3].coins, 2); + } + + #[test] + fn test_action_stealing_max() { + let mut coup = Coup::new(vec![ + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + Box::new(StaticBot), + ]); + coup.setup(); + coup.playing_bots = vec![0, 1, 2, 3]; + coup.bots[2].coins = 5; + + coup.action_stealing(String::from("StaticBot 3")); + + assert_eq!(coup.bots[0].coins, 4); + assert_eq!(coup.bots[1].coins, 2); + assert_eq!(coup.bots[2].coins, 3); + assert_eq!(coup.bots[3].coins, 2); + } + + #[test] + fn test_action_tax() { + let mut coup = Coup::new(vec![Box::new(StaticBot), Box::new(StaticBot)]); + coup.setup(); + coup.playing_bots = vec![0, 1]; + + coup.action_tax(); + + assert_eq!(coup.bots[0].coins, 5); + assert_eq!(coup.bots[1].coins, 2); + } +} diff --git a/test/test.js b/test/test.js deleted file mode 100644 index e4c9339..0000000 --- a/test/test.js +++ /dev/null @@ -1,1378 +0,0 @@ -const { style } = require('../src/helper.js'); -const { COUP } = require('../src/index.js'); - -// defaults -const makeBots = () => ({ - bot1: { - onTurn: () => ({}), - onChallengeActionRound: () => false, - onCounterAction: () => false, - onCounterActionRound: () => false, - onSwappingCards: () => {}, - onCardLoss: ({ myCards }) => myCards[0], - }, - bot2: { - onTurn: () => ({}), - onChallengeActionRound: () => false, - onCounterAction: () => false, - onCounterActionRound: () => false, - onSwappingCards: () => {}, - onCardLoss: ({ myCards }) => myCards[0], - }, - bot3: { - onTurn: () => ({}), - onChallengeActionRound: () => false, - onCounterAction: () => false, - onCounterActionRound: () => false, - onSwappingCards: () => {}, - onCardLoss: ({ myCards }) => myCards[0], - }, -}); - -const makePlayer = () => ({ - bot1: { - card1: undefined, - card2: undefined, - coins: 0, - }, - bot2: { - card1: undefined, - card2: undefined, - coins: 0, - }, - bot3: { - card1: undefined, - card2: undefined, - coins: 0, - }, -}); - -console.log = () => {}; -let pass = true; - -const TEST = { - // _____ _ _ _ ___ _ _ ___ _ - // |_ _| /_\ | |/ / |_ _| | \| | / __| ___ / | - // | | / _ \ | ' < | | | .` | | (_ | |___| | | - // |_| /_/ \_\ |_|\_\ |___| |_|\_| \___| |_| - // bot1 will take one coin - 'taking-1': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'taking-1', against: 'bot1' }); - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.coins === 1 && game.PLAYER.bot1.card1 === 'duke' && game.PLAYER.bot2.card1 === 'duke') { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('taking-1')} action`); - }, - // __ ___ _ _ ___ ___ _ _ ___ - // / __| / _ \ | | | | | _ \ |_ _| | \| | / __| - // | (__ | (_) | | |_| | | _/ | | | .` | | (_ | - // \___| \___/ \___/ |_| |___| |_|\_| \___| - // bot1 will coup bot2 - couping: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot1.coins = 8; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'couping', against: 'bot2' }); - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.card1 === 'duke' && game.PLAYER.bot1.coins === 1 && game.PLAYER.bot2.card1 === undefined) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('couping')} action`); - }, - // _____ _ _ _ ___ _ _ ___ ___ - // |_ _| /_\ | |/ / |_ _| | \| | / __| ___ |__ / - // | | / _ \ | ' < | | | .` | | (_ | |___| |_ \ - // |_| /_/ \_\ |_|\_\ |___| |_|\_| \___| |___/ - // bot1 will take three coins with duke - 'taking-31': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'taking-3', against: 'bot2' }); - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.coins === 3 && game.PLAYER.bot1.card1 === 'duke' && game.PLAYER.bot2.card1 === 'duke') { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('taking-3')} without challenge`); - }, - // bot1 will take three coins with duke, bot2 calls bot1, bot1 did not have the duke - 'taking-32': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'captain'; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'taking-3', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.coins === 0 && game.PLAYER.bot1.card1 === undefined && game.PLAYER.bot2.card1 === 'duke') { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('taking-3')} with successful challenge`); - }, - // bot1 will take three coins with duke, bot2 calls bot1, bot1 did have the duke - 'taking-33': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'taking-3', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.coins === 3 && game.PLAYER.bot1.card1 === 'duke' && game.PLAYER.bot2.card1 === undefined) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('taking-3')} with unsuccessful challenge`); - }, - // _ ___ ___ _ ___ ___ ___ _ _ _ _____ ___ ___ _ _ - // /_\ / __| / __| /_\ / __| / __| |_ _| | \| | /_\ |_ _| |_ _| / _ \ | \| | - // / _ \ \__ \ \__ \ / _ \ \__ \ \__ \ | | | .` | / _ \ | | | | | (_) | | .` | - // /_/ \_\ |___/ |___/ /_/ \_\ |___/ |___/ |___| |_|\_| /_/ \_\ |_| |___| \___/ |_|\_| - // bot1 will assassinate bot2 with assassin - assassination1: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'captain'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onCardLoss = () => 'captain'; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'duke' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot2.card2 === undefined && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} without challenge or counter action`); - }, - // bot1 will assassinate bot2 with assassin, bot2 calls bot1, bot1 did not have the assassin - assassination2: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'captain'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot2.card2 === 'captain' && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} with successful challenge`); - }, - // bot1 will assassinate bot2 with assassin, bot2 calls bot1, bot1 did have the assassin - assassination3: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'captain'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - bots.bot2.onCardLoss = () => 'captain'; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 !== undefined && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === undefined && - game.PLAYER.bot2.card2 === undefined && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} with unsuccessful challenge`); - }, - // bot1 will assassinate bot2 with assassin, bot2 says it has the contessa, bot1 is fine with that - assassination4: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'captain'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'contessa'; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot2.card2 === 'captain' && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} with counter action but no counter challenge`); - }, - // bot1 will assassinate bot2 with assassin, bot2 says it has the contessa, bot1 is challenging bot2, bot2 did not have the contessa - assassination5: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'captain'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'contessa'; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === undefined && - game.PLAYER.bot2.card2 === undefined && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} with counter action and successful counter challenge`); - }, - // bot1 will assassinate bot2 with assassin, bot2 says it has the contessa, bot1 is challenging bot2, bot2 did have the contessa - assassination6: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.coins = 4; - player.bot2.card1 = 'duke'; - player.bot2.card2 = 'contessa'; - player.bot2.coins = 5; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'contessa'; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot2.card2 !== undefined && - game.PLAYER.bot2.coins === 5 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('assassination')} with counter action and unsuccessful counter challenge`); - }, - // ___ _____ ___ _ _ ___ _ _ ___ - // / __| |_ _| | __| /_\ | | |_ _| | \| | / __| - // \__ \ | | | _| / _ \ | |__ | | | .` | | (_ | - // |___/ |_| |___| /_/ \_\ |____| |___| |_|\_| \___| - // bot1 will steal from bot2 with captain - stealing1: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'duke'; - player.bot2.coins = 5; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot1.card1 === 'duke' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot2.card1 === 'duke' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} without challenge or counter action`); - }, - // bot1 will steal from bot2 with captain, bot2 calls bot1, bot1 did not have the captain - stealing2: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'duke'; - player.bot2.coins = 5; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot2.coins === 5 && - game.PLAYER.bot2.card1 === 'duke' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} with successful challenge`); - }, - // bot1 will steal from bot2 with captain, bot2 calls bot1, bot1 did have the captain - stealing3: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'captain'; - player.bot2.coins = 5; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'assassin']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot1.card1 !== undefined && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot2.card1 === undefined - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} with unsuccessful challenge`); - }, - // bot1 will steal from bot2 with captain, bot2 says it has the captain|ambassador, bot1 is fine with that - stealing4: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'captain'; - player.bot2.coins = 5; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'captain'; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'assassin']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot1.card1 === 'captain' && - game.PLAYER.bot2.coins === 5 && - game.PLAYER.bot2.card1 === 'duke' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} with counter action but no counter challenge`); - }, - // bot1 will steal from bot2 with captain, bot2 says it has the captain|ambassador, bot1 is challenging bot2, bot2 did not have the captain|ambassador - stealing5: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'captain'; - player.bot2.coins = 5; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'captain'; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'assassin']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot1.card1 === 'captain' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot2.card1 === undefined - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} with counter action and successful counter challenge`); - }, - // bot1 will steal from bot2 with captain, bot2 says it has the captain|ambassador, bot1 is challenging bot2, bot2 did have the captain|ambassador - stealing6: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 1; - player.bot1.card1 = 'captain'; - player.bot2.coins = 5; - player.bot2.card1 = 'captain'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'captain'; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'assassin']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot2.coins === 5 && - game.PLAYER.bot2.card1 !== undefined - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('stealing')} with counter action and unsuccessful counter challenge`); - }, - // ___ __ __ _ ___ ___ ___ _ _ ___ - // / __| \ \ / / /_\ | _ \ | _ \ |_ _| | \| | / __| - // \__ \ \ \/\/ / / _ \ | _/ | _/ | | | .` | | (_ | - // |___/ \_/\_/ /_/ \_\ |_| |_| |___| |_|\_| \___| - // bot1 will swap cards with ambassador - swapping1: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'contessa'; - player.bot2.card1 = 'ambassador'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'swapping', against: 'bot2' }); - bots.bot1.onSwappingCards = ({ newCards }) => newCards; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'captain' && - game.PLAYER.bot1.card2 === 'duke' && - game.PLAYER.bot2.card1 === 'ambassador' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('swapping')} without challenge`); - }, - // bot1 will swap cards with ambassador, bot2 calls bot1, bot1 did not have the ambassador - swapping2: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'contessa'; - player.bot2.card1 = 'ambassador'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'swapping', against: 'bot2' }); - bots.bot1.onSwappingCards = ({ newCards }) => newCards; - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot1.card2 === 'contessa' && - game.PLAYER.bot2.card1 === 'ambassador' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('swapping')} with successful challenge`); - }, - // bot1 will swap cards with ambassador, bot2 calls bot1, bot1 did have the ambassador - swapping3: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'ambassador'; - player.bot1.card2 = 'contessa'; - player.bot2.card1 = 'ambassador'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'swapping', against: 'bot2' }); - bots.bot1.onSwappingCards = ({ newCards }) => newCards; - bots.bot2.onChallengeActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['duke', 'captain']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 !== undefined && - game.PLAYER.bot1.card2 !== undefined && - game.PLAYER.bot2.card1 === undefined - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} ${style.yellow('swapping')} with unsuccessful challenge`); - }, - // ___ ___ ___ ___ ___ ___ _ _ _ ___ ___ - // | __| / _ \ | _ \ | __| |_ _| / __| | \| | ___ /_\ |_ _| | \ - // | _| | (_) | | / | _| | | | (_ | | .` | |___| / _ \ | | | |) | - // |_| \___/ |_|_\ |___| |___| \___| |_|\_| /_/ \_\ |___| |___/ - // bot1 will take bot2 foreign aid - 'foreign-aid1': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'foreign-aid', against: 'bo2' }); - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if (game.PLAYER.bot1.coins === 2 && game.PLAYER.bot1.card1 === 'duke' && game.PLAYER.bot2.card1 === 'duke') { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${style.red(status)} ${style.yellow('foreign-aid')} without counter action`); - }, - // bot1 will take bot2 foreign aid, bot2 says it has the duke, bot1 is fine with that - 'foreign-aid2': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - player.bot3.card1 = 'captain'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'foreign-aid', against: 'bo2' }); - bots.bot3.onCounterAction = () => 'duke'; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 0 && - game.PLAYER.bot1.card1 === 'duke' && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot3.card1 === 'captain' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${style.red(status)} ${style.yellow('foreign-aid')} with counter action and no counter challenge`); - }, - // bot1 will take bot2 foreign aid, bot2 says it has the duke, bot1 calls bot2, bot2 did not have the duke - 'foreign-aid3': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - player.bot3.card1 = 'captain'; - - const bots = makeBots(); - bots.bot1.onTurn = () => ({ action: 'foreign-aid', against: 'bo2' }); - bots.bot3.onCounterAction = () => 'duke'; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 2 && - game.PLAYER.bot1.card1 === 'duke' && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot3.card1 === undefined - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info( - `${style.red(status)} ${style.yellow('foreign-aid')} with counter action and successful counter challenge` - ); - }, - // bot1 will take bot2 foreign aid, bot2 says it has the duke, bot1 calls bot2, bot2 did have the duke - 'foreign-aid4': async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.coins = 0; - player.bot1.card1 = 'duke'; - player.bot2.card1 = 'duke'; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let runs = []; - bots.bot1.onTurn = () => ({ action: 'foreign-aid', against: 'bo2' }); - bots.bot1.onCounterAction = () => { - runs.push('bot1'); - return 'duke'; - }; - bots.bot2.onCounterAction = () => { - runs.push('bot2'); - return 'duke'; - }; - bots.bot3.onCounterAction = () => { - runs.push('bot3'); - return 'duke'; - }; - bots.bot1.onCounterActionRound = () => true; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = []; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.coins === 0 && - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot2.card1 === 'duke' && - game.PLAYER.bot3.card1 !== undefined && - runs.length === 1 && - !runs.includes('bot1') && - !runs.includes('bot3') - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info( - `${style.red(status)} ${style.yellow('foreign-aid')} with counter action and unsuccessful counter challenge` - ); - }, - - 'challenge-only-once': () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 1; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let runs = 0; - bots.bot2.onCounterAction = () => 'captain'; - bots.bot1.onCardLoss = () => 'assassin'; - bots.bot1.onCounterActionRound = () => { - runs++; - return true; - }; - bots.bot2.onCounterActionRound = () => { - runs++; - return true; - }; - bots.bot3.onCounterActionRound = () => { - runs++; - return true; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 0; - - game.runChallenges({ action: 'stealing', player: 'bot1', target: 'bot2' }); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 !== undefined && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - runs === 1 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} an unsuccessful counter action round yields punishment`); - }, - swapCards1: () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'duke'; - player.bot1.card2 = 'contessa'; - - game.PLAYER = player; - game.DECK = ['duke']; - - game.swapCards({ - chosenCards: ['assassin', 'captain'], - player: 'bot1', - newCards: ['captain', 'assassin'], - }); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.DECK.includes('duke') && - game.DECK.includes('duke') && - game.DECK.includes('contessa') - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} swapCards merges cards correctly`); - }, - swapCards2: () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = undefined; - player.bot1.card2 = 'contessa'; - - game.PLAYER = player; - game.DECK = ['ambassador']; - - game.swapCards({ - chosenCards: ['ambassador', 'captain'], - player: 'bot1', - newCards: ['captain', 'duke'], - }); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'captain' && - game.PLAYER.bot1.card2 === undefined && - game.DECK.includes('ambassador') && - game.DECK.includes('contessa') && - game.DECK.includes('duke') - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} swapCards merges cards correctly even with one card`); - }, - swapCards3: () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'contessa'; - player.bot1.card2 = undefined; - - game.PLAYER = player; - game.DECK = []; - - game.swapCards({ - chosenCards: ['ambassador', 'ambassador'], - player: 'bot1', - newCards: ['captain', 'duke'], - }); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === undefined && - game.PLAYER.bot1.card2 === undefined && - game.DECK.includes('contessa') && - game.DECK.includes('captain') && - game.DECK.includes('duke') - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} swapCards merges cards correctly even when given cards are invalid`); - }, - checkParameters1: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 1; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let output; - bots.bot1.onTurn = (param) => { - output = param; - return { action: 'foreign-aid' }; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot2.card1 === 'captain' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - output.history.length === 0 && - output.myCards[0] === 'assassin' && - output.myCards[1] === 'captain' && - output.myCoins === 1 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} we get the right parameters passed in for onTurn`); - }, - checkParameters2: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 1; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let output; - bots.bot1.onTurn = () => ({ action: 'stealing', against: 'bot2' }); - bots.bot2.onChallengeActionRound = (param) => { - output = param; - return false; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot2.card1 === 'captain' && - game.PLAYER.bot2.coins === 1 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - output.myCards[0] === 'captain' && - output.myCoins === 3 - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} we get the right parameters passed in for onChallengeActionRound`); - }, - checkParameters3: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 1; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let output; - bots.bot1.onTurn = () => ({ action: 'foreign-aid' }); - bots.bot2.onCounterAction = (param) => { - output = param; - return false; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 3 && - game.PLAYER.bot2.card1 === 'captain' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - output.action === 'foreign-aid' && - output.byWhom === 'bot1' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} we get the right parameters passed in for onCounterAction`); - }, - checkParameters4: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 1; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let output; - bots.bot1.onTurn = () => ({ action: 'foreign-aid' }); - bots.bot2.onCounterAction = () => 'duke'; - bots.bot3.onCounterActionRound = (param) => { - output = param; - return false; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'captain' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - output.action === 'foreign-aid' && - output.byWhom === 'bot1' && - output.counterer === 'bot2' && - output.card === 'duke' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} we get the right parameters passed in for onCounterActionRound for duking`); - }, - checkParameters5: async () => { - const game = new COUP(); - - const player = makePlayer(); - player.bot1.card1 = 'assassin'; - player.bot1.card2 = 'captain'; - player.bot1.coins = 4; - player.bot2.card1 = 'captain'; - player.bot2.coins = 3; - player.bot3.card1 = 'duke'; - - const bots = makeBots(); - let output; - bots.bot1.onTurn = () => ({ action: 'assassination', against: 'bot2' }); - bots.bot2.onCounterAction = () => 'contessa'; - bots.bot3.onCounterActionRound = (param) => { - output = param; - return false; - }; - - game.HISTORY = []; - game.DISCARDPILE = []; - game.BOTS = bots; - game.PLAYER = player; - game.DECK = ['contessa']; - game.TURN = 2; - game.whoIsLeft = () => ['bot1']; - - await game.turn(); - - let status = style.red('FAIL'); - if ( - game.PLAYER.bot1.card1 === 'assassin' && - game.PLAYER.bot1.card2 === 'captain' && - game.PLAYER.bot1.coins === 1 && - game.PLAYER.bot2.card1 === 'captain' && - game.PLAYER.bot2.coins === 3 && - game.PLAYER.bot3.card1 === 'duke' && - game.DECK.length === 1 && - output.action === 'assassination' && - output.byWhom === 'bot1' && - output.toWhom === 'bot2' && - output.counterer === 'bot2' && - output.card === 'contessa' - ) { - status = style.green('PASS'); - } else { - pass = false; - } - console.info(`${status} we get the right parameters passed in for onCounterActionRound for assassination`); - }, -}; - -console.info(` - ████████╗ ███████╗ ███████╗ ████████╗ ██╗ ███╗ ██╗ ██████╗ - ╚══██╔══╝ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██║ ████╗ ██║ ██╔════╝ - ██║ █████╗ ███████╗ ██║ ██║ ██╔██╗ ██║ ██║ ███╗ - ██║ ██╔══╝ ╚════██║ ██║ ██║ ██║╚██╗██║ ██║ ██║ - ██║ ███████╗ ███████║ ██║ ██║ ██║ ╚████║ ╚██████╔╝ - ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ -`); - -Object.entries(TEST).forEach(async ([name, test]) => await test()); - -const ExitHandler = (exiting, error) => { - if (error && error !== 1) { - console.error(error); - } - - console.info(); - - if (!pass) { - process.exit(1); - } else { - //now exit with a smile :) - process.exit(0); - } -}; - -process.on('exit', ExitHandler); -process.on('SIGINT', ExitHandler); -process.on('uncaughtException', ExitHandler);