├── .circleci └── config.yml ├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── benches.yml │ ├── rust_deploy.yml │ └── rust_tests.yml ├── .gitignore ├── CHECKS ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Procfile ├── README.md ├── battlesnake-minimax ├── Cargo.toml └── src │ ├── lazy_smp.rs │ ├── lib.rs │ └── paranoid │ ├── cached_score.rs │ ├── eval.rs │ ├── minimax_return.rs │ ├── mod.rs │ ├── move_ordering.rs │ └── score.rs ├── battlesnake-rs ├── Cargo.toml ├── benches │ ├── a-prime.rs │ ├── devin.rs │ ├── flood-fill.rs │ ├── hobbs.rs │ └── improbable_irene.rs ├── fixtures │ ├── a-prime-food-maze.json │ ├── check_board_doubled_up.json │ └── start_of_game.json └── src │ ├── a_prime.rs │ ├── amphibious_arthur.rs │ ├── bombastic_bob.rs │ ├── constant_carter.rs │ ├── devious_devin_eval.rs │ ├── eremetic_eric.rs │ ├── famished_frank.rs │ ├── flood_fill │ ├── jump_flooding.rs │ ├── mod.rs │ ├── spread_from_head.rs │ └── spread_from_head_arcade_maze.rs │ ├── gigantic_george.rs │ ├── hovering_hobbs.rs │ ├── improbable_irene.rs │ ├── jump_flooding_snake.rs │ └── lib.rs ├── fixtures ├── 095b30fa-f2c7-4826-ac93-90b4dde6b785_5.json ├── 095b30fa-f2c7-4826-ac93-90b4dde6b785_6.json ├── 130b18e2-8689-4d64-a09f-c4345f80ae79_25.json ├── 45e7de53-bca5-4fa3-8771-d9914ed141bb.json ├── 4f198c01-d613-4109-b8b9-226208cde009_505.json ├── 65401e8f-a92a-445f-9617-94770044e117.json ├── 6d9cd0b1-6829-4430-926c-562918397774_101.json ├── 7311099d-b98a-4589-9b05-32dc80362bcc_135.json ├── 7a02e19b-f658-4639-8ace-ece46629a6ed_192.json ├── 95d72d73-352b-4ad5-83e4-86139fa556a9_54.json ├── af943832-1b3b-4795-9e35-081f71959aee_108.json ├── arcade_maze_end_game_duels.json ├── arcade_maze_should_win.json ├── b6a045ae-abf2-4f6f-b04c-a80ace7881b4_399.json ├── c2aee0d9-30dc-47ee-bd25-38e67e0fee9d_96.json ├── d9841bf6-c34f-42fb-8818-dfd5d5a09b4a_125.json ├── df732ab7-7e22-41d8-b651-95bb912e45ab.json ├── less_basic_expand_mcts.json └── mojave_12_18_12_34.json ├── fly.toml ├── hurl_tests ├── end_constant_carter.hurl ├── fixtures │ └── start_of_game.json ├── graph_mcts.hurl ├── info_constant_carter.hurl ├── move_constant_carter.hurl ├── move_mcts.hurl ├── start_bombastic_bob.hurl └── start_constant_carter.hurl ├── rust-toolchain.toml ├── script ├── auto.sh ├── average_of_ten_runs.sh ├── duels.rb └── hurl ├── sherlock ├── Cargo.toml ├── README.md └── src │ ├── commands.rs │ ├── commands │ ├── archive.rs │ ├── archive_snake.rs │ ├── archive_user.rs │ ├── fixture.rs │ ├── replay.rs │ └── solve.rs │ ├── main.rs │ ├── unofficial_api.rs │ └── websockets.rs ├── web-axum ├── Cargo.toml └── src │ ├── hobbs.rs │ └── main.rs ├── web-lambda ├── Cargo.toml ├── scripts │ ├── build.sh │ └── deploy.sh ├── src │ └── main.rs └── template.yml └── web-rocket ├── Cargo.toml └── src └── main.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | */target/ 4 | archive/ 5 | 6 | sherlock/tmp 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: coreyja 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/benches.yml: -------------------------------------------------------------------------------- 1 | name: Rust Benchmarks and Profiles 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | bench: 7 | name: Bench 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup | Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup | Rust 13 | uses: ATiltedTree/setup-rust@v1 14 | with: 15 | rust-version: nightly 16 | - uses: awalsh128/cache-apt-pkgs-action@latest 17 | with: 18 | packages: protobuf-compiler 19 | version: v0 20 | - uses: Swatinem/rust-cache@v2 21 | with: 22 | prefix-key: "v0-rust" 23 | - name: Bench 24 | run: cargo bench --locked 25 | - name: Archive Bench Statistics 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: criterion-bench 29 | path: target/criterion 30 | profile: 31 | name: Profile 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Setup | Checkout 35 | uses: actions/checkout@v3 36 | - name: Setup | Rust 37 | uses: ATiltedTree/setup-rust@v1 38 | with: 39 | rust-version: nightly 40 | - uses: awalsh128/cache-apt-pkgs-action@latest 41 | with: 42 | packages: protobuf-compiler 43 | version: v0 44 | - uses: Swatinem/rust-cache@v2 45 | with: 46 | prefix-key: "v0-rust" 47 | - name: Profile Hobbs 48 | run: cargo bench --bench hobbs -- --profile-time 60 49 | - name: Profile Irene 50 | run: cargo bench --bench improbable_irene -- --profile-time 60 51 | - name: Archive Profile Statistics 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: criterion-profile 55 | path: target/criterion 56 | -------------------------------------------------------------------------------- /.github/workflows/rust_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Terrarium 2 | concurrency: deploy 3 | 4 | run-name: Deploy Terrarium @ ${{ github.event.workflow_run.head_sha }} 5 | 6 | on: 7 | workflow_run: 8 | workflows: ["Rust Test"] 9 | types: [completed] 10 | branches: "main" 11 | 12 | jobs: 13 | build: 14 | name: Build Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Fail Deploy if Tests Failed 18 | run: exit 1 19 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 20 | - name: Setup | Checkout 21 | with: 22 | ref: ${{ github.event.workflow_run.head_sha }} 23 | uses: actions/checkout@v3 24 | - name: Setup | Rust 25 | uses: ATiltedTree/setup-rust@v1 26 | with: 27 | rust-version: nightly 28 | - uses: awalsh128/cache-apt-pkgs-action@latest 29 | with: 30 | packages: protobuf-compiler 31 | version: v0 32 | - uses: Swatinem/rust-cache@v2 33 | with: 34 | prefix-key: "v0-rust" 35 | - name: Build 36 | run: cargo build --release --all-targets 37 | - name: Upload Releases 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: releases 41 | path: | 42 | target/release/web-axum 43 | target/release/web-rocket 44 | target/release/web-lambda 45 | target/release/sherlock 46 | -------------------------------------------------------------------------------- /.github/workflows/rust_tests.yml: -------------------------------------------------------------------------------- 1 | name: Rust Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Setup | Checkout 15 | uses: actions/checkout@v3 16 | - uses: awalsh128/cache-apt-pkgs-action@latest 17 | with: 18 | packages: protobuf-compiler 19 | version: v0 20 | - name: Setup | Rust 21 | uses: ATiltedTree/setup-rust@v1 22 | with: 23 | rust-version: nightly 24 | components: clippy 25 | - uses: Swatinem/rust-cache@v2 26 | with: 27 | prefix-key: "v0-rust" 28 | - name: Build | Lint 29 | run: cargo clippy --all-targets --no-deps --locked 30 | doc: 31 | name: Doc 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Setup | Checkout 35 | uses: actions/checkout@v3 36 | - uses: awalsh128/cache-apt-pkgs-action@latest 37 | with: 38 | packages: protobuf-compiler 39 | version: v0 40 | - name: Setup | Rust 41 | uses: ATiltedTree/setup-rust@v1 42 | with: 43 | rust-version: nightly 44 | - uses: Swatinem/rust-cache@v2 45 | with: 46 | prefix-key: "v0-rust" 47 | - name: Cargo Doc 48 | run: cargo doc --workspace --no-deps --locked 49 | test: 50 | name: Test 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Setup | Checkout 54 | uses: actions/checkout@v3 55 | - name: Setup | Rust 56 | uses: ATiltedTree/setup-rust@v1 57 | with: 58 | rust-version: nightly 59 | - uses: awalsh128/cache-apt-pkgs-action@latest 60 | with: 61 | packages: protobuf-compiler 62 | version: v0 63 | - uses: Swatinem/rust-cache@v2 64 | with: 65 | prefix-key: "v0-rust" 66 | - name: Test 67 | run: cargo test --locked 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | **/target 4 | tmp 5 | 6 | */samconfig.toml 7 | 8 | /archive 9 | /archive/* 10 | 11 | .idea/ 12 | .idea/**/* 13 | -------------------------------------------------------------------------------- /CHECKS: -------------------------------------------------------------------------------- 1 | WAIT=1 2 | TIMEOUT=5 3 | ATTEMPTS=60 4 | 5 | /amphibious-arthur coreyja 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "battlesnake-rs", 6 | "battlesnake-minimax", 7 | "web-lambda", 8 | "web-rocket", 9 | "web-axum", 10 | "sherlock", 11 | ] 12 | 13 | [workspace.dependencies] 14 | battlesnake-game-types = { git = "https://github.com/fables-tales/battlesnake-game-types.git", branch = "ca/main/stacked-hazards" } 15 | 16 | # Config for 'cargo dist' 17 | [workspace.metadata.dist] 18 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 19 | cargo-dist-version = "0.0.3-prerelease04" 20 | # The preferred Rust toolchain to use in CI (rustup toolchain syntax) 21 | rust-toolchain-version = "1.67.1" 22 | # CI backends to support (see 'cargo dist generate-ci') 23 | ci = ["github"] 24 | # Target platforms to build apps for (Rust target-triple syntax) 25 | targets = [ 26 | "x86_64-unknown-linux-gnu", 27 | "x86_64-apple-darwin", 28 | "x86_64-pc-windows-msvc", 29 | ] 30 | 31 | # The profile that 'cargo dist' will build with 32 | [profile.dist] 33 | inherits = "release" 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | 3 | WORKDIR /home/rust/ 4 | 5 | USER root 6 | 7 | COPY rust-toolchain.toml . 8 | 9 | RUN rustc --version; cargo --version; rustup --version 10 | 11 | RUN apt update && apt install cmake protobuf-compiler -y 12 | 13 | # Avoid having to install/build all dependencies by copying 14 | # the Cargo files and making a dummy src/main.rs 15 | COPY Cargo.toml . 16 | COPY Cargo.lock . 17 | COPY battlesnake-rs/Cargo.toml ./battlesnake-rs/ 18 | COPY battlesnake-minimax/Cargo.toml ./battlesnake-minimax/ 19 | COPY web-rocket/Cargo.toml ./web-rocket/ 20 | COPY web-lambda/Cargo.toml ./web-lambda/ 21 | COPY web-axum/Cargo.toml ./web-axum/ 22 | COPY sherlock/Cargo.toml ./sherlock/ 23 | RUN mkdir -p ./battlesnake-rs/src/ && echo "fn foo() {}" > ./battlesnake-rs/src/lib.rs 24 | RUN mkdir -p ./battlesnake-minimax/src/ && echo "fn foo() {}" > ./battlesnake-minimax/src/lib.rs 25 | RUN mkdir -p ./web-rocket/src/ && echo "fn main() {}" > ./web-rocket/src/main.rs 26 | RUN mkdir -p ./web-lambda/src/ && echo "fn main() {}" > ./web-lambda/src/main.rs 27 | RUN mkdir -p ./web-axum/src/ && echo "fn main() {}" > ./web-axum/src/main.rs 28 | RUN mkdir -p ./sherlock/src/ && echo "fn main() {}" > ./sherlock/src/main.rs 29 | RUN cargo build --release --locked --bin web-axum 30 | 31 | # We need to touch our real main.rs file or else docker will use 32 | # the cached one. 33 | COPY . . 34 | RUN touch battlesnake-minimax/src/lib.rs && \ 35 | touch battlesnake-rs/src/lib.rs && \ 36 | touch web-axum/src/main.rs && \ 37 | touch web-rocket/src/main.rs 38 | 39 | RUN cargo build --release --locked --bin web-axum 40 | 41 | # Start building the final image 42 | FROM debian:stable-slim 43 | WORKDIR /home/rust/ 44 | COPY --from=builder /home/rust/target/release/web-axum . 45 | 46 | ENV JSON_LOGS=1 47 | ENV PORT=8000 48 | 49 | EXPOSE 8000 50 | 51 | ENTRYPOINT ["./web-axum"] 52 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: env ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/web-rocket 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # battlesnake-rs 2 | 3 | This project holds my Snakes which play on play.battlesnake.com! 4 | 5 | ## Snakes 6 | 7 | ### [Amphibious Arthur](https://play.battlesnake.com/u/coreyja/amphibious-arthur/) 8 | 9 | Arthur was my first snake! He was originally built to be more sole play based, but adopted some 10 | multi-player smarts eventually. 11 | 12 | He is a hand-rolled tree-search based snake. He looks at his possible moves and scores them. The 13 | scoring function works by recursively looking a set number of moves ahead and summing all the 14 | scores. From these possible moves he picks the one with the highest score. 15 | 16 | ### [Bombastic Bob](https://play.battlesnake.com/u/coreyja/bombastic-bob/) 17 | 18 | Bob just goes in a random 'reasonable' direction each time. He won't dive into an existing snake or 19 | off the board, but is happy to run into a head to head. 20 | 21 | ### [Constant Carter](https://play.battlesnake.com/u/coreyja/constant-carter/) 22 | 23 | Carter just goes right. Thats all. Mostly created to test latency between the Battlesnake Server 24 | and my snake server. 25 | 26 | ### [Devious Devin](https://play.battlesnake.com/u/coreyja/devious-devin/) 27 | 28 | AKA: Business Snail 29 | 30 | Devin is my first minimax snake! Has a pretty highly tuned minimax implementation with an scoring 31 | algorithm that is mostly based on the distance to either food or the closest opponents head. 32 | 33 | My current best 'competitive' snake at the moment! Most recently he was invited and competed in the 34 | [Elite Division Winter Classic Invitational 2021](https://play.battlesnake.com/competitions/fall-league-2021/fall-league-2021-elite/brackets/) 35 | 36 | ### [Eremetic Eric](https://play.battlesnake.com/u/coreyja/eremetic-eric/) 37 | 38 | #### Strategy 39 | 40 | Eric is a single player snake. His goal is to survive as many turns as possible. 41 | 42 | To do this he does a lot of tail chasing. The high level strategy goes something like the following: 43 | 44 | - Use A* to find the shortest path to our tail 45 | - Use this path to pretend the snake is a complete 'circle', IE: Change the board such that our snake contains all pieces from the A* search we just did 46 | - If our health is greater than the 'completed circle' snake length, keep circling until we are low on health 47 | - When we are low on health, look for the "best" food item, where best is defined as follows: 48 | - We can get to this food with the lowest possible health, such that we prioritize using as much health as possible 49 | - Once we have gotten determined which food is best, we determine which body piece is where we should 'exit' our circle to grab the food. This is the body piece where A* finds the shortest distance to the chosen food. 50 | - If we are at this chosen body piece, follow the A* prime to the food 51 | - If we are NOT at this chosen body piece, keep looping until we are 52 | 53 | Eric is open for games on play.battlesnake.com, so feel free to start a Solo game with him and watch what he does! 54 | 55 | ### [Famished Frank](https://play.battlesnake.com/u/coreyja/famished-frank/) 56 | 57 | Frank is famished... He needs food! Once he's full he goes for the corners 58 | 59 | Successfully completed the [Occupy 4 Corners Challenge](https://play.battlesnake.com/g/ee518016-997d-4fdf-9354-a73105876174/) 60 | 61 | ### [Gigantic George](https://play.battlesnake.com/u/coreyja/gigantic-george/) 62 | 63 | George wants to get as long as possible 64 | 65 | Successfully completed the [Maximum Snake Challenge](https://play.battlesnake.com/g/136ef25f-27b3-4adc-86a8-d57eb3b11877/) 66 | 67 | ### [Hovering Hobbs](https://play.battlesnake.com/u/coreyja/hovering-hobbs/) 68 | 69 | Hobbs is an area control snake. They take the minimax implementation from Devin, and combines it 70 | with a Flood Fill inspired algorithm to try and control more of the board than their opponents. 71 | 72 | Hobbs is brand new, and excited to compete in the arenas 73 | -------------------------------------------------------------------------------- /battlesnake-minimax/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "battlesnake-minimax" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | itertools = "0.10.0" 10 | debug_print = "1.0.0" 11 | tracing = "0.1.26" 12 | text_trees = "0.1.2" 13 | derivative = "2.2.0" 14 | dotavious = "0.2.1" 15 | dashmap = "5.3.4" 16 | rand = "0.8.5" 17 | fxhash = "0.2.1" 18 | color-eyre = "0.6.2" 19 | battlesnake-game-types = { workspace = true } 20 | 21 | [dev-dependencies] 22 | serde_json = "1.0.79" 23 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/lazy_smp.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, hash::Hash, sync::Arc, thread}; 2 | 3 | use battlesnake_game_types::{types::*, wire_representation::NestedGame}; 4 | use dashmap::DashMap; 5 | use derivative::Derivative; 6 | use fxhash::FxBuildHasher; 7 | use tracing::info_span; 8 | 9 | use crate::{ 10 | paranoid::{move_ordering::MoveOrdering, CachedScore, Scorable, SnakeOptions}, 11 | Instruments, ParanoidMinimaxSnake, 12 | }; 13 | 14 | #[derive(Derivative, Clone)] 15 | #[derivative(Debug)] 16 | #[allow(missing_docs)] 17 | pub struct LazySmpSnake 18 | where 19 | GameType: 'static + Hash + Eq + PartialEq + Copy + Sync + Send, 20 | ScoreType: 'static + Sync + Send + Clone, 21 | ScorableType: Scorable + Sized + Send + Sync + 'static + Clone, 22 | CachedScore: Scorable, 23 | { 24 | cache: Arc>, 25 | main_snake: ParanoidMinimaxSnake< 26 | GameType, 27 | ScoreType, 28 | CachedScore, 29 | N_SNAKES, 30 | >, 31 | background_snake: ParanoidMinimaxSnake< 32 | GameType, 33 | ScoreType, 34 | CachedScore, 35 | N_SNAKES, 36 | >, 37 | } 38 | 39 | impl 40 | LazySmpSnake 41 | where 42 | GameType: SnakeIDGettableGame 43 | + YouDeterminableGame 44 | + PositionGettableGame 45 | + HealthGettableGame 46 | + VictorDeterminableGame 47 | + HeadGettableGame 48 | + NeighborDeterminableGame 49 | + NeckQueryableGame 50 | + SimulableGame 51 | + Clone 52 | + Sync 53 | + Send 54 | + Sized 55 | + Eq 56 | + PartialEq 57 | + Hash 58 | + Copy, 59 | GameType::SnakeIDType: Clone + Send + Sync, 60 | ScoreType: 'static + Copy + Send + Sync + Ord + PartialOrd + Debug, 61 | ScorableType: Scorable + Sized + Send + Sync + 'static + Clone, 62 | { 63 | #[allow(missing_docs)] 64 | pub fn new( 65 | game: GameType, 66 | game_info: NestedGame, 67 | turn: i32, 68 | score_function: ScorableType, 69 | name: &'static str, 70 | options: SnakeOptions, 71 | ) -> Self { 72 | let cache: DashMap = Default::default(); 73 | let cache = Arc::new(cache); 74 | let cached_score = CachedScore::new(score_function, cache.clone()); 75 | 76 | let main_options = { 77 | let mut options = options; 78 | options.move_ordering = MoveOrdering::BestFirst; 79 | options 80 | }; 81 | 82 | let main_snake = ParanoidMinimaxSnake::new( 83 | game, 84 | game_info.clone(), 85 | turn, 86 | cached_score.clone(), 87 | name, 88 | main_options, 89 | ); 90 | 91 | let background_options = { 92 | let mut options = options; 93 | options.move_ordering = MoveOrdering::Random; 94 | options 95 | }; 96 | 97 | let background_snake = ParanoidMinimaxSnake::new( 98 | game, 99 | game_info, 100 | turn, 101 | cached_score, 102 | name, 103 | background_options, 104 | ); 105 | 106 | Self { 107 | cache, 108 | main_snake, 109 | background_snake, 110 | } 111 | } 112 | 113 | pub fn choose_move(&self) -> Move { 114 | info_span!( 115 | "lazy_smp", 116 | snake_name = self.main_snake.name, 117 | game_id = %&self.main_snake.game_info.id, 118 | turn = self.main_snake.turn, 119 | ruleset_name = %self.main_snake.game_info.ruleset.name, 120 | ruleset_version = %self.main_snake.game_info.ruleset.version, 121 | depth = tracing::field::Empty, 122 | ) 123 | .in_scope(|| { 124 | let num_background_snakes: usize = std::thread::available_parallelism() 125 | .map(|x| x.into()) 126 | .map(|x: usize| x / 2) 127 | .unwrap_or(1); 128 | 129 | for _ in 0..num_background_snakes { 130 | let snake = self.background_snake.clone(); 131 | thread::spawn(move || { 132 | snake.choose_move(); 133 | }); 134 | } 135 | 136 | let (m, depth) = self.main_snake.choose_move().unwrap(); 137 | let current_span = tracing::Span::current(); 138 | current_span.record("depth", depth); 139 | 140 | m 141 | }) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | warnings, 3 | missing_copy_implementations, 4 | missing_debug_implementations, 5 | missing_docs 6 | )] 7 | //! This crate implements the minimax algorithm for the battlesnake game. You provide a 'scoring' 8 | //! function that turns a given board into anything that implements the `Ord` trait. 9 | //! 10 | //! There are multiple variants to multiplayer minimax. This crate currently only supports the 11 | //! `paranoid` variant, which can be found in the [paranoid] module 12 | //! For more information check out my [Minimax Blog Post](https://coreyja.com/BattlesnakeMinimax/Minimax%20in%20Battlesnake/) 13 | //! 14 | //! We lean on the [types] crate for the game logic, and in particular for the 15 | //! simulate logic, which is used to generate the next board states. 16 | //! 17 | //! ```rust 18 | //! use std::time::Duration; 19 | //! use battlesnake_minimax::paranoid::{MinMaxReturn, MinimaxSnake, SnakeOptions}; 20 | //! use battlesnake_minimax::types::{types::build_snake_id_map, compact_representation::StandardCellBoard4Snakes11x11, wire_representation::Game}; 21 | //! 22 | //! // This fixture data matches what we expect to come from the Battlesnake Game Server 23 | //! let game_state_from_server = include_str!("../../battlesnake-rs/fixtures/start_of_game.json"); 24 | //! 25 | //! // First we take the JSON from the game server and construct a `Game` struct which 26 | //! // represents the 'wire' representation of the game state 27 | //! let wire_game: Game = serde_json::from_str(game_state_from_server).unwrap(); 28 | //! 29 | //! // The 'compact' representation of the game state doesn't include the game_info but we use 30 | //! // it for some of our tracing so we want to clone it before we create the compact representation 31 | //! let game_info = wire_game.game.clone(); 32 | //! 33 | //! let snake_id_map = build_snake_id_map(&wire_game); 34 | //! let compact_game = StandardCellBoard4Snakes11x11::convert_from_game(wire_game, &snake_id_map).unwrap(); 35 | //! 36 | //! // This is the scoring function that we will use to evaluate the game states 37 | //! // Here it just returns a constant but would ideally contain some logic to decide which 38 | //! // states are better than others 39 | //! fn score_function(board: &StandardCellBoard4Snakes11x11) -> i32 { 4 } 40 | //! 41 | //! // Optional settings for the snake 42 | //! let snake_options = SnakeOptions { 43 | //! network_latency_padding: Duration::from_millis(100), 44 | //! ..Default::default() 45 | //! }; 46 | //! 47 | //! 48 | //! let minimax_snake = MinimaxSnake::from_fn_with_options( 49 | //! compact_game, 50 | //! game_info, 51 | //! 0, 52 | //! &score_function, 53 | //! "minimax_snake", 54 | //! snake_options, 55 | //! ); 56 | //! 57 | //! // Now we can use the minimax snake to generate the next move! 58 | //! // Here we use the function [MinimaxSnake::deepened_minimax_until_timelimit] to run the minimax 59 | //! // algorithm until the time limit specified in the give game 60 | //! let result: MinMaxReturn<_, _> = minimax_snake.deepened_minimax_until_timelimit(snake_id_map.values().cloned().collect(), None).1; 61 | //! ``` 62 | 63 | pub use battlesnake_game_types as types; 64 | 65 | pub mod paranoid; 66 | 67 | pub use paranoid::MinimaxSnake as ParanoidMinimaxSnake; 68 | 69 | pub use dashmap; 70 | 71 | #[allow(missing_docs)] 72 | pub mod lazy_smp; 73 | 74 | /// The move output to be returned to the Battlesnake Engine 75 | #[derive(Debug, Clone)] 76 | pub struct MoveOutput { 77 | /// A stringified move 78 | pub r#move: String, 79 | /// An optional shout that will be echoed to you on your next turn 80 | pub shout: Option, 81 | } 82 | 83 | #[derive(Debug, Clone, Copy)] 84 | /// Any empty struct that implements `SimulatorInstruments` as a no-op which can be used when you don't want 85 | /// to time the simulation 86 | pub struct Instruments {} 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use battlesnake_game_types::{ 91 | compact_representation::{dimensions::Custom, WrappedCellBoard}, 92 | types::{build_snake_id_map, Move, SimulableGame, SnakeIDGettableGame}, 93 | wire_representation::Game, 94 | }; 95 | use itertools::Itertools; 96 | 97 | use crate::{ 98 | paranoid::{MinMaxReturn, MinimaxSnake, SnakeOptions, WrappedScore}, 99 | Instruments, 100 | }; 101 | 102 | #[test] 103 | fn it_finds_that_this_move_is_a_win() { 104 | let fixture = include_str!("../../fixtures/arcade_maze_should_win.json"); 105 | let wire_game: Game = serde_json::from_str(fixture).unwrap(); 106 | let snake_ids = build_snake_id_map(&wire_game); 107 | let game_info = wire_game.game.clone(); 108 | 109 | let game: WrappedCellBoard = wire_game 110 | .as_wrapped_cell_board(&snake_ids) 111 | .expect("Fixture data should be a valid game"); 112 | 113 | let explorer = MinimaxSnake::from_fn_with_options( 114 | game, 115 | game_info.clone(), 116 | 0, 117 | &|_| (), 118 | "explorer", 119 | SnakeOptions::default(), 120 | ); 121 | 122 | let result = explorer.deepend_minimax_to_turn(50); 123 | 124 | let mut next_moves = game.simulate(&Instruments {}, game.get_snake_ids()); 125 | let chosen_next = next_moves 126 | .find(|(action, _)| { 127 | (*action).into_inner() == [Some(Move::Down), Some(Move::Left), None, None] 128 | }) 129 | .unwrap(); 130 | 131 | let next_explorer = MinimaxSnake::from_fn(chosen_next.1, game_info, 0, &|_| (), "explorer"); 132 | let next_result = next_explorer.deepend_minimax_to_turn(100); 133 | let next_score = next_result.score(); 134 | 135 | assert!( 136 | matches!(next_score, WrappedScore::Win(_)), 137 | "The move after the move we are looking at should be a win, its score is {next_score:?}" 138 | ); 139 | 140 | let mut current = &result; 141 | while let MinMaxReturn::Node { 142 | options, 143 | moving_snake_id, 144 | .. 145 | } = current 146 | { 147 | let chosen_move = options.first().unwrap().0; 148 | let all_option_scores = options.iter().map(|(m, r)| (m, r.score())).collect_vec(); 149 | println!( 150 | "Moving Snake {moving_snake_id:?} move: {chosen_move} score: {:?} options: {all_option_scores:?}", 151 | current.score() 152 | ); 153 | current = &options.first().unwrap().1; 154 | } 155 | 156 | assert!( 157 | matches!(result.score(), WrappedScore::Win(_)), 158 | "This game should be a win but was {:?}", 159 | result.score() 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/paranoid/cached_score.rs: -------------------------------------------------------------------------------- 1 | use dashmap::DashMap; 2 | use fxhash::FxBuildHasher; 3 | 4 | use super::Scorable; 5 | 6 | use std::{hash::Hash, sync::Arc}; 7 | 8 | #[derive(Debug, Clone)] 9 | /// Cache the score 10 | pub struct CachedScore 11 | where 12 | ScorableType: Scorable, 13 | GameType: Eq + Hash + Copy, 14 | { 15 | scorable: ScorableType, 16 | cache: Arc>, 17 | _phantom: std::marker::PhantomData<(ScoreType, GameType)>, 18 | } 19 | 20 | impl CachedScore 21 | where 22 | ScorableType: Scorable, 23 | GameType: Eq + Hash + Copy, 24 | { 25 | /// Wrap the given scorable with a cache. We pass in a reference to the cache so that we can 26 | /// create multiple wrappers with a shared cache 27 | pub fn new( 28 | scorable: ScorableType, 29 | cache: Arc>, 30 | ) -> Self { 31 | Self { 32 | scorable, 33 | cache, 34 | _phantom: Default::default(), 35 | } 36 | } 37 | } 38 | 39 | impl Scorable 40 | for CachedScore 41 | where 42 | InnerScorableType: Scorable, 43 | GameType: Eq + Hash + Copy, 44 | ScoreType: Copy, 45 | { 46 | fn score(&self, game: &GameType) -> ScoreType { 47 | *self 48 | .cache 49 | .entry(*game) 50 | .or_insert_with(|| self.scorable.score(game)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/paranoid/mod.rs: -------------------------------------------------------------------------------- 1 | //! There are multiple multiplayer variations to minimax, this module is for the `paranoid` 2 | //! variant. [This is currently the only variant supported by this trait] 3 | //! 4 | //! This variant assumes all your opponents are working together to minimize your score. The 5 | //! implementation uses Alpha-Beta pruning to be efficient 6 | //! 7 | //! This variant works by always scoring nodes as 'yourself'. 8 | //! When propagating scores up the tree, it chooses the highest score when its your turn 9 | //! and the lowest score when its the opponent's turn. 10 | //! 11 | //! For more information check out my [Minimax Blog Post](https://coreyja.com/BattlesnakeMinimax/Minimax%20in%20Battlesnake/) 12 | //! 13 | //! ```rust 14 | //! use std::time::Duration; 15 | //! use battlesnake_minimax::paranoid::{MinMaxReturn, MinimaxSnake, SnakeOptions}; 16 | //! use battlesnake_game_types::{types::build_snake_id_map, compact_representation::StandardCellBoard4Snakes11x11, wire_representation::Game}; 17 | //! 18 | //! // This fixture data matches what we expect to come from the Battlesnake Game Server 19 | //! let game_state_from_server = include_str!("../../../battlesnake-rs/fixtures/start_of_game.json"); 20 | //! 21 | //! // First we take the JSON from the game server and construct a `Game` struct which 22 | //! // represents the 'wire' representation of the game state 23 | //! let wire_game: Game = serde_json::from_str(game_state_from_server).unwrap(); 24 | //! 25 | //! // The 'compact' representation of the game state doesn't include the game_info but we use 26 | //! // it for some of our tracing so we want to clone it before we create the compact representation 27 | //! let game_info = wire_game.game.clone(); 28 | //! 29 | //! let snake_id_map = build_snake_id_map(&wire_game); 30 | //! let compact_game = StandardCellBoard4Snakes11x11::convert_from_game(wire_game, &snake_id_map).unwrap(); 31 | //! 32 | //! // This is the scoring function that we will use to evaluate the game states 33 | //! // Here it just returns a constant but would ideally contain some logic to decide which 34 | //! // states are better than others 35 | //! fn score_function(board: &StandardCellBoard4Snakes11x11) -> i32 { 4 } 36 | //! 37 | //! // Optional settings for the snake 38 | //! let snake_options = SnakeOptions { 39 | //! network_latency_padding: Duration::from_millis(100), 40 | //! ..Default::default() 41 | //! }; 42 | //! 43 | //! 44 | //! let minimax_snake = MinimaxSnake::from_fn_with_options( 45 | //! compact_game, 46 | //! game_info, 47 | //! 0, 48 | //! &score_function, 49 | //! "minimax_snake", 50 | //! snake_options, 51 | //! ); 52 | //! 53 | //! // Now we can use the minimax snake to generate the next move! 54 | //! // Here we use the function [MinimaxSnake::deepened_minimax_until_timelimit] to run the minimax 55 | //! // algorithm until the time limit specified in the give game 56 | //! let result: MinMaxReturn<_, _> = minimax_snake.deepened_minimax_until_timelimit(snake_id_map.values().cloned().collect(), None).1; 57 | //! ``` 58 | 59 | mod score; 60 | pub use score::{Scorable, WrappedScorable, WrappedScore}; 61 | 62 | mod minimax_return; 63 | pub use minimax_return::MinMaxReturn; 64 | 65 | mod eval; 66 | pub use eval::{MinimaxSnake, SnakeOptions}; 67 | 68 | mod cached_score; 69 | pub use cached_score::CachedScore; 70 | 71 | #[allow(missing_docs)] 72 | pub mod move_ordering; 73 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/paranoid/move_ordering.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use itertools::Itertools; 4 | use rand::seq::SliceRandom; 5 | use rand::thread_rng; 6 | 7 | use battlesnake_game_types::types::{Move, SnakeIDGettableGame}; 8 | 9 | use super::MinMaxReturn; 10 | 11 | #[derive(Debug, Clone, Copy)] 12 | pub enum MoveOrdering { 13 | BestFirst, 14 | Random, 15 | } 16 | 17 | fn best_first( 18 | previous_return: Option>, 19 | possible_moves: impl Iterator, 20 | ) -> Vec<(Move, Option>)> 21 | where 22 | GameType: Debug + Clone + SnakeIDGettableGame, 23 | ScoreType: Copy + Ord + PartialOrd + Debug, 24 | { 25 | if let Some(MinMaxReturn::Node { mut options, .. }) = previous_return { 26 | let mut v: Vec<_> = possible_moves 27 | .into_iter() 28 | .map(|m| { 29 | ( 30 | m, 31 | options 32 | .iter() 33 | .position(|x| x.0 == m) 34 | .map(|x| options.remove(x).1), 35 | ) 36 | }) 37 | .collect(); 38 | v.sort_by_cached_key(|(_, r)| r.as_ref().map(|x| *x.score())); 39 | v.reverse(); 40 | v 41 | } else { 42 | possible_moves.into_iter().map(|m| (m, None)).collect() 43 | } 44 | } 45 | 46 | impl MoveOrdering { 47 | pub fn order_moves( 48 | &self, 49 | previous_return: Option>, 50 | possible_moves: impl Iterator, 51 | ) -> Vec<(Move, Option>)> 52 | where 53 | GameType: Debug + Clone + SnakeIDGettableGame, 54 | ScoreType: Copy + Ord + PartialOrd + Debug, 55 | { 56 | match &self { 57 | MoveOrdering::BestFirst => best_first(previous_return, possible_moves), 58 | MoveOrdering::Random => { 59 | let mut moves = possible_moves.map(|x| (x, None)).collect_vec(); 60 | moves.shuffle(&mut thread_rng()); 61 | 62 | moves 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /battlesnake-minimax/src/paranoid/score.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Reverse, fmt::Debug}; 2 | 3 | use battlesnake_game_types::types::{ 4 | HealthGettableGame, VictorDeterminableGame, YouDeterminableGame, 5 | }; 6 | 7 | #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Copy)] 8 | /// The wrapped score type. This takes into account the score provided by the score function, but 9 | /// wraps it with a Score based on the game state. This allows us to say that wins are better than 10 | /// any score and loses are worse than any score, etc. 11 | pub enum WrappedScore 12 | where 13 | ScoreType: PartialOrd + Ord + Debug + Clone + Copy, 14 | { 15 | /// We lost 16 | /// The first value is the number of snakes left alive 17 | /// And then the depth 18 | /// Such that we prefer where less snales are alive, and deeper depths 19 | Lose(Reverse, i64), 20 | /// We tied 21 | /// The first value is the number of snakes left alive 22 | /// And then the depth 23 | /// Such that we prefer where less snales are alive, and deeper depths 24 | Tie(Reverse, i64), 25 | /// We order this based on the score provided by the score function 26 | Scored(ScoreType), 27 | /// We won, the depth is recorded because we prefer winning sooner 28 | Win(Reverse), 29 | } 30 | 31 | const LOWEST_DEPTH: i64 = std::i64::MIN; 32 | 33 | impl WrappedScore 34 | where 35 | ScoreType: PartialOrd + Ord + Debug + Clone + Copy, 36 | { 37 | /// Returns the best possible score 38 | /// 39 | /// This is a Win with the depth set as the maximum i64 such that no WrappedScore can be higher 40 | /// than this given the Ord 41 | pub fn best_possible_score() -> Self { 42 | WrappedScore::Win(Reverse(LOWEST_DEPTH)) 43 | } 44 | 45 | /// Returns the worst possible score 46 | /// 47 | /// This is a Lost with the depth set as the minimum i64 such that no WrappedScore can be higher 48 | /// than this given the Ord 49 | pub fn worst_possible_score() -> Self { 50 | WrappedScore::Lose(Reverse(u8::MAX), LOWEST_DEPTH) 51 | } 52 | 53 | /// Returns the depth from this score IFF the score is a terminal node. Otherwise returns None 54 | pub fn terminal_depth(&self) -> Option { 55 | match &self { 56 | Self::Win(Reverse(d)) => Some(*d), 57 | Self::Tie(_, d) | Self::Lose(_, d) => Some(*d), 58 | _ => None, 59 | } 60 | } 61 | } 62 | 63 | /// This trait is used to control something that can return a score from a game board 64 | /// 65 | /// We use this trait to be able to layer in different scoring approaches, such as caching 66 | pub trait Scorable { 67 | /// Convert the given GameType into a ScoreType 68 | fn score(&self, game: &GameType) -> ScoreType; 69 | } 70 | 71 | impl ScoreType> Scorable 72 | for FnLike 73 | { 74 | fn score(&self, game: &GameType) -> ScoreType { 75 | (self)(game) 76 | } 77 | } 78 | 79 | /// Provides an implementation for `wrapped_score` if the implementer implements the `score` 80 | /// function. 81 | /// 82 | /// `wrapped_score` takes into account if the node is an end_state, and depth based ordering so 83 | /// that the underlying scoring functions don't need to worry about this 84 | pub trait WrappedScorable 85 | where 86 | ScoreType: PartialOrd + Ord + Copy + Debug, 87 | GameType: YouDeterminableGame + VictorDeterminableGame + HealthGettableGame, 88 | { 89 | /// This is the the scoring function for your Minimax snake 90 | /// 91 | /// The score for all non end state nodes will be defined by this score 92 | fn score(&self, node: &GameType) -> ScoreType; 93 | 94 | /// `wrapped_score` takes into account the depth and number of players. It checks the game 95 | /// board and decides if this is a leaf in our Minimax tree. If it IS a leaf we score it based 96 | /// on the outcome of the game board. If we've hit the maximum depth, we use the scoring 97 | /// function provided by `score` 98 | fn wrapped_score( 99 | &self, 100 | node: &GameType, 101 | depth: i64, 102 | max_depth: i64, 103 | num_players: i64, 104 | ) -> Option> { 105 | if depth % num_players != 0 { 106 | return None; 107 | } 108 | 109 | let you_id = node.you_id(); 110 | 111 | if node.is_over() { 112 | let alive_count = node 113 | .get_snake_ids() 114 | .iter() 115 | .filter(|id| node.is_alive(id)) 116 | .count() as u8; 117 | 118 | let score = match node.get_winner() { 119 | Some(s) => { 120 | if s == *you_id { 121 | WrappedScore::Win(Reverse(depth)) 122 | } else { 123 | WrappedScore::Lose(Reverse(alive_count), depth) 124 | } 125 | } 126 | None => WrappedScore::Tie(Reverse(alive_count), depth), 127 | }; 128 | 129 | return Some(score); 130 | } 131 | 132 | if depth >= max_depth { 133 | return Some(WrappedScore::Scored(self.score(node))); 134 | } 135 | 136 | None 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /battlesnake-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "battlesnake-rs" 3 | version = "0.1.0" 4 | authors = ["Corey Alexander "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | serde = "1.0" 11 | serde_json = "1.0" 12 | serde_derive = "1.0" 13 | rand = "0.8" 14 | itertools = "0.10.0" 15 | debug_print = "1.0.0" 16 | tracing = "0.1.26" 17 | 18 | rustc-hash = "1.1.0" 19 | text_trees = "0.1.2" 20 | decorum = "0.3.1" 21 | rayon = "1.5.1" 22 | tinyvec = { version = "1.5.1", features = ["alloc", "rustc_1_40"] } 23 | battlesnake-minimax = { path = "../battlesnake-minimax" } 24 | typed-arena = "2.0.1" 25 | atomic_float = "0.1.0" 26 | dotavious = "0.2.1" 27 | color-eyre = "0.6.2" 28 | 29 | battlesnake-game-types = { workspace = true } 30 | 31 | [dev-dependencies] 32 | criterion = { version = "0.4", features = ["html_reports"] } 33 | pprof = { git ="https://github.com/tikv/pprof-rs.git", rev = "a280c9e", features = ["flamegraph", "criterion"] } 34 | 35 | [lib] 36 | name = "battlesnake_rs" 37 | path = "src/lib.rs" 38 | 39 | [[bench]] 40 | name = "devin" 41 | harness = false 42 | path = "benches/devin.rs" 43 | 44 | [[bench]] 45 | name = "hobbs" 46 | harness = false 47 | path = "benches/hobbs.rs" 48 | 49 | [[bench]] 50 | name = "improbable_irene" 51 | harness = false 52 | path = "benches/improbable_irene.rs" 53 | 54 | [[bench]] 55 | name = "a-prime" 56 | harness = false 57 | path = "benches/a-prime.rs" 58 | 59 | [[bench]] 60 | name = "flood-fill" 61 | harness = false 62 | path = "benches/flood-fill.rs" 63 | -------------------------------------------------------------------------------- /battlesnake-rs/benches/a-prime.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::types::*; 2 | 3 | use battlesnake_rs::a_prime::{APrimeCalculable, ClosestFoodCalculable}; 4 | use battlesnake_rs::*; 5 | 6 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 7 | use pprof::criterion::{Output, PProfProfiler}; 8 | 9 | pub fn criterion_benchmark(c: &mut Criterion) { 10 | let mut g = c.benchmark_group("a-prime"); 11 | // g.bench_function("wire start_of_game", |b| { 12 | // let game_json = include_str!("../fixtures/start_of_game.json"); 13 | // let game: Game = serde_json::from_str(game_json).unwrap(); 14 | 15 | // b.iter(|| { 16 | // let game = black_box(&game); 17 | // game.shortest_distance(&game.you.head, &game.board.food, None) 18 | // }) 19 | // }); 20 | 21 | // g.bench_function("wire a-prime-food-maze", |b| { 22 | // let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 23 | // let game: Game = serde_json::from_str(game_json).unwrap(); 24 | 25 | // b.iter(|| { 26 | // let game = black_box(&game); 27 | // game.shortest_distance(&game.you.head, &game.board.food, None) 28 | // }) 29 | // }); 30 | 31 | g.bench_function("compact start_of_game", |b| { 32 | let game_json = include_str!("../fixtures/start_of_game.json"); 33 | let game: Game = serde_json::from_str(game_json).unwrap(); 34 | 35 | let id_map = build_snake_id_map(&game); 36 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 37 | 38 | b.iter(|| { 39 | let game = black_box(&game); 40 | game.shortest_distance( 41 | &game.get_head_as_native_position(game.you_id()), 42 | &game.get_all_food_as_native_positions(), 43 | None, 44 | ) 45 | }) 46 | }); 47 | 48 | g.bench_function("compact a-prime-food-maze", |b| { 49 | let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 50 | let game: Game = serde_json::from_str(game_json).unwrap(); 51 | 52 | let id_map = build_snake_id_map(&game); 53 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 54 | 55 | b.iter(|| { 56 | let game = black_box(&game); 57 | game.shortest_distance( 58 | &game.get_head_as_native_position(game.you_id()), 59 | &game.get_all_food_as_native_positions(), 60 | None, 61 | ) 62 | }) 63 | }); 64 | 65 | g.bench_function("compact specialized start_of_game", |b| { 66 | let game_json = include_str!("../fixtures/start_of_game.json"); 67 | let game: Game = serde_json::from_str(game_json).unwrap(); 68 | 69 | let id_map = build_snake_id_map(&game); 70 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 71 | 72 | b.iter(|| { 73 | let game = black_box(&game); 74 | game.dist_to_closest_food(&game.get_head_as_native_position(game.you_id()), None) 75 | }) 76 | }); 77 | 78 | g.bench_function("compact specialized a-prime-food-maze", |b| { 79 | let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 80 | let game: Game = serde_json::from_str(game_json).unwrap(); 81 | 82 | let id_map = build_snake_id_map(&game); 83 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 84 | 85 | b.iter(|| { 86 | let game = black_box(&game); 87 | game.dist_to_closest_food(&game.get_head_as_native_position(game.you_id()), None) 88 | }) 89 | }); 90 | } 91 | 92 | criterion_group! { 93 | name = benches; 94 | config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); 95 | targets = criterion_benchmark 96 | } 97 | criterion_main!(benches); 98 | -------------------------------------------------------------------------------- /battlesnake-rs/benches/devin.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::{ 2 | compact_representation::StandardCellBoard4Snakes11x11, types::build_snake_id_map, 3 | wire_representation::Game, 4 | }; 5 | use battlesnake_minimax::paranoid::Scorable; 6 | 7 | use battlesnake_rs::{ 8 | devious_devin_eval::{score, ScoreEndState}, 9 | MinimaxSnake, 10 | }; 11 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 12 | use pprof::criterion::{Output, PProfProfiler}; 13 | 14 | fn create_snake( 15 | game: Game, 16 | ) -> MinimaxSnake< 17 | StandardCellBoard4Snakes11x11, 18 | ScoreEndState, 19 | impl Scorable + Clone, 20 | 4, 21 | > { 22 | let game_info = game.game.clone(); 23 | let turn = game.turn; 24 | let id_map = build_snake_id_map(&game); 25 | 26 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 27 | 28 | MinimaxSnake::from_fn(game, game_info, turn, &score, "devin") 29 | } 30 | 31 | fn bench_minmax_to_turn(c: &mut Criterion, max_turns: usize) { 32 | let game_json = include_str!("../fixtures/start_of_game.json"); 33 | 34 | let mut group = c.benchmark_group(format!("Devin: Turns {max_turns}")); 35 | 36 | group.bench_function("compact eval-minmax", |b| { 37 | b.iter(|| { 38 | let game_state: Game = serde_json::from_str(game_json).unwrap(); 39 | let devin = create_snake(black_box(game_state)); 40 | devin.single_minimax(max_turns) 41 | }) 42 | }); 43 | 44 | group.bench_function("compact eval-minmax iterative deepened", |b| { 45 | b.iter(|| { 46 | let game_state: Game = serde_json::from_str(game_json).unwrap(); 47 | let devin = create_snake(black_box(game_state)); 48 | devin.deepend_minimax_to_turn(max_turns) 49 | }) 50 | }); 51 | 52 | // group.bench_function("wire eval-minmax", |b| { 53 | // b.iter(|| { 54 | // let game_state: Game = serde_json::from_str(game_json).unwrap(); 55 | // let devin = { 56 | // let game = black_box(game_state); 57 | // let game_info = game.game.clone(); 58 | // let turn = game.turn; 59 | // let id_map = build_snake_id_map(&game); 60 | 61 | // MinimaxSnake::from_fn(game, game_info, turn, &score, "devin") 62 | // }; 63 | // devin.single_minimax(max_turns) 64 | // }) 65 | // }); 66 | 67 | // group.bench_function("wire eval-minmax iterative deepened", |b| { 68 | // b.iter(|| { 69 | // let game_state: Game = serde_json::from_str(game_json).unwrap(); 70 | // let devin = { 71 | // let game = black_box(game_state); 72 | // let game_info = game.game.clone(); 73 | // let turn = game.turn; 74 | // let id_map = build_snake_id_map(&game); 75 | 76 | // MinimaxSnake::from_fn(game, game_info, turn, &score, "devin") 77 | // }; 78 | // devin.deepend_minimax_to_turn(max_turns) 79 | // }) 80 | // }); 81 | 82 | group.finish(); 83 | } 84 | 85 | pub fn criterion_benchmark(c: &mut Criterion) { 86 | bench_minmax_to_turn(c, 3); 87 | } 88 | 89 | criterion_group! { 90 | name = benches; 91 | config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); 92 | targets = criterion_benchmark 93 | } 94 | criterion_main!(benches); 95 | -------------------------------------------------------------------------------- /battlesnake-rs/benches/flood-fill.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::{ 2 | types::*, 3 | wire_representation::{Game, Ruleset}, 4 | }; 5 | 6 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 7 | use pprof::criterion::{Output, PProfProfiler}; 8 | 9 | pub fn criterion_benchmark(c: &mut Criterion) { 10 | let mut g = c.benchmark_group("Flood Fill"); 11 | 12 | g.bench_function("compact spread", |b| { 13 | use battlesnake_rs::flood_fill::spread_from_head::SpreadFromHead; 14 | 15 | let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 16 | let game: Game = serde_json::from_str(game_json).unwrap(); 17 | 18 | let id_map = build_snake_id_map(&game); 19 | let game = battlesnake_game_types::compact_representation::StandardCellBoard4Snakes11x11::convert_from_game( 20 | game, &id_map, 21 | ) 22 | .unwrap(); 23 | 24 | b.iter(|| -> [u8; 4] { 25 | let game = black_box(&game); 26 | game.squares_per_snake(5) 27 | }) 28 | }); 29 | 30 | g.bench_function("wrapped spread", |b| { 31 | use battlesnake_rs::flood_fill::spread_from_head::SpreadFromHead; 32 | 33 | let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 34 | let mut game: Game = serde_json::from_str(game_json).unwrap(); 35 | game.game.ruleset = Ruleset { 36 | name: "wrapped".to_string(), 37 | version: "1.0".to_string(), 38 | settings: None, 39 | }; 40 | 41 | let id_map = build_snake_id_map(&game); 42 | let game = battlesnake_game_types::compact_representation::WrappedCellBoard4Snakes11x11::convert_from_game( 43 | game, &id_map, 44 | ) 45 | .unwrap(); 46 | 47 | b.iter(|| -> [u8; 4] { 48 | let game = black_box(&game); 49 | game.squares_per_snake(5) 50 | }) 51 | }); 52 | 53 | g.bench_function("wrapped jump", |b| { 54 | use battlesnake_rs::flood_fill::jump_flooding::JumpFlooding; 55 | 56 | let game_json = include_str!("../fixtures/a-prime-food-maze.json"); 57 | let mut game: Game = serde_json::from_str(game_json).unwrap(); 58 | game.game.ruleset = Ruleset { 59 | name: "wrapped".to_string(), 60 | version: "1.0".to_string(), 61 | settings: None, 62 | }; 63 | 64 | let id_map = build_snake_id_map(&game); 65 | let game = battlesnake_game_types::compact_representation::WrappedCellBoard4Snakes11x11::convert_from_game( 66 | game, &id_map, 67 | ) 68 | .unwrap(); 69 | 70 | b.iter(|| { 71 | let game = black_box(&game); 72 | game.squares_per_snake() 73 | }) 74 | }); 75 | } 76 | 77 | criterion_group! { 78 | name = benches; 79 | config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); 80 | targets = criterion_benchmark 81 | } 82 | criterion_main!(benches); 83 | -------------------------------------------------------------------------------- /battlesnake-rs/benches/hobbs.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_minimax::paranoid::CachedScore; 2 | use battlesnake_rs::{MinimaxSnake, StandardCellBoard4Snakes11x11}; 3 | 4 | use battlesnake_game_types::{ 5 | compact_representation::{ 6 | dimensions::ArcadeMaze, WrappedCellBoard, WrappedCellBoard4Snakes11x11, 7 | }, 8 | types::build_snake_id_map, 9 | wire_representation::{Game, Ruleset}, 10 | }; 11 | 12 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 13 | use pprof::criterion::{Output, PProfProfiler}; 14 | 15 | use battlesnake_rs::hovering_hobbs::standard_score; 16 | 17 | pub fn criterion_benchmark(c: &mut Criterion) { 18 | { 19 | let mut g = c.benchmark_group("Hobbs/fixture: start_of_game.json"); 20 | let game_json = include_str!("../fixtures/start_of_game.json"); 21 | 22 | g.bench_function("Compact", |b| { 23 | b.iter(|| { 24 | let game: Game = serde_json::from_str(game_json).unwrap(); 25 | let game_info = game.game.clone(); 26 | let turn = game.turn; 27 | let id_map = build_snake_id_map(&game); 28 | 29 | let name = "hovering-hobbs"; 30 | let score_map = Default::default(); 31 | let cached_score = CachedScore::new(&standard_score::<_, _, 4>, score_map); 32 | 33 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 34 | 35 | let snake = MinimaxSnake::new( 36 | black_box(game), 37 | game_info, 38 | turn, 39 | cached_score, 40 | name, 41 | Default::default(), 42 | ); 43 | 44 | snake.deepend_minimax_to_turn(3) 45 | }) 46 | }); 47 | 48 | g.bench_function("Wrapped", |b| { 49 | b.iter(|| { 50 | let mut game: Game = serde_json::from_str(game_json).unwrap(); 51 | game.game.ruleset = Ruleset { 52 | name: "wrapped".to_string(), 53 | version: "1.0".to_string(), 54 | settings: None, 55 | }; 56 | let game_info = game.game.clone(); 57 | let turn = game.turn; 58 | let id_map = build_snake_id_map(&game); 59 | 60 | let name = "hovering-hobbs"; 61 | let score_map = Default::default(); 62 | let cached_score = CachedScore::new(&standard_score::<_, _, 4>, score_map); 63 | 64 | let game = WrappedCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 65 | 66 | let snake = MinimaxSnake::new( 67 | black_box(game), 68 | game_info, 69 | turn, 70 | cached_score, 71 | name, 72 | Default::default(), 73 | ); 74 | 75 | snake.deepend_minimax_to_turn(3) 76 | }); 77 | }); 78 | } 79 | 80 | { 81 | let mut g = c.benchmark_group("Hobbs/fixture: arcade_maze_end_game_duels.json"); 82 | let game_json = include_str!("../../fixtures/arcade_maze_end_game_duels.json"); 83 | 84 | g.bench_function("arcade-maze", |b| { 85 | b.iter(|| { 86 | let mut game: Game = serde_json::from_str(game_json).unwrap(); 87 | game.game.ruleset = Ruleset { 88 | name: "wrapped".to_string(), 89 | version: "1.0".to_string(), 90 | settings: None, 91 | }; 92 | let game_info = game.game.clone(); 93 | let turn = game.turn; 94 | let id_map = build_snake_id_map(&game); 95 | 96 | let name = "hovering-hobbs"; 97 | let score_map = Default::default(); 98 | let cached_score = CachedScore::new(&standard_score::<_, _, 4>, score_map); 99 | 100 | let game = WrappedCellBoard::::convert_from_game( 101 | game, &id_map, 102 | ) 103 | .unwrap(); 104 | 105 | let snake = MinimaxSnake::new( 106 | black_box(game), 107 | game_info, 108 | turn, 109 | cached_score, 110 | name, 111 | Default::default(), 112 | ); 113 | 114 | snake.deepend_minimax_to_turn(6) 115 | }); 116 | }); 117 | } 118 | } 119 | 120 | criterion_group! { 121 | name = benches; 122 | config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); 123 | targets = criterion_benchmark 124 | } 125 | criterion_main!(benches); 126 | -------------------------------------------------------------------------------- /battlesnake-rs/benches/improbable_irene.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_rs::{improbable_irene::ImprobableIrene, StandardCellBoard4Snakes11x11}; 2 | 3 | use battlesnake_game_types::{ 4 | compact_representation::WrappedCellBoard4Snakes11x11, 5 | types::build_snake_id_map, 6 | wire_representation::{Game, Ruleset}, 7 | }; 8 | use typed_arena::Arena; 9 | 10 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 11 | use pprof::criterion::{Output, PProfProfiler}; 12 | 13 | pub fn criterion_benchmark(c: &mut Criterion) { 14 | let mut g = c.benchmark_group("MCTS"); 15 | let game_json = include_str!("../fixtures/start_of_game.json"); 16 | 17 | g.bench_function("MCTS Compact", |b| { 18 | b.iter(|| { 19 | let game: Game = serde_json::from_str(game_json).unwrap(); 20 | let game_info = game.game.clone(); 21 | let id_map = build_snake_id_map(&game); 22 | 23 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 24 | 25 | let snake = ImprobableIrene::new(black_box(game), game_info, 0); 26 | 27 | let mut arena = Arena::new(); 28 | snake.mcts_bench(10000, &mut arena); 29 | }) 30 | }); 31 | 32 | g.bench_function("MCTS Wrapped", |b| { 33 | b.iter(|| { 34 | let mut game: Game = serde_json::from_str(game_json).unwrap(); 35 | game.game.ruleset = Ruleset { 36 | name: "wrapped".to_string(), 37 | version: "1.0".to_string(), 38 | settings: None, 39 | }; 40 | let game_info = game.game.clone(); 41 | let id_map = build_snake_id_map(&game); 42 | 43 | let game = WrappedCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 44 | 45 | let snake = ImprobableIrene::new(black_box(game), game_info, 0); 46 | 47 | let mut arena = Arena::new(); 48 | snake.mcts_bench(10000, &mut arena); 49 | }); 50 | }); 51 | } 52 | 53 | criterion_group! { 54 | name = benches; 55 | config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); 56 | targets = criterion_benchmark 57 | } 58 | criterion_main!(benches); 59 | -------------------------------------------------------------------------------- /battlesnake-rs/fixtures/a-prime-food-maze.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "200854", 4 | "ruleset": { "name": "standard", "version": "v.1.2.3" }, 5 | "timeout": 500 6 | }, 7 | "turn": 200, 8 | "you": { 9 | "health": 100, 10 | "id": "you", 11 | "name": "#22aa34", 12 | "body": [ 13 | { "x": 2, "y": 1 }, 14 | { "x": 1, "y": 1 }, 15 | { "x": 0, "y": 1 } 16 | ], 17 | "latency": null, 18 | "head": { "x": 2, "y": 1 }, 19 | "length": 3 20 | }, 21 | "board": { 22 | "food": [ 23 | { "x": 0, "y": 10 }, 24 | { "x": 10, "y": 9 }, 25 | { "x": 4, "y": 7 }, 26 | { "x": 10, "y": 6 }, 27 | { "x": 2, "y": 7 }, 28 | { "x": 1, "y": 5 } 29 | ], 30 | "hazards": [], 31 | "height": 11, 32 | "width": 11, 33 | "snakes": [ 34 | { 35 | "health": 100, 36 | "id": "you", 37 | "name": "#22aa34", 38 | "body": [ 39 | { "x": 2, "y": 1 }, 40 | { "x": 1, "y": 1 }, 41 | { "x": 0, "y": 1 } 42 | ], 43 | "head": { "x": 2, "y": 1 }, 44 | "latency": null, 45 | "length": 3 46 | }, 47 | { 48 | "health": 100, 49 | "id": "#FF17cf", 50 | "name": "#FF17cf", 51 | "body": [ 52 | { "x": 6, "y": 8 }, 53 | { "x": 6, "y": 7 }, 54 | { "x": 6, "y": 6 }, 55 | { "x": 5, "y": 6 }, 56 | { "x": 5, "y": 5 }, 57 | { "x": 5, "y": 4 }, 58 | { "x": 5, "y": 3 }, 59 | { "x": 4, "y": 3 }, 60 | { "x": 0, "y": 3 }, 61 | { "x": 1, "y": 3 }, 62 | { "x": 2, "y": 3 }, 63 | { "x": 3, "y": 3 } 64 | ], 65 | "head": { "x": 6, "y": 8 }, 66 | "latency": null, 67 | "length": 12 68 | }, 69 | { 70 | "health": 100, 71 | "id": "#FF7a17", 72 | "name": "#FF7a17", 73 | "body": [ 74 | { "x": 9, "y": 10 }, 75 | { "x": 9, "y": 9 }, 76 | { "x": 9, "y": 8 }, 77 | { "x": 9, "y": 7 }, 78 | { "x": 9, "y": 6 }, 79 | { "x": 9, "y": 5 }, 80 | { "x": 10, "y": 5 } 81 | ], 82 | "head": { "x": 9, "y": 10 }, 83 | "latency": null, 84 | "length": 7 85 | }, 86 | { 87 | "health": 100, 88 | "id": "#FFc049", 89 | "name": "#FFc049", 90 | "body": [ 91 | { "x": 5, "y": 8 }, 92 | { "x": 4, "y": 8 }, 93 | { "x": 3, "y": 8 }, 94 | { "x": 2, "y": 8 }, 95 | { "x": 1, "y": 8 }, 96 | { "x": 1, "y": 7 }, 97 | { "x": 1, "y": 6 } 98 | ], 99 | "head": { "x": 5, "y": 8 }, 100 | "latency": null, 101 | "length": 7 102 | } 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /battlesnake-rs/fixtures/check_board_doubled_up.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "latency": "96.305", 4 | "id": "fd43df63-ae6a-4bd8-ac87-59f5c15a89f8", 5 | "health": 100, 6 | "length": 5, 7 | "shout": "", 8 | "head": { "y": 5, "x": 5 }, 9 | "customizations": { 10 | "color": "#fc0398", 11 | "tail": "default", 12 | "head": "trans-rights-scarf" 13 | }, 14 | "body": [ 15 | { "y": 5, "x": 5 }, 16 | { "y": 5, "x": 6 }, 17 | { "y": 6, "x": 6 }, 18 | { "y": 6, "x": 5 }, 19 | { "y": 6, "x": 5 } 20 | ], 21 | "name": "Local MCTS", 22 | "squad": "" 23 | }, 24 | "turn": 24, 25 | "board": { 26 | "snakes": [ 27 | { 28 | "latency": "1.818", 29 | "id": "edb52f6c-93e6-4a6d-baaf-f5f823ef836d", 30 | "health": 100, 31 | "length": 4, 32 | "shout": "", 33 | "head": { "y": 2, "x": 0 }, 34 | "customizations": { 35 | "color": "#AA66CC", 36 | "tail": "default", 37 | "head": "trans-rights-scarf" 38 | }, 39 | "body": [ 40 | { "y": 2, "x": 0 }, 41 | { "y": 2, "x": 1 }, 42 | { "y": 3, "x": 1 }, 43 | { "y": 3, "x": 1 } 44 | ], 45 | "name": "Local Bob", 46 | "squad": "" 47 | }, 48 | { 49 | "latency": "96.305", 50 | "id": "fd43df63-ae6a-4bd8-ac87-59f5c15a89f8", 51 | "health": 100, 52 | "length": 5, 53 | "shout": "", 54 | "head": { "y": 5, "x": 5 }, 55 | "customizations": { 56 | "color": "#fc0398", 57 | "tail": "default", 58 | "head": "trans-rights-scarf" 59 | }, 60 | "body": [ 61 | { "y": 5, "x": 5 }, 62 | { "y": 5, "x": 6 }, 63 | { "y": 6, "x": 6 }, 64 | { "y": 6, "x": 5 }, 65 | { "y": 6, "x": 5 } 66 | ], 67 | "name": "Local MCTS", 68 | "squad": "" 69 | } 70 | ], 71 | "width": 11, 72 | "hazards": [], 73 | "height": 11, 74 | "food": [ 75 | { "y": 6, "x": 9 }, 76 | { "y": 0, "x": 4 }, 77 | { "y": 9, "x": 8 }, 78 | { "y": 1, "x": 2 } 79 | ] 80 | }, 81 | "game": { 82 | "source": "custom", 83 | "ruleset": { 84 | "version": "Mojave/3.5.2", 85 | "name": "standard", 86 | "settings": { 87 | "hazardDamagePerTurn": 14, 88 | "royale": { "shrinkEveryNTurns": 25 }, 89 | "squad": { 90 | "sharedHealth": true, 91 | "sharedLength": true, 92 | "allowBodyCollisions": true, 93 | "sharedElimination": true 94 | }, 95 | "minimumFood": 1, 96 | "foodSpawnChance": 15 97 | } 98 | }, 99 | "timeout": 500, 100 | "id": "e5c413d3-43b7-4794-a44e-9def9de87dec" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /battlesnake-rs/fixtures/start_of_game.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "813456", 4 | "ruleset": { "name": "standard", "version": "v.1.2.3" }, 5 | "timeout": 500 6 | }, 7 | "turn": 200, 8 | "you": { 9 | "health": 100, 10 | "id": "you", 11 | "name": "#22aa34", 12 | "body": [ 13 | { "x": 9, "y": 5 }, 14 | { "x": 9, "y": 5 }, 15 | { "x": 9, "y": 5 } 16 | ], 17 | "head": { "x": 9, "y": 5 }, 18 | "latency": null, 19 | "length": 3 20 | }, 21 | "board": { 22 | "food": [ 23 | { "x": 6, "y": 8 }, 24 | { "x": 0, "y": 2 }, 25 | { "x": 5, "y": 5 } 26 | ], 27 | "hazards": [], 28 | "height": 11, 29 | "width": 11, 30 | "snakes": [ 31 | { 32 | "health": 100, 33 | "id": "you", 34 | "name": "#22aa34", 35 | "body": [ 36 | { "x": 9, "y": 5 }, 37 | { "x": 9, "y": 5 }, 38 | { "x": 9, "y": 5 } 39 | ], 40 | "head": { "x": 9, "y": 5 }, 41 | "latency": null, 42 | "length": 3 43 | }, 44 | { 45 | "health": 100, 46 | "id": "#FF6c96", 47 | "name": "#FF6c96", 48 | "body": [ 49 | { "x": 5, "y": 9 }, 50 | { "x": 5, "y": 9 }, 51 | { "x": 5, "y": 9 } 52 | ], 53 | "head": { "x": 5, "y": 9 }, 54 | "latency": null, 55 | "length": 3 56 | }, 57 | { 58 | "health": 100, 59 | "id": "#FF6444", 60 | "name": "#FF6444", 61 | "body": [ 62 | { "x": 1, "y": 1 }, 63 | { "x": 1, "y": 1 }, 64 | { "x": 1, "y": 1 } 65 | ], 66 | "head": { "x": 1, "y": 1 }, 67 | "latency": null, 68 | "length": 3 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /battlesnake-rs/src/amphibious_arthur.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use super::*; 4 | 5 | pub trait MoveToAndSpawn: NeighborDeterminableGame + PositionGettableGame { 6 | fn move_to_and_opponent_sprawl(&self, coor: &Self::NativePositionType) -> Self; 7 | } 8 | 9 | use battlesnake_game_types::types::{ 10 | HeadGettableGame, HealthGettableGame, NeighborDeterminableGame, PositionGettableGame, 11 | YouDeterminableGame, 12 | }; 13 | use rand::seq::SliceRandom; 14 | 15 | impl MoveToAndSpawn for Game { 16 | fn move_to_and_opponent_sprawl(&self, coor: &Position) -> Self { 17 | let mut cloned = self.clone(); 18 | cloned.move_to(coor, &self.you.id); 19 | 20 | let opponents = cloned 21 | .board 22 | .snakes 23 | .iter_mut() 24 | .filter(|s| s.id == self.you.id); 25 | 26 | for s in opponents { 27 | let new_body: Vec<_> = self.neighbors(&s.head).collect(); 28 | s.head = *new_body.choose(&mut rand::thread_rng()).unwrap(); 29 | s.body.append(&mut VecDeque::from(new_body)); 30 | } 31 | 32 | cloned 33 | } 34 | } 35 | 36 | fn score< 37 | T: NeighborDeterminableGame + YouDeterminableGame + HealthGettableGame + MoveToAndSpawn, 38 | >( 39 | game_state: &T, 40 | coor: &T::NativePositionType, 41 | times_to_recurse: u8, 42 | ) -> i64 { 43 | const PREFERRED_HEALTH: i64 = 80; 44 | let you_id = game_state.you_id(); 45 | 46 | if game_state.position_is_snake_body(coor.clone()) { 47 | return 0; 48 | } 49 | 50 | if !game_state.is_alive(you_id) { 51 | return 0; 52 | } 53 | 54 | let ihealth = game_state.get_health_i64(you_id); 55 | let current_score: i64 = (ihealth - PREFERRED_HEALTH).abs(); 56 | let current_score = PREFERRED_HEALTH - current_score; 57 | 58 | if times_to_recurse == 0 { 59 | return current_score; 60 | } 61 | 62 | let recursed_score: i64 = game_state 63 | .neighbors(coor) 64 | .map(|c| { 65 | score( 66 | &game_state.move_to_and_opponent_sprawl(coor), 67 | &c, 68 | times_to_recurse - 1, 69 | ) 70 | }) 71 | .sum(); 72 | 73 | current_score + recursed_score / 2 74 | } 75 | 76 | pub struct AmphibiousArthur { 77 | game: T, 78 | } 79 | 80 | impl< 81 | T: NeighborDeterminableGame 82 | + SnakeIDGettableGame 83 | + HeadGettableGame 84 | + YouDeterminableGame 85 | + MoveToAndSpawn 86 | + HealthGettableGame, 87 | > BattlesnakeAI for AmphibiousArthur 88 | { 89 | fn make_move(&self) -> Result { 90 | let you_id = self.game.you_id(); 91 | let possible = self 92 | .game 93 | .possible_moves(&self.game.get_head_as_native_position(you_id)); 94 | let recursion_limit: u8 = match std::env::var("RECURSION_LIMIT").map(|x| x.parse()) { 95 | Ok(Ok(x)) => x, 96 | _ => 5, 97 | }; 98 | let next_move = possible.max_by_key(|(_mv, coor)| score(&self.game, coor, recursion_limit)); 99 | 100 | let stuck_response: MoveOutput = MoveOutput { 101 | r#move: format!("{}", Move::Up), 102 | shout: Some("Oh NO we are stuck".to_owned()), 103 | }; 104 | 105 | let output = next_move.map_or(stuck_response, |(dir, _coor)| MoveOutput { 106 | r#move: format!("{dir}"), 107 | shout: None, 108 | }); 109 | 110 | Ok(output) 111 | } 112 | } 113 | 114 | pub struct AmphibiousArthurFactory; 115 | 116 | impl BattlesnakeFactory for AmphibiousArthurFactory { 117 | fn name(&self) -> String { 118 | "amphibious-arthur".to_owned() 119 | } 120 | 121 | fn create_from_wire_game(&self, game: Game) -> BoxedSnake { 122 | Box::new(AmphibiousArthur { game }) 123 | } 124 | 125 | fn about(&self) -> AboutMe { 126 | AboutMe { 127 | apiversion: "1".to_owned(), 128 | author: Some("coreyja".to_owned()), 129 | color: Some("#AA66CC".to_owned()), 130 | head: Some("trans-rights-scarf".to_owned()), 131 | tail: Some("swirl".to_owned()), 132 | version: None, 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /battlesnake-rs/src/bombastic_bob.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::types::{ 2 | RandomReasonableMovesGame, SnakeIDGettableGame, YouDeterminableGame, 3 | }; 4 | use rand::thread_rng; 5 | 6 | use super::*; 7 | 8 | pub struct BombasticBob { 9 | game: T, 10 | } 11 | 12 | impl BattlesnakeAI 13 | for BombasticBob 14 | { 15 | fn make_move(&self) -> Result { 16 | let mut rng = thread_rng(); 17 | let chosen = self 18 | .game 19 | .random_reasonable_move_for_each_snake(&mut rng) 20 | .find(|(s, _)| s == self.game.you_id()) 21 | .map(|x| x.1); 22 | let dir = chosen.unwrap_or(Move::Right); 23 | 24 | Ok(MoveOutput { 25 | r#move: format!("{dir}"), 26 | shout: None, 27 | }) 28 | } 29 | } 30 | 31 | pub struct BombasticBobFactory; 32 | 33 | impl BattlesnakeFactory for BombasticBobFactory { 34 | fn name(&self) -> String { 35 | "bombastic-bob".to_owned() 36 | } 37 | 38 | fn create_from_wire_game(&self, game: Game) -> BoxedSnake { 39 | Box::new(BombasticBob { game }) 40 | } 41 | 42 | fn about(&self) -> AboutMe { 43 | AboutMe { 44 | author: Some("coreyja".to_owned()), 45 | color: Some("#AA66CC".to_owned()), 46 | head: Some("trans-rights-scarf".to_owned()), 47 | ..Default::default() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /battlesnake-rs/src/constant_carter.rs: -------------------------------------------------------------------------------- 1 | use tracing::info; 2 | 3 | use super::*; 4 | 5 | pub struct ConstantCarter {} 6 | 7 | impl BattlesnakeAI for ConstantCarter { 8 | fn make_move(&self) -> Result { 9 | Ok(MoveOutput { 10 | r#move: format!("{}", Move::Right), 11 | shout: None, 12 | }) 13 | } 14 | 15 | fn end(&self) { 16 | info!("ConstantCarter has ended"); 17 | } 18 | } 19 | 20 | pub struct ConstantCarterFactory; 21 | 22 | impl BattlesnakeFactory for ConstantCarterFactory { 23 | fn name(&self) -> String { 24 | "constant-carter".to_owned() 25 | } 26 | 27 | fn create_from_wire_game(&self, _game: Game) -> BoxedSnake { 28 | Box::new(ConstantCarter {}) 29 | } 30 | fn about(&self) -> AboutMe { 31 | AboutMe { 32 | author: Some("coreyja".to_owned()), 33 | color: Some("#AA66CC".to_owned()), 34 | head: Some("trans-rights-scarf".to_owned()), 35 | ..Default::default() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /battlesnake-rs/src/famished_frank.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::types::*; 2 | use rand::thread_rng; 3 | 4 | use crate::a_prime::{APrimeNextDirection, APrimeOptions}; 5 | 6 | use super::*; 7 | 8 | pub struct FamishedFrank { 9 | game: T, 10 | } 11 | 12 | impl BattlesnakeAI for FamishedFrank 13 | where 14 | T: SizeDeterminableGame 15 | + FoodGettableGame 16 | + PositionGettableGame 17 | + SnakeBodyGettableGame 18 | + APrimeNextDirection 19 | + RandomReasonableMovesGame 20 | + SnakeIDGettableGame 21 | + YouDeterminableGame, 22 | { 23 | fn make_move(&self) -> Result { 24 | let target_length = self.game.get_height() * 2 + self.game.get_width(); 25 | let you_body = self.game.get_snake_body_vec(self.game.you_id()); 26 | let targets = if you_body.len() < target_length as usize { 27 | self.game.get_all_food_as_native_positions() 28 | } else { 29 | [ 30 | Position { x: 0, y: 0 }, 31 | Position { 32 | x: (self.game.get_width() - 1) as i32, 33 | y: 0, 34 | }, 35 | Position { 36 | x: 0, 37 | y: (self.game.get_height() - 1) as i32, 38 | }, 39 | Position { 40 | x: (self.game.get_width() - 1) as i32, 41 | y: (self.game.get_height() - 1) as i32, 42 | }, 43 | ] 44 | .iter() 45 | .map(|c| self.game.native_from_position(*c)) 46 | .collect() 47 | }; 48 | 49 | let targets: Vec<_> = targets 50 | .into_iter() 51 | .filter(|t| !you_body.contains(t)) 52 | .collect(); 53 | 54 | let head = you_body.first().unwrap(); 55 | let dir = self.game.shortest_path_next_direction( 56 | head, 57 | &targets, 58 | Some(APrimeOptions { 59 | hazard_penalty: 100, 60 | ..Default::default() 61 | }), 62 | ); 63 | 64 | let dir = if let Some(s) = dir { 65 | s 66 | } else { 67 | let you_id = self.game.you_id(); 68 | self.game 69 | .shortest_path_next_direction( 70 | head, 71 | &[you_body.last().unwrap().clone()], 72 | Some(APrimeOptions { 73 | hazard_penalty: 100, 74 | ..Default::default() 75 | }), 76 | ) 77 | .unwrap_or_else(|| { 78 | let mut rng = thread_rng(); 79 | let next_move = self 80 | .game 81 | .random_reasonable_move_for_each_snake(&mut rng) 82 | .find(|(s, _)| s == you_id) 83 | .map(|x| x.1) 84 | .unwrap_or(Move::Right); 85 | next_move 86 | }) 87 | }; 88 | 89 | Ok(MoveOutput { 90 | r#move: format!("{dir}"), 91 | shout: None, 92 | }) 93 | } 94 | } 95 | 96 | pub struct FamishedFrankFactory {} 97 | 98 | impl BattlesnakeFactory for FamishedFrankFactory { 99 | fn name(&self) -> String { 100 | "famished-frank".to_owned() 101 | } 102 | 103 | fn create_from_wire_game(&self, game: Game) -> BoxedSnake { 104 | Box::new(FamishedFrank { game }) 105 | } 106 | fn about(&self) -> AboutMe { 107 | AboutMe { 108 | author: Some("coreyja".to_owned()), 109 | color: Some("#FFBB33".to_owned()), 110 | head: Some("trans-rights-scarf".to_owned()), 111 | ..Default::default() 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /battlesnake-rs/src/flood_fill/jump_flooding.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use battlesnake_game_types::{ 4 | compact_representation::{ 5 | dimensions::Dimensions, CellNum, StandardCellBoard4Snakes11x11, WrappedCellBoard, 6 | }, 7 | types::{HeadGettableGame, PositionGettableGame, SnakeIDGettableGame}, 8 | wire_representation::Position, 9 | }; 10 | 11 | use itertools::Itertools; 12 | 13 | pub trait JumpFlooding: SnakeIDGettableGame 14 | where 15 | Self::SnakeIDType: Copy, 16 | { 17 | fn squares_per_snake(&self) -> HashMap; 18 | } 19 | 20 | struct Grid 21 | where 22 | T: SnakeIDGettableGame, 23 | T::SnakeIDType: Copy, 24 | { 25 | cells: [Option; 11 * 11], 26 | } 27 | 28 | impl JumpFlooding 29 | for WrappedCellBoard 30 | { 31 | fn squares_per_snake(&self) -> HashMap { 32 | let mut grid: Grid = Grid { 33 | cells: [None; 11 * 11], 34 | }; 35 | 36 | // Pre-seed the grid from the Board 37 | for sid in self.get_snake_ids().iter() { 38 | let head = self.get_head_as_native_position(sid); 39 | 40 | grid.cells[head.0.as_usize()] = Some(*sid); 41 | } 42 | 43 | // This comes from k = [ N/2, N/4, N/8, ..., 1 ] 44 | // But I introduced some specific rounding for N = 11 45 | let steps = [6, 3, 1]; 46 | 47 | for neighbor_distance in steps { 48 | (0..(11 * 11)).for_each(|i| { 49 | let neighbor_options = [-neighbor_distance, 0, neighbor_distance]; 50 | let neighbors = neighbor_options 51 | .iter() 52 | .permutations(2) 53 | .filter_map(|coords| { 54 | let y = i / 11; 55 | let x = i % 11; 56 | let pos = Position { x, y }; 57 | 58 | let new_x = pos.x + coords[0]; 59 | if !(0..11).contains(&new_x) { 60 | return None; 61 | } 62 | 63 | let new_y = pos.y + coords[1]; 64 | if !(0..11).contains(&new_y) { 65 | return None; 66 | } 67 | 68 | Some(self.native_from_position(Position { x: new_x, y: new_y })) 69 | }); 70 | 71 | for neighbor in neighbors { 72 | if let Some(nid) = grid.cells[neighbor.0.as_usize()] { 73 | if let Some(sid) = grid.cells[i as usize] { 74 | if sid != nid { 75 | let n_dist = manhattan_distance( 76 | self.get_head_as_native_position(&nid).0.as_usize(), 77 | i, 78 | ); 79 | let s_dist = manhattan_distance( 80 | self.get_head_as_native_position(&sid).0.as_usize(), 81 | i, 82 | ); 83 | 84 | if n_dist < s_dist { 85 | grid.cells[i as usize] = Some(nid); 86 | } 87 | } 88 | } else { 89 | grid.cells[i as usize] = Some(nid); 90 | } 91 | } 92 | } 93 | }) 94 | } 95 | 96 | grid.cells.iter().filter_map(|x| *x).counts() 97 | } 98 | } 99 | 100 | fn manhattan_distance(a: usize, b: i32) -> i32 { 101 | let a = a as i32; 102 | let diff = if a > b { a - b } else { b - a }; 103 | 104 | (diff / 11) + (diff % 11) 105 | } 106 | -------------------------------------------------------------------------------- /battlesnake-rs/src/flood_fill/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jump_flooding; 2 | pub mod spread_from_head; 3 | pub mod spread_from_head_arcade_maze; 4 | -------------------------------------------------------------------------------- /battlesnake-rs/src/flood_fill/spread_from_head.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Reverse; 2 | use std::ops::Deref; 3 | 4 | use battlesnake_game_types::{ 5 | compact_representation::{CellNum, *}, 6 | types::{ 7 | FoodQueryableGame, HazardQueryableGame, HeadGettableGame, LengthGettableGame, 8 | NeighborDeterminableGame, PositionGettableGame, SizeDeterminableGame, 9 | SnakeBodyGettableGame, SnakeIDGettableGame, SnakeId, 10 | }, 11 | }; 12 | use tinyvec::TinyVec; 13 | 14 | pub struct Grid 15 | where 16 | BoardType: SnakeIDGettableGame + ?Sized, 17 | BoardType::SnakeIDType: Copy, 18 | { 19 | pub(crate) cells: Vec>, 20 | } 21 | 22 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 23 | pub struct Scores { 24 | pub(crate) food: u16, 25 | pub(crate) hazard: u16, 26 | pub(crate) empty: u16, 27 | } 28 | 29 | pub trait SpreadFromHead { 30 | type GridType; 31 | 32 | fn calculate(&self, number_of_cycles: usize) -> Self::GridType; 33 | fn squares_per_snake(&self, number_of_cycles: usize) -> [u8; MAX_SNAKES]; 34 | fn squares_per_snake_with_scores( 35 | &self, 36 | number_of_cycles: usize, 37 | scores: Scores, 38 | ) -> [u16; MAX_SNAKES]; 39 | } 40 | 41 | pub struct CellWrapper(pub(crate) CellIndex); 42 | 43 | impl Default for CellWrapper { 44 | fn default() -> Self { 45 | CellWrapper(CellIndex::from_usize(0)) 46 | } 47 | } 48 | 49 | impl Deref for CellWrapper { 50 | type Target = CellIndex; 51 | 52 | fn deref(&self) -> &Self::Target { 53 | &self.0 54 | } 55 | } 56 | 57 | impl SpreadFromHead 58 | for BoardType 59 | where 60 | BoardType: SnakeIDGettableGame 61 | + PositionGettableGame> 62 | + SizeDeterminableGame 63 | + HazardQueryableGame 64 | + FoodQueryableGame 65 | + LengthGettableGame 66 | + NeighborDeterminableGame 67 | + HeadGettableGame 68 | + SnakeBodyGettableGame, 69 | CellType: CellNum, 70 | { 71 | type GridType = Grid; 72 | 73 | fn calculate(&self, number_of_cycles: usize) -> Self::GridType { 74 | let mut grid: Grid = Grid { 75 | cells: vec![None; (self.get_height() * self.get_width()) as usize], 76 | }; 77 | 78 | let sorted_snake_ids = { 79 | let mut sids = self.get_snake_ids(); 80 | sids.sort_unstable_by_key(|sid| Reverse(self.get_length(sid))); 81 | 82 | sids 83 | }; 84 | 85 | let mut todos: TinyVec<[CellWrapper; 16]> = TinyVec::new(); 86 | let mut todos_per_snake: [u8; MAX_SNAKES] = [0; MAX_SNAKES]; 87 | 88 | for sid in &sorted_snake_ids { 89 | for pos in self.get_snake_body_iter(sid) { 90 | grid.cells[pos.as_usize()] = Some(*sid); 91 | } 92 | } 93 | 94 | for sid in &sorted_snake_ids { 95 | let head = self.get_head_as_native_position(sid); 96 | todos.push(CellWrapper(head)); 97 | todos_per_snake[sid.as_usize()] += 1; 98 | } 99 | 100 | for _ in 0..number_of_cycles { 101 | if todos.is_empty() { 102 | break; 103 | } 104 | 105 | let mut new_todos = TinyVec::new(); 106 | let mut new_todos_per_snake = [0; MAX_SNAKES]; 107 | 108 | let mut todos_iter = todos.into_iter(); 109 | 110 | for sid in &sorted_snake_ids { 111 | for _ in 0..todos_per_snake[sid.as_usize()] { 112 | // Mark Neighbors 113 | let pos = todos_iter.next().unwrap(); 114 | 115 | for neighbor in self.neighbors(&pos) { 116 | if grid.cells[neighbor.as_usize()].is_none() { 117 | grid.cells[neighbor.as_usize()] = Some(*sid); 118 | new_todos.push(CellWrapper(neighbor)); 119 | new_todos_per_snake[sid.as_usize()] += 1; 120 | } 121 | } 122 | } 123 | } 124 | 125 | todos = new_todos; 126 | todos_per_snake = new_todos_per_snake; 127 | } 128 | 129 | grid 130 | } 131 | 132 | fn squares_per_snake(&self, number_of_cycles: usize) -> [u8; MAX_SNAKES] { 133 | let result = SpreadFromHead::::calculate(self, number_of_cycles); 134 | let cell_sids = result.cells.iter().filter_map(|x| *x); 135 | 136 | let mut total_values = [0; MAX_SNAKES]; 137 | 138 | for sid in cell_sids { 139 | total_values[sid.as_usize()] += 1; 140 | } 141 | 142 | total_values 143 | } 144 | 145 | fn squares_per_snake_with_scores( 146 | &self, 147 | number_of_cycles: usize, 148 | scores: Scores, 149 | ) -> [u16; MAX_SNAKES] { 150 | let grid = SpreadFromHead::::calculate(self, number_of_cycles); 151 | 152 | let sid_and_values = grid 153 | .cells 154 | .iter() 155 | .enumerate() 156 | .filter_map(|x| x.1.map(|sid| (x.0, sid))) 157 | .map(|(i, sid)| { 158 | let value = if self.is_hazard( 159 | &::NativePositionType::from_usize(i), 160 | ) { 161 | scores.hazard 162 | } else if self.is_food( 163 | &::NativePositionType::from_usize(i), 164 | ) { 165 | scores.food 166 | } else { 167 | scores.empty 168 | }; 169 | 170 | (sid, value) 171 | }); 172 | 173 | let mut total_values = [0_u16; MAX_SNAKES]; 174 | 175 | for (sid, value) in sid_and_values { 176 | total_values[sid.as_usize()] += value; 177 | } 178 | 179 | total_values 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /battlesnake-rs/src/flood_fill/spread_from_head_arcade_maze.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Reverse; 2 | 3 | use battlesnake_game_types::{ 4 | compact_representation::{CellIndex, CellNum}, 5 | types::{ 6 | HazardQueryableGame, HeadGettableGame, LengthGettableGame, NeighborDeterminableGame, 7 | PositionGettableGame, SizeDeterminableGame, SnakeBodyGettableGame, SnakeIDGettableGame, 8 | SnakeId, 9 | }, 10 | }; 11 | use tinyvec::TinyVec; 12 | 13 | pub use super::spread_from_head::*; 14 | 15 | pub trait SpreadFromHeadArcadeMaze { 16 | type GridType; 17 | 18 | fn calculate(&self, number_of_cycles: usize) -> Self::GridType; 19 | fn squares_per_snake_hazard_maze(&self, number_of_cycles: usize) -> [u8; MAX_SNAKES]; 20 | } 21 | 22 | // Board: 19 x 21 23 | // (1, 1) = 1 + 1*19 = 20 24 | // (3, 11) = 11 + 3*19 = 68 25 | // (4, 7) = 7 + 4*19 = 83 26 | // (4, 17) = 17 + 4*19 = 93 27 | // (9, 1) = 1 + 9*19 = 172 28 | // (9, 5) = 5 + 9*19 = 176 29 | // (9, 11) = 11 + 9*19 = 182 30 | // (9, 17) = 17 + 9*19 = 188 31 | // (14, 7) = 7 + 14*19 = 273 32 | // (14, 17) = 17 + 14*19 = 283 33 | // (15, 11) = 11 + 15*19 = 286 34 | // (17, 1) = 1 + 17*19 = 334 35 | const FOOD_SPAWN_LOCATION_INDEX: [usize; 12] = 36 | [20, 68, 83, 93, 172, 176, 182, 188, 273, 283, 286, 334]; 37 | 38 | impl SpreadFromHeadArcadeMaze 39 | for BoardType 40 | where 41 | BoardType: SnakeIDGettableGame 42 | + PositionGettableGame> 43 | + SizeDeterminableGame 44 | + HazardQueryableGame 45 | + LengthGettableGame 46 | + NeighborDeterminableGame 47 | + HeadGettableGame 48 | + SnakeBodyGettableGame, 49 | CellType: CellNum, 50 | { 51 | type GridType = Grid; 52 | 53 | fn calculate(&self, number_of_cycles: usize) -> Self::GridType { 54 | let mut grid: Grid = Grid { 55 | cells: vec![None; (self.get_height() * self.get_width()) as usize], 56 | }; 57 | 58 | let sorted_snake_ids = { 59 | let mut sids = self.get_snake_ids(); 60 | sids.sort_unstable_by_key(|sid| Reverse(self.get_length(sid))); 61 | 62 | sids 63 | }; 64 | 65 | let mut todos: TinyVec<[CellWrapper; 16]> = TinyVec::new(); 66 | let mut todos_per_snake: [u8; MAX_SNAKES] = [0; MAX_SNAKES]; 67 | 68 | for sid in &sorted_snake_ids { 69 | for pos in self.get_snake_body_iter(sid) { 70 | grid.cells[pos.as_usize()] = Some(*sid); 71 | } 72 | } 73 | 74 | for sid in &sorted_snake_ids { 75 | let head = self.get_head_as_native_position(sid); 76 | todos.push(CellWrapper(head)); 77 | todos_per_snake[sid.as_usize()] += 1; 78 | } 79 | 80 | for _ in 0..number_of_cycles { 81 | if todos.is_empty() { 82 | break; 83 | } 84 | 85 | let mut new_todos = TinyVec::new(); 86 | let mut new_todos_per_snake = [0; MAX_SNAKES]; 87 | 88 | let mut todos_iter = todos.into_iter(); 89 | 90 | for sid in &sorted_snake_ids { 91 | for _ in 0..todos_per_snake[sid.as_usize()] { 92 | // Mark Neighbors 93 | let pos = todos_iter.next().unwrap(); 94 | 95 | for neighbor in self.neighbors(&pos).filter(|p| !self.is_hazard(p)) { 96 | if grid.cells[neighbor.as_usize()].is_none() { 97 | grid.cells[neighbor.as_usize()] = Some(*sid); 98 | new_todos.push(CellWrapper(neighbor)); 99 | new_todos_per_snake[sid.as_usize()] += 1; 100 | } 101 | } 102 | } 103 | } 104 | 105 | todos = new_todos; 106 | todos_per_snake = new_todos_per_snake; 107 | } 108 | 109 | grid 110 | } 111 | 112 | fn squares_per_snake_hazard_maze(&self, number_of_cycles: usize) -> [u8; MAX_SNAKES] { 113 | let result = 114 | SpreadFromHeadArcadeMaze::::calculate(self, number_of_cycles); 115 | 116 | let mut total_values = [0; MAX_SNAKES]; 117 | 118 | for (i, sid) in result 119 | .cells 120 | .iter() 121 | .enumerate() 122 | .filter_map(|(i, x)| x.map(|sid| (i, sid))) 123 | { 124 | if FOOD_SPAWN_LOCATION_INDEX.contains(&i) { 125 | total_values[sid.as_usize()] += 12 126 | } else { 127 | total_values[sid.as_usize()] += 1 128 | } 129 | } 130 | 131 | total_values 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /battlesnake-rs/src/gigantic_george.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, convert::TryInto}; 2 | 3 | use battlesnake_game_types::types::*; 4 | 5 | use crate::eremetic_eric::EremeticEric; 6 | 7 | use super::*; 8 | 9 | pub struct GiganticGeorge { 10 | game: T, 11 | } 12 | 13 | fn path_to_full_board( 14 | reversed_body: &[T::NativePositionType], 15 | game: &T, 16 | ) -> Option> { 17 | let max_size = game.get_width() * game.get_height(); 18 | if reversed_body.len() == max_size as usize { 19 | return Some(vec![]); 20 | } 21 | 22 | for (dir, coor) in game 23 | .possible_moves(reversed_body.last().unwrap()) 24 | .filter(|(_, c)| !reversed_body.contains(c)) 25 | { 26 | let mut new_body = reversed_body.to_vec(); 27 | new_body.push(coor.clone()); 28 | 29 | if let Some(mut path) = path_to_full_board(&new_body, game) { 30 | path.push((dir, coor)); 31 | return Some(path); 32 | } 33 | } 34 | 35 | None 36 | } 37 | 38 | pub trait FullBoardDeterminable { 39 | fn contains_empty_squares(&self) -> bool; 40 | } 41 | impl FullBoardDeterminable for Game { 42 | fn contains_empty_squares(&self) -> bool { 43 | let mut map: HashSet = HashSet::new(); 44 | 45 | for c in self.board.food.iter() { 46 | map.insert(*c); 47 | } 48 | 49 | for s in self.board.snakes.iter() { 50 | for c in s.body.iter() { 51 | map.insert(*c); 52 | } 53 | } 54 | 55 | let full_size: usize = (self.get_height() * self.get_width()).try_into().unwrap(); 56 | 57 | full_size != map.len() 58 | } 59 | } 60 | 61 | impl BattlesnakeAI for GiganticGeorge 62 | where 63 | T: FullBoardDeterminable 64 | + ShoutGettableGame 65 | + YouDeterminableGame 66 | + NeighborDeterminableGame 67 | + SizeDeterminableGame 68 | + PositionGettableGame 69 | + HeadGettableGame 70 | + SnakeBodyGettableGame 71 | + SnakeTailPushableGame 72 | + FoodGettableGame 73 | + HealthGettableGame 74 | + a_prime::APrimeNextDirection 75 | + TurnDeterminableGame 76 | + std::clone::Clone, 77 | { 78 | fn make_move(&self) -> Result { 79 | let you_id = self.game.you_id(); 80 | 81 | if let Some(s) = self.game.get_shout(you_id) { 82 | if s.starts_with("PATH:") { 83 | let path = s.split("PATH:").nth(1).unwrap(); 84 | 85 | let next_char = path.to_lowercase().chars().last().unwrap(); 86 | let dir = match next_char { 87 | 'l' => Some(Move::Left), 88 | 'r' => Some(Move::Right), 89 | 'u' => Some(Move::Up), 90 | 'd' => Some(Move::Down), 91 | _ => None, 92 | }; 93 | 94 | if let Some(d) = dir { 95 | return Ok(MoveOutput { 96 | r#move: format!("{d}"), 97 | shout: Some(format!("PATH:{}", &path[..path.len() - 1])), 98 | }); 99 | } 100 | } 101 | } 102 | 103 | if !self.game.contains_empty_squares() { 104 | println!("Ok now can we complete the board?"); 105 | 106 | let reversed_body = { 107 | let mut x = self.game.get_snake_body_vec(you_id); 108 | x.pop(); // Remove my current tail cause I will need to fill that space too 109 | x.reverse(); 110 | x 111 | }; 112 | 113 | if let Some(mut path) = path_to_full_board(&reversed_body, &self.game) { 114 | let new = path.pop(); 115 | let path_string: String = path 116 | .iter() 117 | .map(|(d, _)| format!("{d}").chars().next().unwrap()) 118 | .collect(); 119 | return Ok(MoveOutput { 120 | r#move: format!("{}", new.unwrap().0), 121 | shout: Some("PATH:".to_string() + &path_string), 122 | }); 123 | } else { 124 | println!("Nah lets keep looping"); 125 | } 126 | } 127 | 128 | let eric = EremeticEric { 129 | game: self.game.clone(), 130 | }; 131 | eric.make_move() 132 | } 133 | } 134 | 135 | pub struct GiganticGeorgeFactory {} 136 | 137 | impl BattlesnakeFactory for GiganticGeorgeFactory { 138 | fn create_from_wire_game(&self, game: Game) -> BoxedSnake { 139 | Box::new(GiganticGeorge { game }) 140 | } 141 | 142 | fn name(&self) -> String { 143 | "gigantic-george".to_owned() 144 | } 145 | fn about(&self) -> AboutMe { 146 | AboutMe { 147 | author: Some("coreyja".to_owned()), 148 | color: Some("#FFBB33".to_owned()), 149 | head: Some("trans-rights-scarf".to_owned()), 150 | ..Default::default() 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /battlesnake-rs/src/jump_flooding_snake.rs: -------------------------------------------------------------------------------- 1 | use crate::flood_fill::jump_flooding::JumpFlooding; 2 | use crate::*; 3 | 4 | use battlesnake_minimax::paranoid::MinimaxSnake; 5 | 6 | use battlesnake_game_types::compact_representation::WrappedCellBoard4Snakes11x11; 7 | 8 | use decorum::N64; 9 | 10 | pub fn score(node: &T) -> N64 11 | where 12 | T::SnakeIDType: Copy, 13 | T: SnakeIDGettableGame + YouDeterminableGame + JumpFlooding, 14 | { 15 | let square_counts = node.squares_per_snake(); 16 | 17 | let my_space: f64 = (square_counts.get(node.you_id()).copied().unwrap_or(0) as u16).into(); 18 | let total_space: f64 = (square_counts.values().sum::() as u16).into(); 19 | 20 | N64::from(my_space / total_space) 21 | } 22 | 23 | pub struct JumpFloodingSnakeFactory; 24 | 25 | impl BattlesnakeFactory for JumpFloodingSnakeFactory { 26 | fn name(&self) -> String { 27 | "jump-flooding".to_owned() 28 | } 29 | 30 | fn create_from_wire_game(&self, game: Game) -> BoxedSnake { 31 | let game_info = game.game.clone(); 32 | let turn = game.turn; 33 | let id_map = build_snake_id_map(&game); 34 | 35 | let game = WrappedCellBoard4Snakes11x11::convert_from_game(game, &id_map).unwrap(); 36 | 37 | let snake = MinimaxSnake::from_fn(game, game_info, turn, &score, "jump-flooding"); 38 | 39 | Box::new(snake) 40 | } 41 | 42 | fn about(&self) -> AboutMe { 43 | AboutMe { 44 | apiversion: "1".to_owned(), 45 | author: Some("coreyja".to_owned()), 46 | color: Some("#efae09".to_owned()), 47 | head: Some("trans-rights-scarf".to_owned()), 48 | tail: None, 49 | version: None, 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests {} 56 | -------------------------------------------------------------------------------- /fixtures/095b30fa-f2c7-4826-ac93-90b4dde6b785_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_6FctbXB7DCvtSTS9BHpCrjxJ", 4 | "name": "Hovering Hobbs", 5 | "head": { 6 | "x": 3, 7 | "y": 6 8 | }, 9 | "body": [ 10 | { 11 | "x": 3, 12 | "y": 6 13 | }, 14 | { 15 | "x": 3, 16 | "y": 7 17 | }, 18 | { 19 | "x": 4, 20 | "y": 7 21 | } 22 | ], 23 | "health": 95, 24 | "shout": "" 25 | }, 26 | "board": { 27 | "height": 11, 28 | "width": 11, 29 | "food": [ 30 | { 31 | "x": 10, 32 | "y": 2 33 | }, 34 | { 35 | "x": 0, 36 | "y": 2 37 | }, 38 | { 39 | "x": 2, 40 | "y": 8 41 | } 42 | ], 43 | "snakes": [ 44 | { 45 | "id": "gs_PdMvmpSPkwJJrPj7BRWrYdVM", 46 | "name": "Big Slammu", 47 | "head": { 48 | "x": 8, 49 | "y": 1 50 | }, 51 | "body": [ 52 | { 53 | "x": 8, 54 | "y": 1 55 | }, 56 | { 57 | "x": 7, 58 | "y": 1 59 | }, 60 | { 61 | "x": 7, 62 | "y": 2 63 | } 64 | ], 65 | "health": 95, 66 | "shout": "Chompin'!" 67 | }, 68 | { 69 | "id": "gs_YYGHbTRQ79xmQrRt87DW86YQ", 70 | "name": "Spaceheater", 71 | "head": { 72 | "x": 3, 73 | "y": 2 74 | }, 75 | "body": [ 76 | { 77 | "x": 3, 78 | "y": 2 79 | }, 80 | { 81 | "x": 4, 82 | "y": 2 83 | }, 84 | { 85 | "x": 4, 86 | "y": 3 87 | } 88 | ], 89 | "health": 95, 90 | "shout": "" 91 | }, 92 | { 93 | "id": "gs_88VCPhggJbYDKRGKpdkgPGbG", 94 | "name": "Ami ☮", 95 | "head": { 96 | "x": 1, 97 | "y": 7 98 | }, 99 | "body": [ 100 | { 101 | "x": 1, 102 | "y": 7 103 | }, 104 | { 105 | "x": 1, 106 | "y": 8 107 | }, 108 | { 109 | "x": 0, 110 | "y": 8 111 | }, 112 | { 113 | "x": 10, 114 | "y": 8 115 | } 116 | ], 117 | "health": 97, 118 | "shout": "" 119 | }, 120 | { 121 | "id": "gs_6FctbXB7DCvtSTS9BHpCrjxJ", 122 | "name": "Hovering Hobbs", 123 | "head": { 124 | "x": 3, 125 | "y": 6 126 | }, 127 | "body": [ 128 | { 129 | "x": 3, 130 | "y": 6 131 | }, 132 | { 133 | "x": 3, 134 | "y": 7 135 | }, 136 | { 137 | "x": 4, 138 | "y": 7 139 | } 140 | ], 141 | "health": 95, 142 | "shout": "" 143 | } 144 | ], 145 | "hazards": [ 146 | { 147 | "x": 5, 148 | "y": 10 149 | }, 150 | { 151 | "x": 5, 152 | "y": 9 153 | }, 154 | { 155 | "x": 5, 156 | "y": 7 157 | }, 158 | { 159 | "x": 5, 160 | "y": 6 161 | }, 162 | { 163 | "x": 5, 164 | "y": 5 165 | }, 166 | { 167 | "x": 5, 168 | "y": 4 169 | }, 170 | { 171 | "x": 5, 172 | "y": 3 173 | }, 174 | { 175 | "x": 5, 176 | "y": 0 177 | }, 178 | { 179 | "x": 5, 180 | "y": 1 181 | }, 182 | { 183 | "x": 6, 184 | "y": 5 185 | }, 186 | { 187 | "x": 7, 188 | "y": 5 189 | }, 190 | { 191 | "x": 9, 192 | "y": 5 193 | }, 194 | { 195 | "x": 10, 196 | "y": 5 197 | }, 198 | { 199 | "x": 4, 200 | "y": 5 201 | }, 202 | { 203 | "x": 3, 204 | "y": 5 205 | }, 206 | { 207 | "x": 1, 208 | "y": 5 209 | }, 210 | { 211 | "x": 0, 212 | "y": 5 213 | }, 214 | { 215 | "x": 1, 216 | "y": 10 217 | }, 218 | { 219 | "x": 9, 220 | "y": 10 221 | }, 222 | { 223 | "x": 1, 224 | "y": 0 225 | }, 226 | { 227 | "x": 9, 228 | "y": 0 229 | }, 230 | { 231 | "x": 10, 232 | "y": 1 233 | }, 234 | { 235 | "x": 10, 236 | "y": 0 237 | }, 238 | { 239 | "x": 10, 240 | "y": 10 241 | }, 242 | { 243 | "x": 10, 244 | "y": 9 245 | }, 246 | { 247 | "x": 0, 248 | "y": 10 249 | }, 250 | { 251 | "x": 0, 252 | "y": 9 253 | }, 254 | { 255 | "x": 0, 256 | "y": 1 257 | }, 258 | { 259 | "x": 0, 260 | "y": 0 261 | }, 262 | { 263 | "x": 0, 264 | "y": 6 265 | }, 266 | { 267 | "x": 0, 268 | "y": 4 269 | }, 270 | { 271 | "x": 10, 272 | "y": 6 273 | }, 274 | { 275 | "x": 10, 276 | "y": 4 277 | }, 278 | { 279 | "x": 6, 280 | "y": 10 281 | }, 282 | { 283 | "x": 4, 284 | "y": 10 285 | }, 286 | { 287 | "x": 6, 288 | "y": 0 289 | }, 290 | { 291 | "x": 4, 292 | "y": 0 293 | } 294 | ] 295 | }, 296 | "turn": 5, 297 | "game": { 298 | "id": "095b30fa-f2c7-4826-ac93-90b4dde6b785", 299 | "ruleset": { 300 | "name": "wrapped", 301 | "version": "No version in frames", 302 | "settings": { 303 | "foodSpawnChance": 15, 304 | "minimumFood": 1, 305 | "hazardDamagePerTurn": 100, 306 | "hazardMap": null, 307 | "hazardMapAuthor": null, 308 | "royale": null 309 | } 310 | }, 311 | "timeout": 500, 312 | "map": "hz_islands_bridges", 313 | "source": "tournament" 314 | } 315 | } -------------------------------------------------------------------------------- /fixtures/095b30fa-f2c7-4826-ac93-90b4dde6b785_6.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_6FctbXB7DCvtSTS9BHpCrjxJ", 4 | "name": "Hovering Hobbs", 5 | "head": { 6 | "x": 2, 7 | "y": 6 8 | }, 9 | "body": [ 10 | { 11 | "x": 2, 12 | "y": 6 13 | }, 14 | { 15 | "x": 3, 16 | "y": 6 17 | }, 18 | { 19 | "x": 3, 20 | "y": 7 21 | } 22 | ], 23 | "health": 94, 24 | "shout": "" 25 | }, 26 | "board": { 27 | "height": 11, 28 | "width": 11, 29 | "food": [ 30 | { 31 | "x": 10, 32 | "y": 2 33 | }, 34 | { 35 | "x": 0, 36 | "y": 2 37 | }, 38 | { 39 | "x": 2, 40 | "y": 8 41 | } 42 | ], 43 | "snakes": [ 44 | { 45 | "id": "gs_PdMvmpSPkwJJrPj7BRWrYdVM", 46 | "name": "Big Slammu", 47 | "head": { 48 | "x": 8, 49 | "y": 2 50 | }, 51 | "body": [ 52 | { 53 | "x": 8, 54 | "y": 2 55 | }, 56 | { 57 | "x": 8, 58 | "y": 1 59 | }, 60 | { 61 | "x": 7, 62 | "y": 1 63 | } 64 | ], 65 | "health": 94, 66 | "shout": "Piece of crabcake!" 67 | }, 68 | { 69 | "id": "gs_YYGHbTRQ79xmQrRt87DW86YQ", 70 | "name": "Spaceheater", 71 | "head": { 72 | "x": 3, 73 | "y": 3 74 | }, 75 | "body": [ 76 | { 77 | "x": 3, 78 | "y": 3 79 | }, 80 | { 81 | "x": 3, 82 | "y": 2 83 | }, 84 | { 85 | "x": 4, 86 | "y": 2 87 | } 88 | ], 89 | "health": 94, 90 | "shout": "" 91 | }, 92 | { 93 | "id": "gs_88VCPhggJbYDKRGKpdkgPGbG", 94 | "name": "Ami ☮", 95 | "head": { 96 | "x": 2, 97 | "y": 7 98 | }, 99 | "body": [ 100 | { 101 | "x": 2, 102 | "y": 7 103 | }, 104 | { 105 | "x": 1, 106 | "y": 7 107 | }, 108 | { 109 | "x": 1, 110 | "y": 8 111 | }, 112 | { 113 | "x": 0, 114 | "y": 8 115 | } 116 | ], 117 | "health": 96, 118 | "shout": "" 119 | }, 120 | { 121 | "id": "gs_6FctbXB7DCvtSTS9BHpCrjxJ", 122 | "name": "Hovering Hobbs", 123 | "head": { 124 | "x": 2, 125 | "y": 6 126 | }, 127 | "body": [ 128 | { 129 | "x": 2, 130 | "y": 6 131 | }, 132 | { 133 | "x": 3, 134 | "y": 6 135 | }, 136 | { 137 | "x": 3, 138 | "y": 7 139 | } 140 | ], 141 | "health": 94, 142 | "shout": "" 143 | } 144 | ], 145 | "hazards": [ 146 | { 147 | "x": 5, 148 | "y": 10 149 | }, 150 | { 151 | "x": 5, 152 | "y": 9 153 | }, 154 | { 155 | "x": 5, 156 | "y": 7 157 | }, 158 | { 159 | "x": 5, 160 | "y": 6 161 | }, 162 | { 163 | "x": 5, 164 | "y": 5 165 | }, 166 | { 167 | "x": 5, 168 | "y": 4 169 | }, 170 | { 171 | "x": 5, 172 | "y": 3 173 | }, 174 | { 175 | "x": 5, 176 | "y": 0 177 | }, 178 | { 179 | "x": 5, 180 | "y": 1 181 | }, 182 | { 183 | "x": 6, 184 | "y": 5 185 | }, 186 | { 187 | "x": 7, 188 | "y": 5 189 | }, 190 | { 191 | "x": 9, 192 | "y": 5 193 | }, 194 | { 195 | "x": 10, 196 | "y": 5 197 | }, 198 | { 199 | "x": 4, 200 | "y": 5 201 | }, 202 | { 203 | "x": 3, 204 | "y": 5 205 | }, 206 | { 207 | "x": 1, 208 | "y": 5 209 | }, 210 | { 211 | "x": 0, 212 | "y": 5 213 | }, 214 | { 215 | "x": 1, 216 | "y": 10 217 | }, 218 | { 219 | "x": 9, 220 | "y": 10 221 | }, 222 | { 223 | "x": 1, 224 | "y": 0 225 | }, 226 | { 227 | "x": 9, 228 | "y": 0 229 | }, 230 | { 231 | "x": 10, 232 | "y": 1 233 | }, 234 | { 235 | "x": 10, 236 | "y": 0 237 | }, 238 | { 239 | "x": 10, 240 | "y": 10 241 | }, 242 | { 243 | "x": 10, 244 | "y": 9 245 | }, 246 | { 247 | "x": 0, 248 | "y": 10 249 | }, 250 | { 251 | "x": 0, 252 | "y": 9 253 | }, 254 | { 255 | "x": 0, 256 | "y": 1 257 | }, 258 | { 259 | "x": 0, 260 | "y": 0 261 | }, 262 | { 263 | "x": 0, 264 | "y": 6 265 | }, 266 | { 267 | "x": 0, 268 | "y": 4 269 | }, 270 | { 271 | "x": 10, 272 | "y": 6 273 | }, 274 | { 275 | "x": 10, 276 | "y": 4 277 | }, 278 | { 279 | "x": 6, 280 | "y": 10 281 | }, 282 | { 283 | "x": 4, 284 | "y": 10 285 | }, 286 | { 287 | "x": 6, 288 | "y": 0 289 | }, 290 | { 291 | "x": 4, 292 | "y": 0 293 | } 294 | ] 295 | }, 296 | "turn": 6, 297 | "game": { 298 | "id": "095b30fa-f2c7-4826-ac93-90b4dde6b785", 299 | "ruleset": { 300 | "name": "wrapped", 301 | "version": "No version in frames", 302 | "settings": { 303 | "foodSpawnChance": 15, 304 | "minimumFood": 1, 305 | "hazardDamagePerTurn": 100, 306 | "hazardMap": null, 307 | "hazardMapAuthor": null, 308 | "royale": null 309 | } 310 | }, 311 | "timeout": 500, 312 | "map": "hz_islands_bridges", 313 | "source": "tournament" 314 | } 315 | } -------------------------------------------------------------------------------- /fixtures/130b18e2-8689-4d64-a09f-c4345f80ae79_25.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_wWHwDCggVBcp8TC3QktV7B43", 4 | "name": "TBD MCTS", 5 | "head": { 6 | "x": 3, 7 | "y": 10 8 | }, 9 | "body": [ 10 | { 11 | "x": 3, 12 | "y": 10 13 | }, 14 | { 15 | "x": 4, 16 | "y": 10 17 | }, 18 | { 19 | "x": 5, 20 | "y": 10 21 | }, 22 | { 23 | "x": 6, 24 | "y": 10 25 | } 26 | ], 27 | "health": 77, 28 | "shout": "" 29 | }, 30 | "board": { 31 | "height": 11, 32 | "width": 11, 33 | "food": [ 34 | { 35 | "x": 3, 36 | "y": 0 37 | } 38 | ], 39 | "snakes": [ 40 | { 41 | "id": "gs_kSP4CqkgFRBJd3w9MdkTrgYS", 42 | "name": "One", 43 | "head": { 44 | "x": 2, 45 | "y": 5 46 | }, 47 | "body": [ 48 | { 49 | "x": 2, 50 | "y": 5 51 | }, 52 | { 53 | "x": 2, 54 | "y": 6 55 | }, 56 | { 57 | "x": 3, 58 | "y": 6 59 | }, 60 | { 61 | "x": 3, 62 | "y": 5 63 | }, 64 | { 65 | "x": 4, 66 | "y": 5 67 | } 68 | ], 69 | "health": 85, 70 | "shout": "" 71 | }, 72 | { 73 | "id": "gs_GRT6qmVW9bpwRyBxhCGjxpTR", 74 | "name": "Fairy Rust", 75 | "head": { 76 | "x": 5, 77 | "y": 4 78 | }, 79 | "body": [ 80 | { 81 | "x": 5, 82 | "y": 4 83 | }, 84 | { 85 | "x": 5, 86 | "y": 5 87 | }, 88 | { 89 | "x": 5, 90 | "y": 6 91 | }, 92 | { 93 | "x": 6, 94 | "y": 6 95 | } 96 | ], 97 | "health": 77, 98 | "shout": "" 99 | }, 100 | { 101 | "id": "gs_49c8rKykgqDBd4KSJSVyqYvK", 102 | "name": "soma-mini v1[standard]", 103 | "head": { 104 | "x": 1, 105 | "y": 8 106 | }, 107 | "body": [ 108 | { 109 | "x": 1, 110 | "y": 8 111 | }, 112 | { 113 | "x": 1, 114 | "y": 7 115 | }, 116 | { 117 | "x": 2, 118 | "y": 7 119 | }, 120 | { 121 | "x": 3, 122 | "y": 7 123 | } 124 | ], 125 | "health": 77, 126 | "shout": "406" 127 | }, 128 | { 129 | "id": "gs_wWHwDCggVBcp8TC3QktV7B43", 130 | "name": "TBD MCTS", 131 | "head": { 132 | "x": 3, 133 | "y": 10 134 | }, 135 | "body": [ 136 | { 137 | "x": 3, 138 | "y": 10 139 | }, 140 | { 141 | "x": 4, 142 | "y": 10 143 | }, 144 | { 145 | "x": 5, 146 | "y": 10 147 | }, 148 | { 149 | "x": 6, 150 | "y": 10 151 | } 152 | ], 153 | "health": 77, 154 | "shout": "" 155 | } 156 | ], 157 | "hazards": [] 158 | }, 159 | "turn": 25, 160 | "game": { 161 | "id": "130b18e2-8689-4d64-a09f-c4345f80ae79", 162 | "ruleset": { 163 | "name": "standard", 164 | "version": "No version in frames", 165 | "settings": { 166 | "foodSpawnChance": 15, 167 | "minimumFood": 1, 168 | "hazardDamagePerTurn": 14, 169 | "hazardMap": null, 170 | "hazardMapAuthor": null, 171 | "royale": null 172 | } 173 | }, 174 | "timeout": 500, 175 | "map": "standard", 176 | "source": "ladder" 177 | } 178 | } -------------------------------------------------------------------------------- /fixtures/45e7de53-bca5-4fa3-8771-d9914ed141bb.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "45e7de53-bca5-4fa3-8771-d9914ed141bb", 4 | "ruleset": { 5 | "name": "standard", 6 | "version": "?", 7 | "settings": { 8 | "foodSpawnChance": 15, 9 | "minimumFood": 1, 10 | "hazardDamagePerTurn": 14, 11 | "royale": { "shrinkEveryNTurns": 0 }, 12 | "squad": { 13 | "allowBodyCollisions": false, 14 | "sharedElimination": false, 15 | "sharedHealth": false, 16 | "sharedLength": false 17 | } 18 | } 19 | }, 20 | "map": "standard", 21 | "timeout": 500, 22 | "source": "custom" 23 | }, 24 | "turn": 52, 25 | "board": { 26 | "width": 11, 27 | "height": 11, 28 | "food": [{ "x": 5, "y": 0 }], 29 | "hazards": [], 30 | "snakes": [ 31 | { 32 | "id": "gs_PHDC4FXtXw78cBFD9FcRr8W8", 33 | "name": "Local MCTS", 34 | "health": 74, 35 | "body": [ 36 | { "x": 6, "y": 4 }, 37 | { "x": 5, "y": 4 }, 38 | { "x": 5, "y": 5 }, 39 | { "x": 6, "y": 5 }, 40 | { "x": 6, "y": 6 }, 41 | { "x": 6, "y": 7 } 42 | ], 43 | "latency": 451, 44 | "head": { "x": 6, "y": 4 }, 45 | "length": 6, 46 | "shout": "", 47 | "squad": "", 48 | "customizations": { 49 | "color": "#fc0398", 50 | "head": "trans-rights-scarf", 51 | "tail": "default" 52 | } 53 | }, 54 | { 55 | "id": "gs_qt8736cVpJPmKmvcRCJSB3VP", 56 | "name": "Hovering Hobbs", 57 | "health": 52, 58 | "body": [ 59 | { "x": 5, "y": 7 }, 60 | { "x": 5, "y": 8 }, 61 | { "x": 4, "y": 8 }, 62 | { "x": 4, "y": 7 } 63 | ], 64 | "latency": 454, 65 | "head": { "x": 5, "y": 7 }, 66 | "length": 4, 67 | "shout": "", 68 | "squad": "", 69 | "customizations": { 70 | "color": "#da8a1a", 71 | "head": "beach-puffin-special", 72 | "tail": "beach-puffin-special" 73 | } 74 | }, 75 | { 76 | "id": "gs_CdVJ3rjvBpxGG4QftVQRTFC8", 77 | "name": "Ziggy Snakedust", 78 | "health": 97, 79 | "body": [ 80 | { "x": 10, "y": 0 }, 81 | { "x": 9, "y": 0 }, 82 | { "x": 9, "y": 1 }, 83 | { "x": 9, "y": 2 }, 84 | { "x": 10, "y": 2 }, 85 | { "x": 10, "y": 1 } 86 | ], 87 | "latency": 449, 88 | "head": { "x": 10, "y": 0 }, 89 | "length": 6, 90 | "shout": "", 91 | "squad": "", 92 | "customizations": { 93 | "color": "#fcb040", 94 | "head": "iguana", 95 | "tail": "iguana" 96 | } 97 | }, 98 | { 99 | "id": "gs_dc3RHm6GD4tJccfgFdb3yKDG", 100 | "name": "Shapeshifter", 101 | "health": 97, 102 | "body": [ 103 | { "x": 5, "y": 3 }, 104 | { "x": 4, "y": 3 }, 105 | { "x": 3, "y": 3 }, 106 | { "x": 2, "y": 3 }, 107 | { "x": 2, "y": 2 }, 108 | { "x": 2, "y": 1 }, 109 | { "x": 1, "y": 1 }, 110 | { "x": 1, "y": 2 }, 111 | { "x": 1, "y": 3 }, 112 | { "x": 0, "y": 3 } 113 | ], 114 | "latency": 402, 115 | "head": { "x": 5, "y": 3 }, 116 | "length": 10, 117 | "shout": "", 118 | "squad": "", 119 | "customizations": { 120 | "color": "#900050", 121 | "head": "cosmic-horror-special", 122 | "tail": "cosmic-horror" 123 | } 124 | } 125 | ] 126 | }, 127 | "you": { 128 | "id": "gs_PHDC4FXtXw78cBFD9FcRr8W8", 129 | "name": "Local MCTS", 130 | "health": 74, 131 | "body": [ 132 | { "x": 6, "y": 4 }, 133 | { "x": 5, "y": 4 }, 134 | { "x": 5, "y": 5 }, 135 | { "x": 6, "y": 5 }, 136 | { "x": 6, "y": 6 }, 137 | { "x": 6, "y": 7 } 138 | ], 139 | "latency": 451, 140 | "head": { "x": 6, "y": 4 }, 141 | "length": 6, 142 | "shout": "", 143 | "squad": "", 144 | "customizations": { 145 | "color": "#fc0398", 146 | "head": "trans-rights-scarf", 147 | "tail": "default" 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /fixtures/65401e8f-a92a-445f-9617-94770044e117.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "65401e8f-a92a-445f-9617-94770044e117", 4 | "ruleset": { 5 | "name": "standard", 6 | "version": "?", 7 | "settings": { 8 | "foodSpawnChance": 15, 9 | "minimumFood": 1, 10 | "hazardDamagePerTurn": 14, 11 | "royale": { "shrinkEveryNTurns": 10 }, 12 | "squad": { 13 | "allowBodyCollisions": false, 14 | "sharedElimination": false, 15 | "sharedHealth": false, 16 | "sharedLength": false 17 | } 18 | } 19 | }, 20 | "map": "standard", 21 | "timeout": 500, 22 | "source": "custom" 23 | }, 24 | "turn": 47, 25 | "board": { 26 | "width": 11, 27 | "height": 11, 28 | "food": [ 29 | { "x": 0, "y": 2 }, 30 | { "x": 0, "y": 6 }, 31 | { "x": 2, "y": 7 }, 32 | { "x": 1, "y": 7 }, 33 | { "x": 1, "y": 3 } 34 | ], 35 | "hazards": [], 36 | "snakes": [ 37 | { 38 | "id": "gs_TBBQCQXCQyKCtGmwTv7yvkBW", 39 | "name": "Hovering Hobbs", 40 | "health": 85, 41 | "body": [ 42 | { "x": 2, "y": 5 }, 43 | { "x": 2, "y": 4 }, 44 | { "x": 2, "y": 3 }, 45 | { "x": 3, "y": 3 } 46 | ], 47 | "latency": 458, 48 | "head": { "x": 2, "y": 5 }, 49 | "length": 4, 50 | "shout": "", 51 | "squad": "", 52 | "customizations": { 53 | "color": "#da8a1a", 54 | "head": "beach-puffin-special", 55 | "tail": "beach-puffin-special" 56 | } 57 | }, 58 | { 59 | "id": "gs_TTB748gKwy8h69GTfYQ4jF7C", 60 | "name": "Local MCTS", 61 | "health": 75, 62 | "body": [ 63 | { "x": 5, "y": 8 }, 64 | { "x": 6, "y": 8 }, 65 | { "x": 6, "y": 7 }, 66 | { "x": 5, "y": 7 } 67 | ], 68 | "latency": 443, 69 | "head": { "x": 5, "y": 8 }, 70 | "length": 4, 71 | "shout": "", 72 | "squad": "", 73 | "customizations": { 74 | "color": "#fc0398", 75 | "head": "trans-rights-scarf", 76 | "tail": "default" 77 | } 78 | }, 79 | { 80 | "id": "gs_FmP7GJCMgytBV9pyQmPTpGFM", 81 | "name": "Ziggy Snakedust", 82 | "health": 96, 83 | "body": [ 84 | { "x": 2, "y": 9 }, 85 | { "x": 3, "y": 9 }, 86 | { "x": 4, "y": 9 }, 87 | { "x": 5, "y": 9 }, 88 | { "x": 6, "y": 9 }, 89 | { "x": 7, "y": 9 }, 90 | { "x": 8, "y": 9 } 91 | ], 92 | "latency": 451, 93 | "head": { "x": 2, "y": 9 }, 94 | "length": 7, 95 | "shout": "", 96 | "squad": "", 97 | "customizations": { 98 | "color": "#fcb040", 99 | "head": "iguana", 100 | "tail": "iguana" 101 | } 102 | }, 103 | { 104 | "id": "gs_J7fCCHJRk7ftX6jYqXSWvpYd", 105 | "name": "Shapeshifter", 106 | "health": 98, 107 | "body": [ 108 | { "x": 8, "y": 1 }, 109 | { "x": 9, "y": 1 }, 110 | { "x": 9, "y": 0 }, 111 | { "x": 8, "y": 0 }, 112 | { "x": 7, "y": 0 }, 113 | { "x": 6, "y": 0 }, 114 | { "x": 6, "y": 1 }, 115 | { "x": 6, "y": 2 }, 116 | { "x": 5, "y": 2 }, 117 | { "x": 4, "y": 2 }, 118 | { "x": 4, "y": 3 } 119 | ], 120 | "latency": 401, 121 | "head": { "x": 8, "y": 1 }, 122 | "length": 11, 123 | "shout": "", 124 | "squad": "", 125 | "customizations": { 126 | "color": "#900050", 127 | "head": "cosmic-horror-special", 128 | "tail": "cosmic-horror" 129 | } 130 | } 131 | ] 132 | }, 133 | "you": { 134 | "id": "gs_TTB748gKwy8h69GTfYQ4jF7C", 135 | "name": "Local MCTS", 136 | "health": 75, 137 | "body": [ 138 | { "x": 5, "y": 8 }, 139 | { "x": 6, "y": 8 }, 140 | { "x": 6, "y": 7 }, 141 | { "x": 5, "y": 7 } 142 | ], 143 | "latency": 443, 144 | "head": { "x": 5, "y": 8 }, 145 | "length": 4, 146 | "shout": "", 147 | "squad": "", 148 | "customizations": { 149 | "color": "#fc0398", 150 | "head": "trans-rights-scarf", 151 | "tail": "default" 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /fixtures/7a02e19b-f658-4639-8ace-ece46629a6ed_192.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_fSdkwVMdrwmtRVkdDf8P6hT7", 4 | "name": "TBD MCTS", 5 | "head": { 6 | "x": 7, 7 | "y": 3 8 | }, 9 | "body": [ 10 | { 11 | "x": 7, 12 | "y": 3 13 | }, 14 | { 15 | "x": 7, 16 | "y": 2 17 | }, 18 | { 19 | "x": 8, 20 | "y": 2 21 | }, 22 | { 23 | "x": 8, 24 | "y": 1 25 | }, 26 | { 27 | "x": 7, 28 | "y": 1 29 | }, 30 | { 31 | "x": 7, 32 | "y": 0 33 | }, 34 | { 35 | "x": 8, 36 | "y": 0 37 | } 38 | ], 39 | "health": 92, 40 | "shout": "" 41 | }, 42 | "board": { 43 | "height": 11, 44 | "width": 11, 45 | "food": [ 46 | { 47 | "x": 10, 48 | "y": 10 49 | }, 50 | { 51 | "x": 7, 52 | "y": 9 53 | }, 54 | { 55 | "x": 4, 56 | "y": 9 57 | }, 58 | { 59 | "x": 10, 60 | "y": 7 61 | }, 62 | { 63 | "x": 8, 64 | "y": 8 65 | } 66 | ], 67 | "snakes": [ 68 | { 69 | "id": "gs_bwtRBFhYCg8X87VvJBVFSRYT", 70 | "name": "MegaShark", 71 | "head": { 72 | "x": 6, 73 | "y": 2 74 | }, 75 | "body": [ 76 | { 77 | "x": 6, 78 | "y": 2 79 | }, 80 | { 81 | "x": 6, 82 | "y": 1 83 | }, 84 | { 85 | "x": 6, 86 | "y": 0 87 | }, 88 | { 89 | "x": 5, 90 | "y": 0 91 | }, 92 | { 93 | "x": 4, 94 | "y": 0 95 | }, 96 | { 97 | "x": 3, 98 | "y": 0 99 | }, 100 | { 101 | "x": 3, 102 | "y": 1 103 | }, 104 | { 105 | "x": 4, 106 | "y": 1 107 | }, 108 | { 109 | "x": 5, 110 | "y": 1 111 | }, 112 | { 113 | "x": 5, 114 | "y": 2 115 | }, 116 | { 117 | "x": 4, 118 | "y": 2 119 | }, 120 | { 121 | "x": 3, 122 | "y": 2 123 | }, 124 | { 125 | "x": 2, 126 | "y": 2 127 | }, 128 | { 129 | "x": 1, 130 | "y": 2 131 | }, 132 | { 133 | "x": 1, 134 | "y": 3 135 | }, 136 | { 137 | "x": 1, 138 | "y": 4 139 | }, 140 | { 141 | "x": 0, 142 | "y": 4 143 | }, 144 | { 145 | "x": 0, 146 | "y": 5 147 | }, 148 | { 149 | "x": 0, 150 | "y": 6 151 | }, 152 | { 153 | "x": 0, 154 | "y": 7 155 | }, 156 | { 157 | "x": 0, 158 | "y": 8 159 | }, 160 | { 161 | "x": 0, 162 | "y": 9 163 | }, 164 | { 165 | "x": 1, 166 | "y": 9 167 | }, 168 | { 169 | "x": 1, 170 | "y": 10 171 | }, 172 | { 173 | "x": 2, 174 | "y": 10 175 | }, 176 | { 177 | "x": 2, 178 | "y": 9 179 | }, 180 | { 181 | "x": 2, 182 | "y": 8 183 | }, 184 | { 185 | "x": 2, 186 | "y": 7 187 | }, 188 | { 189 | "x": 2, 190 | "y": 6 191 | } 192 | ], 193 | "health": 91, 194 | "shout": "" 195 | }, 196 | { 197 | "id": "gs_fSdkwVMdrwmtRVkdDf8P6hT7", 198 | "name": "TBD MCTS", 199 | "head": { 200 | "x": 7, 201 | "y": 3 202 | }, 203 | "body": [ 204 | { 205 | "x": 7, 206 | "y": 3 207 | }, 208 | { 209 | "x": 7, 210 | "y": 2 211 | }, 212 | { 213 | "x": 8, 214 | "y": 2 215 | }, 216 | { 217 | "x": 8, 218 | "y": 1 219 | }, 220 | { 221 | "x": 7, 222 | "y": 1 223 | }, 224 | { 225 | "x": 7, 226 | "y": 0 227 | }, 228 | { 229 | "x": 8, 230 | "y": 0 231 | } 232 | ], 233 | "health": 92, 234 | "shout": "" 235 | } 236 | ], 237 | "hazards": [] 238 | }, 239 | "turn": 192, 240 | "game": { 241 | "id": "7a02e19b-f658-4639-8ace-ece46629a6ed", 242 | "ruleset": { 243 | "name": "standard", 244 | "version": "No version in frames", 245 | "settings": { 246 | "foodSpawnChance": 15, 247 | "minimumFood": 1, 248 | "hazardDamagePerTurn": 14, 249 | "hazardMap": null, 250 | "hazardMapAuthor": null, 251 | "royale": null 252 | } 253 | }, 254 | "timeout": 500, 255 | "map": "standard", 256 | "source": "ladder" 257 | } 258 | } -------------------------------------------------------------------------------- /fixtures/95d72d73-352b-4ad5-83e4-86139fa556a9_54.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_TcwJQJ6HtX3RqBppQww8f8CD", 4 | "name": "Improbable Irene", 5 | "head": { 6 | "x": 5, 7 | "y": 3 8 | }, 9 | "body": [ 10 | { 11 | "x": 5, 12 | "y": 3 13 | }, 14 | { 15 | "x": 4, 16 | "y": 3 17 | }, 18 | { 19 | "x": 4, 20 | "y": 2 21 | }, 22 | { 23 | "x": 4, 24 | "y": 1 25 | }, 26 | { 27 | "x": 5, 28 | "y": 1 29 | } 30 | ], 31 | "health": 89, 32 | "shout": "" 33 | }, 34 | "board": { 35 | "height": 11, 36 | "width": 11, 37 | "food": [ 38 | { 39 | "x": 2, 40 | "y": 4 41 | }, 42 | { 43 | "x": 6, 44 | "y": 3 45 | }, 46 | { 47 | "x": 5, 48 | "y": 10 49 | } 50 | ], 51 | "snakes": [ 52 | { 53 | "id": "gs_TcwJQJ6HtX3RqBppQww8f8CD", 54 | "name": "Improbable Irene", 55 | "head": { 56 | "x": 5, 57 | "y": 3 58 | }, 59 | "body": [ 60 | { 61 | "x": 5, 62 | "y": 3 63 | }, 64 | { 65 | "x": 4, 66 | "y": 3 67 | }, 68 | { 69 | "x": 4, 70 | "y": 2 71 | }, 72 | { 73 | "x": 4, 74 | "y": 1 75 | }, 76 | { 77 | "x": 5, 78 | "y": 1 79 | } 80 | ], 81 | "health": 89, 82 | "shout": "" 83 | }, 84 | { 85 | "id": "gs_7DrdgRBM4WpWKxhmvhwTGMTR", 86 | "name": "soma-mini[sink]", 87 | "head": { 88 | "x": 6, 89 | "y": 4 90 | }, 91 | "body": [ 92 | { 93 | "x": 6, 94 | "y": 4 95 | }, 96 | { 97 | "x": 5, 98 | "y": 4 99 | }, 100 | { 101 | "x": 5, 102 | "y": 5 103 | }, 104 | { 105 | "x": 6, 106 | "y": 5 107 | }, 108 | { 109 | "x": 6, 110 | "y": 6 111 | }, 112 | { 113 | "x": 7, 114 | "y": 6 115 | }, 116 | { 117 | "x": 7, 118 | "y": 5 119 | }, 120 | { 121 | "x": 8, 122 | "y": 5 123 | } 124 | ], 125 | "health": 89, 126 | "shout": "413" 127 | }, 128 | { 129 | "id": "gs_mQtK3tJW8WDhGwyXRp6DJqrG", 130 | "name": "bamboozle snake", 131 | "head": { 132 | "x": 3, 133 | "y": 7 134 | }, 135 | "body": [ 136 | { 137 | "x": 3, 138 | "y": 7 139 | }, 140 | { 141 | "x": 3, 142 | "y": 6 143 | }, 144 | { 145 | "x": 4, 146 | "y": 6 147 | }, 148 | { 149 | "x": 4, 150 | "y": 7 151 | }, 152 | { 153 | "x": 5, 154 | "y": 7 155 | }, 156 | { 157 | "x": 5, 158 | "y": 8 159 | }, 160 | { 161 | "x": 4, 162 | "y": 8 163 | }, 164 | { 165 | "x": 4, 166 | "y": 9 167 | } 168 | ], 169 | "health": 92, 170 | "shout": "" 171 | } 172 | ], 173 | "hazards": [ 174 | { 175 | "x": 0, 176 | "y": 0 177 | }, 178 | { 179 | "x": 0, 180 | "y": 1 181 | }, 182 | { 183 | "x": 0, 184 | "y": 2 185 | }, 186 | { 187 | "x": 0, 188 | "y": 3 189 | }, 190 | { 191 | "x": 0, 192 | "y": 4 193 | }, 194 | { 195 | "x": 0, 196 | "y": 5 197 | }, 198 | { 199 | "x": 0, 200 | "y": 6 201 | }, 202 | { 203 | "x": 0, 204 | "y": 7 205 | }, 206 | { 207 | "x": 0, 208 | "y": 8 209 | }, 210 | { 211 | "x": 0, 212 | "y": 9 213 | }, 214 | { 215 | "x": 0, 216 | "y": 10 217 | }, 218 | { 219 | "x": 1, 220 | "y": 0 221 | }, 222 | { 223 | "x": 2, 224 | "y": 0 225 | }, 226 | { 227 | "x": 3, 228 | "y": 0 229 | }, 230 | { 231 | "x": 4, 232 | "y": 0 233 | }, 234 | { 235 | "x": 5, 236 | "y": 0 237 | }, 238 | { 239 | "x": 6, 240 | "y": 0 241 | }, 242 | { 243 | "x": 7, 244 | "y": 0 245 | }, 246 | { 247 | "x": 8, 248 | "y": 0 249 | }, 250 | { 251 | "x": 9, 252 | "y": 0 253 | }, 254 | { 255 | "x": 10, 256 | "y": 0 257 | } 258 | ] 259 | }, 260 | "turn": 54, 261 | "game": { 262 | "id": "95d72d73-352b-4ad5-83e4-86139fa556a9", 263 | "ruleset": { 264 | "name": "royale", 265 | "version": "No version in frames", 266 | "settings": { 267 | "foodSpawnChance": 20, 268 | "minimumFood": 1, 269 | "hazardDamagePerTurn": 14, 270 | "hazardMap": null, 271 | "hazardMapAuthor": null, 272 | "royale": null 273 | } 274 | }, 275 | "timeout": 500, 276 | "map": "standard", 277 | "source": "ladder" 278 | } 279 | } -------------------------------------------------------------------------------- /fixtures/af943832-1b3b-4795-9e35-081f71959aee_108.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_kjkSw6BDqTST3Ww8DKFBQwy4", 4 | "name": "TBD MCTS", 5 | "head": { 6 | "x": 9, 7 | "y": 3 8 | }, 9 | "body": [ 10 | { 11 | "x": 9, 12 | "y": 3 13 | }, 14 | { 15 | "x": 9, 16 | "y": 2 17 | }, 18 | { 19 | "x": 8, 20 | "y": 2 21 | }, 22 | { 23 | "x": 8, 24 | "y": 1 25 | }, 26 | { 27 | "x": 9, 28 | "y": 1 29 | } 30 | ], 31 | "health": 66, 32 | "shout": "" 33 | }, 34 | "board": { 35 | "height": 11, 36 | "width": 11, 37 | "food": [ 38 | { 39 | "x": 0, 40 | "y": 1 41 | }, 42 | { 43 | "x": 0, 44 | "y": 8 45 | }, 46 | { 47 | "x": 1, 48 | "y": 4 49 | }, 50 | { 51 | "x": 5, 52 | "y": 0 53 | }, 54 | { 55 | "x": 10, 56 | "y": 2 57 | } 58 | ], 59 | "snakes": [ 60 | { 61 | "id": "gs_kjkSw6BDqTST3Ww8DKFBQwy4", 62 | "name": "TBD MCTS", 63 | "head": { 64 | "x": 9, 65 | "y": 3 66 | }, 67 | "body": [ 68 | { 69 | "x": 9, 70 | "y": 3 71 | }, 72 | { 73 | "x": 9, 74 | "y": 2 75 | }, 76 | { 77 | "x": 8, 78 | "y": 2 79 | }, 80 | { 81 | "x": 8, 82 | "y": 1 83 | }, 84 | { 85 | "x": 9, 86 | "y": 1 87 | } 88 | ], 89 | "health": 66, 90 | "shout": "" 91 | }, 92 | { 93 | "id": "gs_dH7xpGhHkHrpVBwybv3mFYbX", 94 | "name": "Hovering Hobbs", 95 | "head": { 96 | "x": 9, 97 | "y": 5 98 | }, 99 | "body": [ 100 | { 101 | "x": 9, 102 | "y": 5 103 | }, 104 | { 105 | "x": 8, 106 | "y": 5 107 | }, 108 | { 109 | "x": 7, 110 | "y": 5 111 | }, 112 | { 113 | "x": 7, 114 | "y": 4 115 | }, 116 | { 117 | "x": 7, 118 | "y": 3 119 | }, 120 | { 121 | "x": 7, 122 | "y": 2 123 | }, 124 | { 125 | "x": 6, 126 | "y": 2 127 | }, 128 | { 129 | "x": 6, 130 | "y": 3 131 | }, 132 | { 133 | "x": 5, 134 | "y": 3 135 | } 136 | ], 137 | "health": 96, 138 | "shout": "" 139 | }, 140 | { 141 | "id": "gs_4wv94YPf9R3FwCBwcTDdGt3G", 142 | "name": "Ziggy Snakedust", 143 | "head": { 144 | "x": 3, 145 | "y": 9 146 | }, 147 | "body": [ 148 | { 149 | "x": 3, 150 | "y": 9 151 | }, 152 | { 153 | "x": 2, 154 | "y": 9 155 | }, 156 | { 157 | "x": 1, 158 | "y": 9 159 | }, 160 | { 161 | "x": 0, 162 | "y": 9 163 | }, 164 | { 165 | "x": 0, 166 | "y": 10 167 | }, 168 | { 169 | "x": 1, 170 | "y": 10 171 | }, 172 | { 173 | "x": 2, 174 | "y": 10 175 | }, 176 | { 177 | "x": 3, 178 | "y": 10 179 | }, 180 | { 181 | "x": 4, 182 | "y": 10 183 | }, 184 | { 185 | "x": 4, 186 | "y": 9 187 | } 188 | ], 189 | "health": 89, 190 | "shout": "" 191 | } 192 | ], 193 | "hazards": [] 194 | }, 195 | "turn": 108, 196 | "game": { 197 | "id": "af943832-1b3b-4795-9e35-081f71959aee", 198 | "ruleset": { 199 | "name": "standard", 200 | "version": "No version in frames", 201 | "settings": { 202 | "foodSpawnChance": 15, 203 | "minimumFood": 1, 204 | "hazardDamagePerTurn": 14, 205 | "hazardMap": null, 206 | "hazardMapAuthor": null, 207 | "royale": null 208 | } 209 | }, 210 | "timeout": 500, 211 | "map": "standard", 212 | "source": "custom" 213 | } 214 | } -------------------------------------------------------------------------------- /fixtures/c2aee0d9-30dc-47ee-bd25-38e67e0fee9d_96.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_r79xmktCxmKTcV9cPGdkgfJc", 4 | "name": "Improbable Irene", 5 | "head": { 6 | "x": 7, 7 | "y": 4 8 | }, 9 | "body": [ 10 | { 11 | "x": 7, 12 | "y": 4 13 | }, 14 | { 15 | "x": 7, 16 | "y": 3 17 | }, 18 | { 19 | "x": 7, 20 | "y": 2 21 | }, 22 | { 23 | "x": 7, 24 | "y": 1 25 | }, 26 | { 27 | "x": 7, 28 | "y": 0 29 | }, 30 | { 31 | "x": 8, 32 | "y": 0 33 | }, 34 | { 35 | "x": 9, 36 | "y": 0 37 | }, 38 | { 39 | "x": 10, 40 | "y": 0 41 | }, 42 | { 43 | "x": 10, 44 | "y": 10 45 | } 46 | ], 47 | "health": 94, 48 | "shout": "" 49 | }, 50 | "board": { 51 | "height": 11, 52 | "width": 11, 53 | "food": [ 54 | { 55 | "x": 8, 56 | "y": 10 57 | }, 58 | { 59 | "x": 1, 60 | "y": 2 61 | }, 62 | { 63 | "x": 5, 64 | "y": 9 65 | } 66 | ], 67 | "snakes": [ 68 | { 69 | "id": "gs_r79xmktCxmKTcV9cPGdkgfJc", 70 | "name": "Improbable Irene", 71 | "head": { 72 | "x": 7, 73 | "y": 4 74 | }, 75 | "body": [ 76 | { 77 | "x": 7, 78 | "y": 4 79 | }, 80 | { 81 | "x": 7, 82 | "y": 3 83 | }, 84 | { 85 | "x": 7, 86 | "y": 2 87 | }, 88 | { 89 | "x": 7, 90 | "y": 1 91 | }, 92 | { 93 | "x": 7, 94 | "y": 0 95 | }, 96 | { 97 | "x": 8, 98 | "y": 0 99 | }, 100 | { 101 | "x": 9, 102 | "y": 0 103 | }, 104 | { 105 | "x": 10, 106 | "y": 0 107 | }, 108 | { 109 | "x": 10, 110 | "y": 10 111 | } 112 | ], 113 | "health": 94, 114 | "shout": "" 115 | }, 116 | { 117 | "id": "gs_DwDXKcYKvmYp93g6w67RQDCG", 118 | "name": "Hovering Hobbs", 119 | "head": { 120 | "x": 6, 121 | "y": 3 122 | }, 123 | "body": [ 124 | { 125 | "x": 6, 126 | "y": 3 127 | }, 128 | { 129 | "x": 6, 130 | "y": 2 131 | }, 132 | { 133 | "x": 6, 134 | "y": 1 135 | }, 136 | { 137 | "x": 6, 138 | "y": 0 139 | }, 140 | { 141 | "x": 6, 142 | "y": 10 143 | }, 144 | { 145 | "x": 6, 146 | "y": 9 147 | }, 148 | { 149 | "x": 6, 150 | "y": 8 151 | }, 152 | { 153 | "x": 6, 154 | "y": 7 155 | }, 156 | { 157 | "x": 6, 158 | "y": 6 159 | }, 160 | { 161 | "x": 5, 162 | "y": 6 163 | }, 164 | { 165 | "x": 5, 166 | "y": 5 167 | } 168 | ], 169 | "health": 74, 170 | "shout": "" 171 | } 172 | ], 173 | "hazards": [ 174 | { 175 | "x": 0, 176 | "y": 0 177 | }, 178 | { 179 | "x": 0, 180 | "y": 1 181 | }, 182 | { 183 | "x": 0, 184 | "y": 2 185 | }, 186 | { 187 | "x": 0, 188 | "y": 3 189 | }, 190 | { 191 | "x": 0, 192 | "y": 4 193 | }, 194 | { 195 | "x": 0, 196 | "y": 5 197 | }, 198 | { 199 | "x": 0, 200 | "y": 6 201 | }, 202 | { 203 | "x": 0, 204 | "y": 7 205 | }, 206 | { 207 | "x": 0, 208 | "y": 8 209 | }, 210 | { 211 | "x": 0, 212 | "y": 9 213 | }, 214 | { 215 | "x": 0, 216 | "y": 10 217 | }, 218 | { 219 | "x": 1, 220 | "y": 0 221 | }, 222 | { 223 | "x": 1, 224 | "y": 1 225 | }, 226 | { 227 | "x": 1, 228 | "y": 2 229 | }, 230 | { 231 | "x": 1, 232 | "y": 3 233 | }, 234 | { 235 | "x": 1, 236 | "y": 4 237 | }, 238 | { 239 | "x": 1, 240 | "y": 5 241 | }, 242 | { 243 | "x": 1, 244 | "y": 6 245 | }, 246 | { 247 | "x": 1, 248 | "y": 7 249 | }, 250 | { 251 | "x": 1, 252 | "y": 8 253 | }, 254 | { 255 | "x": 1, 256 | "y": 9 257 | }, 258 | { 259 | "x": 1, 260 | "y": 10 261 | }, 262 | { 263 | "x": 2, 264 | "y": 10 265 | }, 266 | { 267 | "x": 3, 268 | "y": 10 269 | }, 270 | { 271 | "x": 4, 272 | "y": 10 273 | }, 274 | { 275 | "x": 5, 276 | "y": 10 277 | }, 278 | { 279 | "x": 6, 280 | "y": 10 281 | }, 282 | { 283 | "x": 7, 284 | "y": 10 285 | }, 286 | { 287 | "x": 8, 288 | "y": 10 289 | }, 290 | { 291 | "x": 9, 292 | "y": 10 293 | }, 294 | { 295 | "x": 10, 296 | "y": 10 297 | } 298 | ] 299 | }, 300 | "turn": 96, 301 | "game": { 302 | "id": "c2aee0d9-30dc-47ee-bd25-38e67e0fee9d", 303 | "ruleset": { 304 | "name": "wrapped", 305 | "version": "No version in frames", 306 | "settings": { 307 | "foodSpawnChance": 15, 308 | "minimumFood": 1, 309 | "hazardDamagePerTurn": 14, 310 | "hazardMap": null, 311 | "hazardMapAuthor": null, 312 | "royale": null 313 | } 314 | }, 315 | "timeout": 500, 316 | "map": "royale", 317 | "source": "custom" 318 | } 319 | } -------------------------------------------------------------------------------- /fixtures/d9841bf6-c34f-42fb-8818-dfd5d5a09b4a_125.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "id": "gs_dYHchfRbYtYFQM3QPYGjR3VQ", 4 | "name": "TBD MCTS", 5 | "head": { 6 | "x": 9, 7 | "y": 4 8 | }, 9 | "body": [ 10 | { 11 | "x": 9, 12 | "y": 4 13 | }, 14 | { 15 | "x": 10, 16 | "y": 4 17 | }, 18 | { 19 | "x": 10, 20 | "y": 3 21 | }, 22 | { 23 | "x": 10, 24 | "y": 2 25 | }, 26 | { 27 | "x": 10, 28 | "y": 1 29 | }, 30 | { 31 | "x": 9, 32 | "y": 1 33 | }, 34 | { 35 | "x": 8, 36 | "y": 1 37 | }, 38 | { 39 | "x": 8, 40 | "y": 2 41 | }, 42 | { 43 | "x": 8, 44 | "y": 3 45 | }, 46 | { 47 | "x": 8, 48 | "y": 4 49 | }, 50 | { 51 | "x": 7, 52 | "y": 4 53 | }, 54 | { 55 | "x": 6, 56 | "y": 4 57 | }, 58 | { 59 | "x": 5, 60 | "y": 4 61 | }, 62 | { 63 | "x": 5, 64 | "y": 3 65 | } 66 | ], 67 | "health": 93, 68 | "shout": "" 69 | }, 70 | "board": { 71 | "height": 11, 72 | "width": 11, 73 | "food": [ 74 | { 75 | "x": 3, 76 | "y": 9 77 | }, 78 | { 79 | "x": 7, 80 | "y": 9 81 | }, 82 | { 83 | "x": 3, 84 | "y": 7 85 | }, 86 | { 87 | "x": 10, 88 | "y": 10 89 | }, 90 | { 91 | "x": 1, 92 | "y": 9 93 | } 94 | ], 95 | "snakes": [ 96 | { 97 | "id": "gs_dYHchfRbYtYFQM3QPYGjR3VQ", 98 | "name": "TBD MCTS", 99 | "head": { 100 | "x": 9, 101 | "y": 4 102 | }, 103 | "body": [ 104 | { 105 | "x": 9, 106 | "y": 4 107 | }, 108 | { 109 | "x": 10, 110 | "y": 4 111 | }, 112 | { 113 | "x": 10, 114 | "y": 3 115 | }, 116 | { 117 | "x": 10, 118 | "y": 2 119 | }, 120 | { 121 | "x": 10, 122 | "y": 1 123 | }, 124 | { 125 | "x": 9, 126 | "y": 1 127 | }, 128 | { 129 | "x": 8, 130 | "y": 1 131 | }, 132 | { 133 | "x": 8, 134 | "y": 2 135 | }, 136 | { 137 | "x": 8, 138 | "y": 3 139 | }, 140 | { 141 | "x": 8, 142 | "y": 4 143 | }, 144 | { 145 | "x": 7, 146 | "y": 4 147 | }, 148 | { 149 | "x": 6, 150 | "y": 4 151 | }, 152 | { 153 | "x": 5, 154 | "y": 4 155 | }, 156 | { 157 | "x": 5, 158 | "y": 3 159 | } 160 | ], 161 | "health": 93, 162 | "shout": "" 163 | }, 164 | { 165 | "id": "gs_88kvkBy3pvfvVQKXWdrBrkRb", 166 | "name": "Hovering Hobbs", 167 | "head": { 168 | "x": 5, 169 | "y": 2 170 | }, 171 | "body": [ 172 | { 173 | "x": 5, 174 | "y": 2 175 | }, 176 | { 177 | "x": 5, 178 | "y": 1 179 | }, 180 | { 181 | "x": 5, 182 | "y": 0 183 | }, 184 | { 185 | "x": 4, 186 | "y": 0 187 | }, 188 | { 189 | "x": 4, 190 | "y": 1 191 | }, 192 | { 193 | "x": 4, 194 | "y": 2 195 | }, 196 | { 197 | "x": 4, 198 | "y": 3 199 | }, 200 | { 201 | "x": 4, 202 | "y": 4 203 | }, 204 | { 205 | "x": 4, 206 | "y": 5 207 | }, 208 | { 209 | "x": 4, 210 | "y": 6 211 | }, 212 | { 213 | "x": 4, 214 | "y": 7 215 | } 216 | ], 217 | "health": 87, 218 | "shout": "" 219 | } 220 | ], 221 | "hazards": [] 222 | }, 223 | "turn": 125, 224 | "game": { 225 | "id": "d9841bf6-c34f-42fb-8818-dfd5d5a09b4a", 226 | "ruleset": { 227 | "name": "standard", 228 | "version": "No version in frames", 229 | "settings": { 230 | "foodSpawnChance": 15, 231 | "minimumFood": 1, 232 | "hazardDamagePerTurn": 14, 233 | "hazardMap": null, 234 | "hazardMapAuthor": null, 235 | "royale": null 236 | } 237 | }, 238 | "timeout": 500, 239 | "map": "standard", 240 | "source": "custom" 241 | } 242 | } -------------------------------------------------------------------------------- /fixtures/df732ab7-7e22-41d8-b651-95bb912e45ab.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "df732ab7-7e22-41d8-b651-95bb912e45ab", 4 | "ruleset": { 5 | "name": "standard", 6 | "version": "?", 7 | "settings": { 8 | "foodSpawnChance": 15, 9 | "minimumFood": 1, 10 | "hazardDamagePerTurn": 14, 11 | "royale": { "shrinkEveryNTurns": 10 }, 12 | "squad": { 13 | "allowBodyCollisions": false, 14 | "sharedElimination": false, 15 | "sharedHealth": false, 16 | "sharedLength": false 17 | } 18 | } 19 | }, 20 | "map": "standard", 21 | "timeout": 500, 22 | "source": "custom" 23 | }, 24 | "turn": 10, 25 | "board": { 26 | "width": 11, 27 | "height": 11, 28 | "food": [{ "x": 10, "y": 2 }], 29 | "hazards": [], 30 | "snakes": [ 31 | { 32 | "id": "gs_3y6cg4wCXKVSQ4BbxWcYP8Tc", 33 | "name": "Shapeshifter", 34 | "health": 92, 35 | "body": [ 36 | { "x": 6, "y": 2 }, 37 | { "x": 5, "y": 2 }, 38 | { "x": 4, "y": 2 }, 39 | { "x": 4, "y": 3 } 40 | ], 41 | "latency": 401, 42 | "head": { "x": 6, "y": 2 }, 43 | "length": 4, 44 | "shout": "", 45 | "squad": "", 46 | "customizations": { 47 | "color": "#900050", 48 | "head": "cosmic-horror-special", 49 | "tail": "cosmic-horror" 50 | } 51 | }, 52 | { 53 | "id": "gs_d8CRSbfWBT6cxRwcy8kgSVCH", 54 | "name": "Ziggy Snakedust", 55 | "health": 92, 56 | "body": [ 57 | { "x": 0, "y": 10 }, 58 | { "x": 1, "y": 10 }, 59 | { "x": 1, "y": 9 }, 60 | { "x": 0, "y": 9 } 61 | ], 62 | "latency": 82, 63 | "head": { "x": 0, "y": 10 }, 64 | "length": 4, 65 | "shout": "", 66 | "squad": "", 67 | "customizations": { 68 | "color": "#fcb040", 69 | "head": "iguana", 70 | "tail": "iguana" 71 | } 72 | }, 73 | { 74 | "id": "gs_7W4VSP6vjct3xVbS8DHkwpgK", 75 | "name": "Local MCTS", 76 | "health": 90, 77 | "body": [ 78 | { "x": 8, "y": 6 }, 79 | { "x": 8, "y": 5 }, 80 | { "x": 8, "y": 4 } 81 | ], 82 | "latency": 443, 83 | "head": { "x": 8, "y": 6 }, 84 | "length": 3, 85 | "shout": "", 86 | "squad": "", 87 | "customizations": { 88 | "color": "#fc0398", 89 | "head": "trans-rights-scarf", 90 | "tail": "default" 91 | } 92 | }, 93 | { 94 | "id": "gs_ywKtcKkHxqpkGSdvPYWRfF4Y", 95 | "name": "Hovering Hobbs", 96 | "health": 100, 97 | "body": [ 98 | { "x": 5, "y": 5 }, 99 | { "x": 5, "y": 6 }, 100 | { "x": 6, "y": 6 }, 101 | { "x": 7, "y": 6 }, 102 | { "x": 7, "y": 6 } 103 | ], 104 | "latency": 456, 105 | "head": { "x": 5, "y": 5 }, 106 | "length": 5, 107 | "shout": "", 108 | "squad": "", 109 | "customizations": { 110 | "color": "#da8a1a", 111 | "head": "beach-puffin-special", 112 | "tail": "beach-puffin-special" 113 | } 114 | } 115 | ] 116 | }, 117 | "you": { 118 | "id": "gs_7W4VSP6vjct3xVbS8DHkwpgK", 119 | "name": "Local MCTS", 120 | "health": 90, 121 | "body": [ 122 | { "x": 8, "y": 6 }, 123 | { "x": 8, "y": 5 }, 124 | { "x": 8, "y": 4 } 125 | ], 126 | "latency": 443, 127 | "head": { "x": 8, "y": 6 }, 128 | "length": 3, 129 | "shout": "", 130 | "squad": "", 131 | "customizations": { 132 | "color": "#fc0398", 133 | "head": "trans-rights-scarf", 134 | "tail": "default" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /fixtures/less_basic_expand_mcts.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "477537", 4 | "ruleset": { 5 | "name": "standard", 6 | "version": "v.1.2.3" 7 | }, 8 | "timeout": 500 9 | }, 10 | "turn": 200, 11 | "you": { 12 | "health": 100, 13 | "id": "you", 14 | "name": "#22aa34", 15 | "body": [ 16 | { 17 | "x": 10, 18 | "y": 7 19 | }, 20 | { 21 | "x": 9, 22 | "y": 7 23 | }, 24 | { 25 | "x": 9, 26 | "y": 6 27 | }, 28 | { 29 | "x": 8, 30 | "y": 6 31 | }, 32 | { 33 | "x": 8, 34 | "y": 5 35 | }, 36 | { 37 | "x": 9, 38 | "y": 5 39 | } 40 | ], 41 | "head": { 42 | "x": 10, 43 | "y": 7 44 | }, 45 | "length": 8 46 | }, 47 | "board": { 48 | "food": [ 49 | { 50 | "x": 6, 51 | "y": 8 52 | }, 53 | { 54 | "x": 0, 55 | "y": 2 56 | }, 57 | { 58 | "x": 5, 59 | "y": 5 60 | } 61 | ], 62 | "hazards": [], 63 | "height": 11, 64 | "width": 11, 65 | "snakes": [ 66 | { 67 | "health": 100, 68 | "id": "you", 69 | "name": "#22aa34", 70 | "body": [ 71 | { 72 | "x": 10, 73 | "y": 7 74 | }, 75 | { 76 | "x": 9, 77 | "y": 7 78 | }, 79 | { 80 | "x": 9, 81 | "y": 6 82 | }, 83 | { 84 | "x": 8, 85 | "y": 6 86 | }, 87 | { 88 | "x": 8, 89 | "y": 5 90 | }, 91 | { 92 | "x": 9, 93 | "y": 5 94 | } 95 | ], 96 | "head": { 97 | "x": 10, 98 | "y": 7 99 | }, 100 | "length": 8 101 | }, 102 | { 103 | "health": 100, 104 | "id": "#FFfb1e", 105 | "name": "#FFfb1e", 106 | "body": [ 107 | { 108 | "x": 0, 109 | "y": 10 110 | }, 111 | { 112 | "x": 0, 113 | "y": 10 114 | }, 115 | { 116 | "x": 0, 117 | "y": 10 118 | } 119 | ], 120 | "head": { 121 | "x": 0, 122 | "y": 10 123 | }, 124 | "length": 1 125 | } 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /fixtures/mojave_12_18_12_34.json: -------------------------------------------------------------------------------- 1 | { 2 | "you": { 3 | "latency": "404.926", 4 | "id": "26debd4e-99fe-4e6f-90f0-4e812792cb17", 5 | "health": 68, 6 | "length": 3, 7 | "shout": "", 8 | "head": { 9 | "y": 10, 10 | "x": 2 11 | }, 12 | "customizations": { 13 | "color": "#5a25a8", 14 | "tail": "default", 15 | "head": "default" 16 | }, 17 | "body": [ 18 | { 19 | "y": 10, 20 | "x": 2 21 | }, 22 | { 23 | "y": 10, 24 | "x": 1 25 | }, 26 | { 27 | "y": 10, 28 | "x": 0 29 | } 30 | ], 31 | "name": "Local Irene", 32 | "squad": "" 33 | }, 34 | "turn": 32, 35 | "board": { 36 | "snakes": [ 37 | { 38 | "latency": "456.403", 39 | "id": "76487538-d195-4e1c-97da-c091e02d4372", 40 | "health": 85, 41 | "length": 4, 42 | "shout": "", 43 | "head": { 44 | "y": 9, 45 | "x": 3 46 | }, 47 | "customizations": { 48 | "color": "#da8a1a", 49 | "tail": "flame", 50 | "head": "trans-rights-scarf" 51 | }, 52 | "body": [ 53 | { 54 | "y": 9, 55 | "x": 3 56 | }, 57 | { 58 | "y": 10, 59 | "x": 3 60 | }, 61 | { 62 | "y": 0, 63 | "x": 3 64 | }, 65 | { 66 | "y": 0, 67 | "x": 2 68 | } 69 | ], 70 | "name": "Local Hobbs", 71 | "squad": "" 72 | }, 73 | { 74 | "latency": "404.926", 75 | "id": "26debd4e-99fe-4e6f-90f0-4e812792cb17", 76 | "health": 68, 77 | "length": 3, 78 | "shout": "", 79 | "head": { 80 | "y": 10, 81 | "x": 2 82 | }, 83 | "customizations": { 84 | "color": "#5a25a8", 85 | "tail": "default", 86 | "head": "default" 87 | }, 88 | "body": [ 89 | { 90 | "y": 10, 91 | "x": 2 92 | }, 93 | { 94 | "y": 10, 95 | "x": 1 96 | }, 97 | { 98 | "y": 10, 99 | "x": 0 100 | } 101 | ], 102 | "name": "Local Irene", 103 | "squad": "" 104 | } 105 | ], 106 | "width": 11, 107 | "hazards": [], 108 | "height": 11, 109 | "food": [ 110 | { 111 | "y": 2, 112 | "x": 10 113 | }, 114 | { 115 | "y": 5, 116 | "x": 5 117 | }, 118 | { 119 | "y": 9, 120 | "x": 10 121 | }, 122 | { 123 | "y": 9, 124 | "x": 2 125 | }, 126 | { 127 | "y": 8, 128 | "x": 3 129 | } 130 | ] 131 | }, 132 | "game": { 133 | "source": "custom", 134 | "ruleset": { 135 | "version": "Mojave/3.5.2", 136 | "name": "wrapped", 137 | "settings": { 138 | "hazardDamagePerTurn": 14, 139 | "royale": { 140 | "shrinkEveryNTurns": 25 141 | }, 142 | "squad": { 143 | "sharedHealth": true, 144 | "sharedLength": true, 145 | "allowBodyCollisions": true, 146 | "sharedElimination": true 147 | }, 148 | "minimumFood": 1, 149 | "foodSpawnChance": 15 150 | } 151 | }, 152 | "timeout": 500, 153 | "id": "669d88a7-d7b6-4e67-987f-1eea43ec58de" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for battlesnake-rs on 2022-01-21T21:36:16-05:00 2 | 3 | app = "battlesnake-rs" 4 | 5 | kill_signal = "SIGINT" 6 | kill_timeout = 5 7 | processes = [] 8 | 9 | [env] 10 | FORCE_DEPLOY_BY_CHANGING_THIS_VARIABLE = "2" 11 | 12 | [experimental] 13 | allowed_public_ports = [] 14 | auto_rollback = true 15 | 16 | [[services]] 17 | http_checks = [] 18 | internal_port = 8000 19 | processes = ["app"] 20 | protocol = "tcp" 21 | script_checks = [] 22 | 23 | [services.concurrency] 24 | hard_limit = 4 25 | soft_limit = 1 26 | type = "connections" 27 | 28 | [[services.ports]] 29 | handlers = ["http"] 30 | port = 80 31 | 32 | [[services.ports]] 33 | handlers = ["tls", "http"] 34 | port = 443 35 | -------------------------------------------------------------------------------- /hurl_tests/end_constant_carter.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/constant-carter/end 2 | Content-Type: application/json 3 | file,fixtures/start_of_game.json; 4 | 5 | HTTP/1.1 204 6 | -------------------------------------------------------------------------------- /hurl_tests/fixtures/start_of_game.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "id": "813456", 4 | "ruleset": { "name": "standard", "version": "v.1.2.3" }, 5 | "timeout": 500 6 | }, 7 | "turn": 200, 8 | "you": { 9 | "health": 100, 10 | "id": "you", 11 | "name": "#22aa34", 12 | "body": [ 13 | { "x": 9, "y": 5 }, 14 | { "x": 9, "y": 5 }, 15 | { "x": 9, "y": 5 } 16 | ], 17 | "head": { "x": 9, "y": 5 }, 18 | "latency": null, 19 | "length": 3 20 | }, 21 | "board": { 22 | "food": [ 23 | { "x": 6, "y": 8 }, 24 | { "x": 0, "y": 2 }, 25 | { "x": 5, "y": 5 } 26 | ], 27 | "hazards": [], 28 | "height": 11, 29 | "width": 11, 30 | "snakes": [ 31 | { 32 | "health": 100, 33 | "id": "you", 34 | "name": "#22aa34", 35 | "body": [ 36 | { "x": 9, "y": 5 }, 37 | { "x": 9, "y": 5 }, 38 | { "x": 9, "y": 5 } 39 | ], 40 | "head": { "x": 9, "y": 5 }, 41 | "latency": null, 42 | "length": 3 43 | }, 44 | { 45 | "health": 100, 46 | "id": "#FF6c96", 47 | "name": "#FF6c96", 48 | "body": [ 49 | { "x": 5, "y": 9 }, 50 | { "x": 5, "y": 9 }, 51 | { "x": 5, "y": 9 } 52 | ], 53 | "head": { "x": 5, "y": 9 }, 54 | "latency": null, 55 | "length": 3 56 | }, 57 | { 58 | "health": 100, 59 | "id": "#FF6444", 60 | "name": "#FF6444", 61 | "body": [ 62 | { "x": 1, "y": 1 }, 63 | { "x": 1, "y": 1 }, 64 | { "x": 1, "y": 1 } 65 | ], 66 | "head": { "x": 1, "y": 1 }, 67 | "latency": null, 68 | "length": 3 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /hurl_tests/graph_mcts.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/mcts/graph 2 | Content-Type: application/json 3 | file,fixtures/start_of_game.json; 4 | 5 | HTTP/1.1 200 6 | -------------------------------------------------------------------------------- /hurl_tests/info_constant_carter.hurl: -------------------------------------------------------------------------------- 1 | GET http://localhost:3000/constant-carter 2 | 3 | HTTP/1.1 200 4 | 5 | [Asserts] 6 | jsonpath "$.author" == "coreyja" 7 | jsonpath "$.color" == "#AA66CC" 8 | jsonpath "$.head" == "trans-rights-scarf" 9 | -------------------------------------------------------------------------------- /hurl_tests/move_constant_carter.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/constant-carter/move 2 | Content-Type: application/json 3 | file,fixtures/start_of_game.json; 4 | 5 | HTTP/1.1 200 6 | 7 | [Asserts] 8 | jsonpath "$.move" == "right" 9 | -------------------------------------------------------------------------------- /hurl_tests/move_mcts.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/mcts/move 2 | Content-Type: application/json 3 | file,fixtures/start_of_game.json; 4 | 5 | HTTP/1.1 200 6 | -------------------------------------------------------------------------------- /hurl_tests/start_bombastic_bob.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/bombastic-bob/start 2 | 3 | HTTP/1.1 204 4 | -------------------------------------------------------------------------------- /hurl_tests/start_constant_carter.hurl: -------------------------------------------------------------------------------- 1 | POST http://localhost:3000/constant-carter/start 2 | 3 | HTTP/1.1 204 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /script/auto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | while true 4 | do 5 | cargo run --bin sherlock -- archive-snake --snake-url https://play.battlesnake.com/u/coreyja/tbd-mcts/ 6 | cargo run --bin sherlock -- archive-snake --snake-url https://play.battlesnake.com/u/coreyja/hovering-hobbs/ 7 | cargo run --bin sherlock -- archive-snake --snake-url https://play.battlesnake.com/u/jonathanarns/shapeshifter/ 8 | cargo run --bin sherlock -- archive-snake --snake-url https://play.battlesnake.com/u/jlafayette/snakebeard/ 9 | cargo run --bin sherlock -- archive-snake --snake-url https://play.battlesnake.com/u/waryferryman/jagwire/ 10 | sleep 600 11 | done 12 | -------------------------------------------------------------------------------- /script/average_of_ten_runs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare -i NUMBER_OF_RUNS=10 4 | 5 | sumss=0 6 | for i in $(seq $NUMBER_OF_RUNS) # you can also use {0..9} 7 | do 8 | declare -i thisRun 9 | thisRun=$(battlesnake play -g solo -n 'test' -u "http://localhost:8000/amphibious-arthur" -H 7 -W 7 |& grep 'DONE' | sed -n "s/.*Game completed after \(.*\) turn.*/\1/p") 10 | echo "Run $i: $thisRun" 11 | sumss=$((sumss+thisRun)) 12 | done 13 | 14 | echo "Average: $((sumss/NUMBER_OF_RUNS))" 15 | 16 | -------------------------------------------------------------------------------- /script/duels.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'yaml' 4 | 5 | BASE_URL = 'http://localhost:8000'.freeze 6 | RUNS = 1000 7 | 8 | CLI_RESULT_REGEX = /after (.*) turns\. (.*) is the winner/ 9 | 10 | def factorial(n) 11 | return 1 if n == 0 12 | 13 | (1..n).inject(:*) || 1 14 | end 15 | 16 | def combination(n, x) 17 | factorial(n) / (factorial(x) * factorial(n - x)) 18 | end 19 | 20 | def binomial(n, x, p) 21 | combination(n, x) * (p**x) * ((1 - p)**(n - x)) 22 | end 23 | 24 | def cumulative_probability(n, x, p) 25 | (x..n).sum { |i| binomial(n, i, p) } 26 | end 27 | 28 | class Snake 29 | attr_reader :name 30 | 31 | def initialize(name) 32 | @name = name 33 | end 34 | 35 | def url 36 | "#{BASE_URL}/#{name}" 37 | end 38 | end 39 | 40 | snake_names = %w[hovering-hobbs devious-devin] 41 | snakes = snake_names.map { |n| Snake.new(n) } 42 | 43 | snake_args = snakes.map { |s| "-n #{s.name} -u #{s.url}" }.join ' ' 44 | 45 | wins = {} 46 | draws = [] 47 | 48 | def print_output(wins) 49 | total_runs = wins.values.flatten.count 50 | 51 | big_winner, winning_turns = wins.max_by { |_, v| v.count } 52 | winning_pct = winning_turns.count.to_f / total_runs * 100.0 53 | 54 | puts 55 | puts 56 | puts "The big winner is ... #{big_winner}! They won #{winning_pct}% of the rounds (out of #{total_runs} total non draw rounds)" 57 | end 58 | 59 | def print_binomial(snakes, wins, draws) 60 | total_runs = wins.values.sum(&:count) + draws.count 61 | 62 | first_snake, second_snake = snakes 63 | wins_for_first_snake = wins[first_snake.name]&.count || 0 64 | # wins_for_second_snake = wins[second_snake].count || 0 65 | 66 | binomial_prob = binomial(total_runs, wins_for_first_snake, 0.5) 67 | cummulative_prob = cumulative_probability(total_runs, wins_for_first_snake, 0.5) 68 | 69 | puts "The cumulative probability of #{first_snake.name} being better than #{second_snake.name} is #{1 - cummulative_prob}" 70 | puts "The binomial probability of this result is #{binomial_prob}" 71 | 72 | if binomial_prob < 0.001 73 | # First snake is better 74 | puts "We reached signifigance!" 75 | 76 | if cummulative_prob > 0.5 77 | puts "#{first_snake.name} is better than #{second_snake.name} with a score thingy of #{cummulative_prob}!" 78 | else 79 | puts "#{second_snake.name} is better than #{first_snake.name} with a score thingy of #{1 - cummulative_prob}!" 80 | end 81 | 82 | true 83 | else 84 | puts "We don't have enoiugh data to say anything" 85 | puts 86 | 87 | false 88 | end 89 | end 90 | 91 | trap('SIGINT') do 92 | print_output(wins) 93 | exit! 94 | end 95 | 96 | (0...RUNS).each do |i| 97 | run_result = `battlesnake play #{snake_args} -H 11 -W 11 -t 500 2>&1 >/dev/null | tail -n1` 98 | match = CLI_RESULT_REGEX.match(run_result) 99 | if match 100 | turns, winning_name = match.captures 101 | 102 | wins[winning_name] ||= [] 103 | wins[winning_name] << turns 104 | 105 | puts "#{winning_name} won round #{i} in #{turns} turns" 106 | puts "Current Results: #{wins.map { |k, v| [k, v.count] }}" 107 | 108 | break if print_binomial(snakes, wins, draws) 109 | else 110 | draws << i 111 | puts "Round #{i} was a draw" 112 | end 113 | end 114 | 115 | print_output wins 116 | # print_binomial(snakes, wins, draws) 117 | 118 | # puts "FullResults\n#{wins.to_yaml}" 119 | -------------------------------------------------------------------------------- /script/hurl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | pushd $(git rev-parse --show-toplevel) 6 | hurl hurl_tests/*.hurl 7 | popd 8 | -------------------------------------------------------------------------------- /sherlock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sherlock" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde_json = "1.0.85" 10 | ureq = { version = "2.4.0", features = ["json"] } 11 | clap = { version = "4.0.32", features = ["derive"] } 12 | 13 | battlesnake-minimax = { path = "../battlesnake-minimax" } 14 | battlesnake-game-types = { workspace = true } 15 | itertools = "0.10.3" 16 | serde = { version = "1.0.144", features = ["derive"] } 17 | color-eyre = "0.6.2" 18 | axum = { version = "0.6.1", features = ["ws"] } 19 | tokio = { version = "1.21.0", features = ["full"] } 20 | tungstenite = { version = "0.18.0", features = ["rustls-tls-native-roots"] } 21 | url = { version = "2.3.1", features = ["serde"] } 22 | tower-http = { version = "0.3.4", features = ["cors"] } 23 | scraper = "0.14.0" 24 | colored = "2.0.0" 25 | term = "0.7.0" 26 | -------------------------------------------------------------------------------- /sherlock/README.md: -------------------------------------------------------------------------------- 1 | # Sherlock 2 | 3 | Sherlock is a standalone binary that provides various tools that may be useful for a Battlesnake Developer. 4 | 5 | Some of them may be documented here 6 | 7 | ## Replaying Games Howto 8 | 9 | ### Install Sherlock and move it to a place on your path 10 | 11 | ```bash 12 | # PreReq Install Rust 13 | # Rustup will help you there! 14 | # https://rustup.rs/ 15 | 16 | # Clone the repo 17 | git clone https://github.com/coreyja/battlesnake-rs.git 18 | cd battlesnake-rs/sherlock 19 | 20 | cargo build --release 21 | 22 | cp ../target/release/sherlock ~/bin/ 23 | ``` 24 | 25 | ### Archive a game 26 | 27 | ```bash 28 | sherlock archive --game-id 'GAME_ID_HERE' 29 | ``` 30 | 31 | This will save an archive of the game to `./archive` 32 | 33 | ### Replay an archive game 34 | 35 | ```bash 36 | sherlock replay archive 37 | ``` 38 | 39 | This will start the replay server on `localhost:8085` 40 | 41 | 42 | Now you just need to board a Board instance at your local replay server. 43 | We can even use the live board for this! 44 | 45 | We can navigate to something like the following to view a game we have saved in our archive! 46 | 47 | ``` 48 | https://board.battlesnake.com/?engine=http://localhost:8085&game=GAME_ID 49 | ``` 50 | -------------------------------------------------------------------------------- /sherlock/src/commands.rs: -------------------------------------------------------------------------------- 1 | pub mod archive; 2 | pub mod archive_snake; 3 | pub mod archive_user; 4 | pub mod fixture; 5 | pub mod replay; 6 | pub mod solve; 7 | 8 | use archive::Archive; 9 | use archive_snake::ArchiveSnake; 10 | use archive_user::ArchiveUser; 11 | use fixture::Fixture; 12 | use replay::Replay; 13 | use solve::Solve; 14 | 15 | use clap::Subcommand; 16 | use color_eyre::eyre::Result; 17 | 18 | #[derive(Debug, Subcommand)] 19 | pub(crate) enum Command { 20 | Solve(Solve), 21 | Fixture(Fixture), 22 | Archive(Archive), 23 | Replay(Replay), 24 | ArchiveSnake(ArchiveSnake), 25 | ArchiveUser(ArchiveUser), 26 | } 27 | 28 | impl Command { 29 | pub fn run(self) -> Result<()> { 30 | match self { 31 | Command::Solve(s) => s.run()?, 32 | Command::Fixture(f) => f.run()?, 33 | Command::Archive(a) => a.run()?, 34 | Command::Replay(r) => r.run()?, 35 | Command::ArchiveSnake(a) => a.run()?, 36 | Command::ArchiveUser(a) => a.run()?, 37 | } 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sherlock/src/commands/archive.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write, path::PathBuf}; 2 | 3 | use color_eyre::eyre::Result; 4 | use colored::Colorize; 5 | use serde_json::Value; 6 | use ureq::Error; 7 | 8 | use crate::{unofficial_api::get_frames_for_game, websockets::get_raw_messages_from_game}; 9 | 10 | #[derive(clap::Args, Debug)] 11 | pub(crate) struct Archive { 12 | /// Game ID to debug 13 | #[clap(short, long, value_parser)] 14 | game_id: String, 15 | 16 | #[clap(flatten)] 17 | shared: ArchiveShared, 18 | } 19 | 20 | #[derive(clap::Args, Debug, Clone, Default)] 21 | pub(crate) struct ArchiveShared { 22 | /// Directory to archive games to 23 | #[clap(short, long, value_parser, default_value = "archive")] 24 | archive_dir: PathBuf, 25 | 26 | /// Ignores local results and overwrite. Defaults to false 27 | #[clap(long, action, default_value = "false")] 28 | force: bool, 29 | } 30 | 31 | impl Archive { 32 | pub fn new(game_id: String, shared: ArchiveShared) -> Self { 33 | Self { game_id, shared } 34 | } 35 | 36 | pub(crate) fn run(self) -> Result<()> { 37 | let game_id = self.game_id; 38 | 39 | let game_dir = self.shared.archive_dir.join(&game_id); 40 | let game_info_path = game_dir.join("info.json"); 41 | 42 | let mut t = term::stdout().unwrap(); 43 | 44 | if game_info_path.is_file() && !self.shared.force { 45 | println!("🎉 Archive already exists for {game_id}"); 46 | 47 | return Ok(()); 48 | } 49 | 50 | println!( 51 | "{}", 52 | format!("⏳ Archive in progress for {game_id}").yellow() 53 | ); 54 | 55 | let game_details_resp = 56 | ureq::get(format!("https://engine.battlesnake.com/games/{game_id}").as_str()).call(); 57 | 58 | let game_details: Value = match game_details_resp { 59 | Ok(game_details) => game_details.into_json()?, 60 | Err(Error::Status(code, response)) => { 61 | if code == 404 { 62 | t.cursor_up()?; 63 | t.delete_line()?; 64 | println!( 65 | "{}", 66 | "❌ Game does not exist in engine (likely already deleted)".yellow() 67 | ); 68 | 69 | return Ok(()); 70 | } 71 | 72 | return Err(Error::Status(code, response).into()); 73 | } 74 | Err(e) => return Err(e.into()), 75 | }; 76 | 77 | let last_turn = game_details["LastFrame"]["Turn"].as_i64().unwrap() as usize; 78 | 79 | let frames = get_frames_for_game(&game_id, last_turn)?; 80 | 81 | std::fs::create_dir_all(game_dir.as_path())?; 82 | 83 | // Archive the Info 'raw' from the API 84 | { 85 | let contents = serde_json::to_string(&game_details)?; 86 | let mut file = File::create(game_info_path)?; 87 | file.write_all(contents.as_bytes())?; 88 | } 89 | 90 | // Archive the Frames 'raw' from the API 91 | { 92 | let frame_document: Result = 93 | frames.iter().map(|g| serde_json::to_string(&g)).collect(); 94 | let mut file = File::create(game_dir.join("frames.jsonl"))?; 95 | file.write_all(frame_document?.as_bytes())?; 96 | } 97 | 98 | // Archive the 'raw' WebSockets messages 99 | { 100 | let websocket_messages = get_raw_messages_from_game(&game_id)?; 101 | 102 | let document = websocket_messages.join("\n"); 103 | let mut file = File::create(game_dir.join("websockets.jsonl"))?; 104 | file.write_all(document.as_bytes())?; 105 | } 106 | 107 | t.cursor_up()?; 108 | t.delete_line()?; 109 | println!( 110 | "{}", 111 | format!("✔️ Archive created for game {game_id}").green() 112 | ); 113 | 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sherlock/src/commands/archive_snake.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use colored::Colorize; 3 | use scraper::{Html, Selector}; 4 | 5 | use crate::commands::archive::Archive; 6 | 7 | use super::archive::ArchiveShared; 8 | 9 | #[derive(clap::Args, Debug)] 10 | pub(crate) struct ArchiveSnake { 11 | /// The URL for the snake to archive 12 | #[clap(short, long, value_parser)] 13 | snake_url: String, 14 | 15 | #[clap(flatten)] 16 | shared: ArchiveShared, 17 | } 18 | 19 | impl ArchiveSnake { 20 | pub(crate) fn run(self) -> Result<()> { 21 | let res = ureq::get(&self.snake_url).call()?; 22 | let html_string = res.into_string()?; 23 | let document = Html::parse_document(&html_string); 24 | 25 | let snake_name: String = document 26 | .select(&Selector::parse(".page-header h1").unwrap()) 27 | .next() 28 | .unwrap() 29 | .text() 30 | .collect(); 31 | 32 | println!( 33 | "{}", 34 | format!("⏳🐍 Archive in progress for {snake_name}").yellow() 35 | ); 36 | 37 | for element in document.select(&Selector::parse(".list-group-item a").unwrap()) { 38 | let url = element 39 | .value() 40 | .attr("href") 41 | .expect("No URL found") 42 | .to_string(); 43 | assert!(url.starts_with("/g/")); 44 | let game_id = { 45 | let game_id = url.strip_prefix("/g/").unwrap(); 46 | let game_id = game_id.strip_suffix('/').unwrap(); 47 | game_id.to_string() 48 | }; 49 | 50 | Archive::new(game_id, self.shared.clone()).run()? 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn new(snake_url: String, shared: ArchiveShared) -> Self { 57 | Self { snake_url, shared } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sherlock/src/commands/archive_user.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::Result; 2 | use scraper::{Html, Selector}; 3 | 4 | use crate::commands::archive_snake::ArchiveSnake; 5 | 6 | use super::archive::ArchiveShared; 7 | 8 | #[derive(clap::Args, Debug)] 9 | pub(crate) struct ArchiveUser { 10 | /// The URL for the snake to archive 11 | #[clap(short, long, value_parser)] 12 | user_url: String, 13 | 14 | #[clap(flatten)] 15 | shared: ArchiveShared, 16 | } 17 | 18 | impl ArchiveUser { 19 | pub(crate) fn run(self) -> Result<()> { 20 | let res = ureq::get(&self.user_url).call()?; 21 | let html_string = res.into_string()?; 22 | let document = Html::parse_document(&html_string); 23 | 24 | let selector = 25 | Selector::parse("#tab-battlesnakes .list-group-item p:first-of-type a").unwrap(); 26 | 27 | for element in document.select(&selector) { 28 | let href = element 29 | .value() 30 | .attr("href") 31 | .expect("No URL found") 32 | .to_string(); 33 | assert!(href.starts_with("/u/")); 34 | 35 | let snake_url = format!("https://play.battlesnake.com{href}"); 36 | 37 | ArchiveSnake::new(snake_url, self.shared.clone()).run()? 38 | } 39 | 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sherlock/src/commands/fixture.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use color_eyre::eyre::Result; 4 | use serde_json::Value; 5 | 6 | use crate::unofficial_api::{frame_to_game, get_frame_for_turn}; 7 | 8 | #[derive(clap::Args, Debug)] 9 | pub struct Fixture { 10 | /// Game ID to debug 11 | #[clap(short, long, value_parser)] 12 | game_id: String, 13 | 14 | /// The name of the snake to use as "you" 15 | #[clap(short, long, value_parser)] 16 | you_name: String, 17 | 18 | /// Turn to make a fixture for 19 | #[clap(short, long, value_parser)] 20 | turn: i32, 21 | } 22 | 23 | impl Fixture { 24 | pub fn run(self) -> Result<()> { 25 | let game_id = self.game_id; 26 | let turn = self.turn; 27 | 28 | let body: Value = 29 | ureq::get(format!("https://engine.battlesnake.com/games/{game_id}").as_str()) 30 | .call()? 31 | .into_json()?; 32 | 33 | let frame = get_frame_for_turn(&game_id, self.turn)?; 34 | let wire_game = frame_to_game(&frame, &body["Game"], &self.you_name).unwrap(); 35 | 36 | let file = File::create(format!("./fixtures/{game_id}_{turn}.json"))?; 37 | serde_json::to_writer_pretty(file, &wire_game)?; 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sherlock/src/commands/replay.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::read_to_string, net::SocketAddr, path::PathBuf}; 2 | 3 | use clap::Subcommand; 4 | use color_eyre::eyre::Result; 5 | 6 | #[derive(clap::Args, Debug)] 7 | pub(crate) struct Replay { 8 | #[clap(subcommand)] 9 | command: ReplayCommand, 10 | } 11 | 12 | #[derive(Debug, Subcommand)] 13 | pub(crate) enum ReplayCommand { 14 | /// Start an engine that uses the local archive of Websocket Games 15 | Archive, 16 | /// Start the engine with a local file from the Rules repo output 17 | File(File), 18 | } 19 | 20 | #[derive(clap::Args, Debug)] 21 | pub(crate) struct File { 22 | /// File to replay 23 | #[clap(value_parser)] 24 | file: PathBuf, 25 | } 26 | 27 | use axum::{ 28 | extract::{ 29 | ws::{rejection::WebSocketUpgradeRejection, Message, WebSocket, WebSocketUpgrade}, 30 | Path, 31 | }, 32 | http::{Method, StatusCode}, 33 | response::{IntoResponse, Response}, 34 | routing::get, 35 | Json, Router, 36 | }; 37 | use serde_json::Value; 38 | use tower_http::cors::CorsLayer; 39 | 40 | use crate::websockets::rules_format_to_websocket; 41 | 42 | async fn game_handler(Path(game_id): Path) -> Response { 43 | println!("We got a game for {game_id}"); 44 | 45 | let (info, _frames, _end_frame) = rules_format_to_websocket( 46 | read_to_string("/Users/coreyja/Downloads/group_0_game_0.jsonl.txt").unwrap(), 47 | ); 48 | 49 | let game_info = if game_id == ":local:" { 50 | Ok(serde_json::to_string(&info).unwrap()) 51 | } else { 52 | read_to_string(format!("./archive/{game_id}/info.json")) 53 | }; 54 | 55 | match game_info { 56 | Ok(info) => IntoResponse::into_response(Json::( 57 | serde_json::from_str(&info) 58 | .expect("This should be safe since we just deserialized from json"), 59 | )), 60 | Err(e) => match e.kind() { 61 | std::io::ErrorKind::NotFound => StatusCode::NOT_FOUND.into_response(), 62 | _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), 63 | }, 64 | } 65 | } 66 | 67 | async fn websocket_handler( 68 | ws: Result, 69 | Path(game_id): Path, 70 | ) -> Response { 71 | println!("Websocket for game {game_id}"); 72 | 73 | let (_info, frames, end_frame) = rules_format_to_websocket( 74 | read_to_string("/Users/coreyja/Downloads/group_0_game_0.jsonl.txt").unwrap(), 75 | ); 76 | 77 | let game_lines = if game_id == ":local:" { 78 | let mut lines: Vec = vec![]; 79 | lines.extend(frames.iter().map(|s| serde_json::to_string(s).unwrap())); 80 | lines.push(serde_json::to_string(&end_frame).unwrap()); 81 | 82 | Ok(lines.join("\n")) 83 | } else { 84 | read_to_string(format!("./archive/{game_id}/websockets.jsonl")) 85 | }; 86 | match game_lines { 87 | Ok(l) => match ws { 88 | Ok(ws) => ws.on_upgrade(move |s| handle_socket(s, l)), 89 | Err(_) => "fallback".into_response(), 90 | }, 91 | Err(e) => match e.kind() { 92 | std::io::ErrorKind::NotFound => StatusCode::NOT_FOUND.into_response(), 93 | _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), 94 | }, 95 | } 96 | } 97 | 98 | async fn handle_socket(mut socket: WebSocket, lines: String) { 99 | for line in lines.lines() { 100 | let message: Message = Message::Text(line.to_string()); 101 | 102 | if socket.send(message).await.is_err() { 103 | return; 104 | } 105 | } 106 | 107 | let _ = socket.send(Message::Close(None)).await; 108 | println!("Closing websocket connection"); 109 | } 110 | 111 | impl Replay { 112 | pub(crate) fn run(self) -> Result<()> { 113 | tokio::runtime::Builder::new_multi_thread() 114 | .enable_all() 115 | .build() 116 | .unwrap() 117 | .block_on(async { 118 | let addr = SocketAddr::from(([0, 0, 0, 0], 8085)); 119 | 120 | let cors = CorsLayer::new() 121 | .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]) 122 | .allow_origin(tower_http::cors::Any) 123 | .allow_credentials(false); 124 | 125 | let app = Router::new() 126 | .route("/games/:game_id", get(game_handler)) 127 | .route("/games/:game_id/events", get(websocket_handler)) 128 | .layer(cors); 129 | 130 | axum::Server::bind(&addr) 131 | .serve(app.into_make_service()) 132 | .await 133 | .unwrap(); 134 | }); 135 | 136 | Ok(()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /sherlock/src/commands/solve.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt::Debug}; 2 | 3 | use battlesnake_game_types::{ 4 | compact_representation::{dimensions::Square, WrappedCellBoard}, 5 | types::{build_snake_id_map, Move, SnakeIDGettableGame, SnakeId, YouDeterminableGame}, 6 | }; 7 | use battlesnake_minimax::paranoid::{MinMaxReturn, MinimaxSnake, WrappedScore}; 8 | use color_eyre::eyre::Result; 9 | use itertools::Itertools; 10 | use serde_json::Value; 11 | 12 | use crate::unofficial_api::{frame_to_game, get_frame_for_turn}; 13 | 14 | #[derive(clap::Args, Debug)] 15 | pub(crate) struct Solve { 16 | /// Game ID to debug 17 | #[clap(short, long, value_parser)] 18 | game_id: String, 19 | 20 | /// Number of times to greet 21 | #[clap(short, long, value_parser)] 22 | you_name: String, 23 | 24 | /// Number of turns past the last frame to check 25 | #[clap(short, long, value_parser, default_value_t = 20)] 26 | turns_after_lose: i32, 27 | 28 | /// Turn to start looking back from. Uses the last turn of the game if not specified 29 | #[clap(short, long, value_parser)] 30 | search_starting_turn: Option, 31 | } 32 | 33 | impl Solve { 34 | pub(crate) fn run(self) -> Result<()> { 35 | let body: Value = 36 | ureq::get(format!("https://engine.battlesnake.com/games/{}", self.game_id).as_str()) 37 | .call()? 38 | .into_json()?; 39 | 40 | let last_frame = &body["LastFrame"]; 41 | let last_turn = last_frame["Turn"].as_i64().expect("Missing Turn") as i32; 42 | let mut current_turn = self.search_starting_turn.unwrap_or(last_turn - 1); 43 | 44 | loop { 45 | let current_frame = get_frame_for_turn(&self.game_id, current_turn)?; 46 | let wire_game = frame_to_game(¤t_frame, &body["Game"], &self.you_name); 47 | 48 | if wire_game.is_ok() { 49 | break; 50 | } 51 | println!("You were not alive at turn {current_turn} moving backwards"); 52 | 53 | current_turn -= 1; 54 | 55 | if current_turn < 0 { 56 | panic!("Something is wrong we made it past the end of the game"); 57 | } 58 | } 59 | 60 | let last_living_turn = current_turn; 61 | 62 | println!("Ending Turn {}", &last_frame["Turn"]); 63 | println!("Last Living Turn {last_living_turn}"); 64 | 65 | loop { 66 | let current_frame = get_frame_for_turn(&self.game_id, current_turn)?; 67 | let wire_game = frame_to_game(¤t_frame, &body["Game"], &self.you_name).unwrap(); 68 | 69 | if !wire_game.is_wrapped() { 70 | unimplemented!("Only implementing for wrapped games, RIGHT NOW"); 71 | } 72 | 73 | let snake_ids = build_snake_id_map(&wire_game); 74 | let game_info = wire_game.game.clone(); 75 | let game: WrappedCellBoard = 76 | wire_game.as_wrapped_cell_board(&snake_ids).unwrap(); 77 | 78 | let you_id = game.you_id(); 79 | 80 | let explorer_snake = 81 | MinimaxSnake::from_fn(game, game_info, current_turn, &|_| {}, "explorer"); 82 | 83 | let max_turns = (last_living_turn + 1 - current_turn + self.turns_after_lose) as usize; 84 | let result = explorer_snake.deepend_minimax_to_turn(max_turns); 85 | 86 | let score = *result.score(); 87 | 88 | if matches!(score, WrappedScore::Lose(..) | WrappedScore::Tie(..)) { 89 | println!("At turn {current_turn}, there were no safe options"); 90 | } else if matches!(score, WrappedScore::Win(_)) { 91 | println!("At turn {current_turn}, you could have won!"); 92 | if let MinMaxReturn::Node { options, .. } = &result { 93 | let winning_moves = options 94 | .iter() 95 | .filter(|(_, r)| matches!(r.score(), WrappedScore::Win(_))) 96 | .map(|(m, _)| *m) 97 | .collect_vec(); 98 | 99 | println!("At turn {current_turn}, the winning moves were {winning_moves:?}",); 100 | print_moves(&result, current_turn, winning_moves[0]); 101 | } 102 | break; 103 | } else if let MinMaxReturn::Node { 104 | options, 105 | moving_snake_id, 106 | .. 107 | } = &result 108 | { 109 | assert!(moving_snake_id == you_id); 110 | let safe_options = options 111 | .iter() 112 | .filter(|(_, r)| matches!(r.score(), WrappedScore::Scored(_))) 113 | .collect_vec(); 114 | let safe_moves = safe_options.iter().map(|(m, _)| *m).collect_vec(); 115 | 116 | println!("At turn {current_turn}, the safe options were {safe_moves:?}",); 117 | println!("Turn {current_turn} is the decision point"); 118 | 119 | for m in safe_moves { 120 | print_moves(&result, current_turn, m); 121 | } 122 | 123 | // let mut file = File::create("tmp.dot").unwrap(); 124 | // file.write_all(format!("{}", result.to_dot_graph(you_id)).as_bytes()) 125 | // .unwrap(); 126 | 127 | // Command::new("dot") 128 | // .arg("-Tsvg") 129 | // .arg("-O") 130 | // .arg("tmp.dot") 131 | // .output() 132 | // .unwrap(); 133 | // Command::new("open").arg("tmp.dot.svg").output().unwrap(); 134 | 135 | break; 136 | } else { 137 | panic!("We shouldn't ever have a leaf here") 138 | } 139 | 140 | current_turn -= 1; 141 | } 142 | 143 | Ok(()) 144 | } 145 | } 146 | 147 | fn print_moves( 148 | result: &MinMaxReturn, 149 | current_turn: i32, 150 | m: Move, 151 | ) where 152 | GameType: SnakeIDGettableGame + Debug + Clone, 153 | ScoreType: Copy + Ord + Debug, 154 | { 155 | let all_snake_path = result.chosen_route(); 156 | let sids = all_snake_path 157 | .iter() 158 | .map(|(sid, _)| sid) 159 | .unique() 160 | .collect_vec(); 161 | let mut paths_per_snake: HashMap> = HashMap::new(); 162 | for &sid in &sids { 163 | let path = all_snake_path 164 | .iter() 165 | .filter(|(s, _)| s == sid) 166 | .map(|(_, p)| p) 167 | .cloned() 168 | .collect_vec(); 169 | paths_per_snake.insert(*sid, path); 170 | } 171 | println!( 172 | "At turn {current_turn}, the {m} path takes {} turn lookahead:", 173 | all_snake_path.len() / sids.len() 174 | ); 175 | for (sid, path) in paths_per_snake { 176 | println!("{sid:?}: {}", path.iter().join(", ")); 177 | } 178 | println!() 179 | } 180 | -------------------------------------------------------------------------------- /sherlock/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains)] 2 | 3 | mod commands; 4 | mod unofficial_api; 5 | mod websockets; 6 | 7 | use color_eyre::eyre::Result; 8 | use commands::Command; 9 | 10 | use std::fmt::Debug; 11 | 12 | use clap::Parser; 13 | 14 | #[derive(Parser, Debug)] 15 | #[clap(author, version, about, long_about = None)] 16 | struct Args { 17 | #[clap(subcommand)] 18 | command: Command, 19 | } 20 | 21 | fn main() -> Result<()> { 22 | color_eyre::install()?; 23 | let args = Args::parse(); 24 | 25 | args.command.run()?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /sherlock/src/websockets.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::{ 2 | types::SizeDeterminableGame, 3 | wire_representation::{BattleSnake, Game, NestedGame, Position}, 4 | }; 5 | use color_eyre::eyre::Result; 6 | use itertools::Itertools; 7 | use serde::{Deserialize, Serialize}; 8 | use tungstenite::{connect, Message}; 9 | use url::Url; 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | struct Point { 13 | #[serde(rename = "X")] 14 | x: u32, 15 | #[serde(rename = "Y")] 16 | y: u32, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug)] 20 | struct Snake { 21 | #[serde(rename = "Author")] 22 | author: String, 23 | #[serde(rename = "Body")] 24 | body: Vec, 25 | #[serde(rename = "Color")] 26 | color: String, 27 | #[serde(rename = "Death")] 28 | death: Option, 29 | #[serde(rename = "Error")] 30 | error: Option, 31 | #[serde(rename = "HeadType")] 32 | head_type: String, 33 | #[serde(rename = "Health")] 34 | health: u32, 35 | #[serde(rename = "ID")] 36 | id: String, 37 | #[serde(rename = "IsBot")] 38 | is_bot: bool, 39 | #[serde(rename = "IsEnvironment")] 40 | is_environment: bool, 41 | #[serde(rename = "Latency")] 42 | latency: String, 43 | #[serde(rename = "Name")] 44 | name: String, 45 | #[serde(rename = "Shout")] 46 | shout: Option, 47 | #[serde(rename = "Squad")] 48 | squad: Option, 49 | #[serde(rename = "StatusCode")] 50 | status_code: u32, 51 | #[serde(rename = "TailType")] 52 | tail_type: String, 53 | } 54 | 55 | #[derive(Serialize, Deserialize, Debug)] 56 | pub(crate) struct Frame { 57 | #[serde(rename = "Food")] 58 | food: Vec, 59 | #[serde(rename = "Hazards")] 60 | hazards: Vec, 61 | #[serde(rename = "Snakes")] 62 | snakes: Vec, 63 | #[serde(rename = "Turn")] 64 | turn: u32, 65 | } 66 | 67 | #[derive(Serialize, Debug)] 68 | pub(crate) struct WrappedFrame { 69 | #[serde(rename = "Type")] 70 | t: String, 71 | #[serde(rename = "Data")] 72 | data: Frame, 73 | } 74 | 75 | #[derive(Serialize, Deserialize, Debug)] 76 | pub(crate) struct EndFrame { 77 | #[serde(rename = "Type")] 78 | t: String, 79 | #[serde(rename = "Data")] 80 | data: GameInfo, 81 | } 82 | 83 | impl From for WrappedFrame { 84 | fn from(f: Frame) -> Self { 85 | Self { 86 | t: "frame".into(), 87 | data: f, 88 | } 89 | } 90 | } 91 | 92 | impl From<&Position> for Point { 93 | fn from(p: &Position) -> Self { 94 | Point { 95 | x: p.x as u32, 96 | y: p.y as u32, 97 | } 98 | } 99 | } 100 | 101 | impl From<&BattleSnake> for Snake { 102 | fn from(s: &BattleSnake) -> Self { 103 | Snake { 104 | author: "Snakedev".into(), 105 | body: s.body.iter().map(|p| p.into()).collect_vec(), 106 | color: "".into(), 107 | death: None, 108 | error: None, 109 | head_type: "".into(), 110 | health: s.health as u32, 111 | id: s.id.clone(), 112 | is_bot: false, 113 | is_environment: false, 114 | latency: "".into(), 115 | name: s.name.clone(), 116 | shout: s.shout.clone(), 117 | squad: None, 118 | status_code: 200, 119 | tail_type: "".into(), 120 | } 121 | } 122 | } 123 | 124 | fn frame_from_game(input: Game, turn: u32) -> Frame { 125 | let snakes: Vec = input.board.snakes.iter().map(|s| s.into()).collect(); 126 | let hazards = vec![]; 127 | let food = vec![]; 128 | 129 | Frame { 130 | snakes, 131 | food, 132 | hazards, 133 | turn, 134 | } 135 | } 136 | 137 | #[derive(Serialize, Deserialize, Debug, Clone)] 138 | pub struct FrameRuleset { 139 | name: String, 140 | } 141 | 142 | #[derive(Serialize, Deserialize, Debug, Clone)] 143 | pub struct FrameGame { 144 | #[serde(rename = "Ruleset")] 145 | ruleset: FrameRuleset, 146 | } 147 | 148 | #[derive(Serialize, Deserialize, Debug, Clone)] 149 | pub struct GameInfo { 150 | #[serde(rename = "Game")] 151 | game: FrameGame, 152 | #[serde(rename = "Height")] 153 | height: u32, 154 | #[serde(rename = "Width")] 155 | width: u32, 156 | } 157 | 158 | pub(crate) fn rules_format_to_websocket(input: String) -> (GameInfo, Vec, EndFrame) { 159 | let mut lines = input.lines(); 160 | 161 | let header = lines.next().unwrap(); 162 | let inner_game: NestedGame = serde_json::from_str(header).unwrap(); 163 | 164 | let mut rest = lines.collect_vec(); 165 | let first_game = serde_json::from_str::(rest[0]).unwrap(); 166 | 167 | let _footer = rest.pop().unwrap(); 168 | 169 | let frames: Vec = rest 170 | .into_iter() 171 | .map(|s| serde_json::from_str::(s).unwrap()) 172 | .enumerate() 173 | .map(|(i, g)| frame_from_game(g, i as u32).into()) 174 | .collect_vec(); 175 | 176 | let game_info = GameInfo { 177 | game: FrameGame { 178 | ruleset: FrameRuleset { 179 | name: inner_game.ruleset.name, 180 | }, 181 | }, 182 | height: first_game.get_height(), 183 | width: first_game.get_width(), 184 | }; 185 | 186 | let end = EndFrame { 187 | t: "game_end".into(), 188 | data: game_info.clone(), 189 | }; 190 | 191 | (game_info, frames, end) 192 | } 193 | 194 | pub(crate) fn get_raw_messages_from_game(game_id: &str) -> Result> { 195 | let url = Url::parse(&format!( 196 | "wss://engine.battlesnake.com/games/{game_id}/events" 197 | ))?; 198 | let (mut socket, _response) = connect(url)?; 199 | 200 | let mut messages = vec![]; 201 | while let Ok(Message::Text(msg)) = socket.read_message() { 202 | messages.push(msg); 203 | } 204 | 205 | Ok(messages) 206 | } 207 | -------------------------------------------------------------------------------- /web-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-axum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | battlesnake-rs = { path = "../battlesnake-rs" } 8 | battlesnake-minimax = { path = "../battlesnake-minimax" } 9 | battlesnake-game-types = { workspace = true } 10 | 11 | axum = { version = "0.6.0" } 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0.68" 14 | tokio = { version = "1.0", features = ["full", "macros"] } 15 | tracing = "0.1" 16 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 17 | tower = "0.4.12" 18 | tracing-tree = "0.2.1" 19 | tower-http = { version = "0.3.4", features = ["tracing", "trace"] } 20 | axum-macros = "0.3.0" 21 | itertools = "0.10.5" 22 | parking_lot = "0.12.1" 23 | fxhash = "0.2.1" 24 | opentelemetry = { version = "0.18.0", features = ["rt-tokio"], default-features = false } 25 | opentelemetry-otlp = { version = "0.11.0", features = ["http-proto", "reqwest-rustls", "reqwest-client"], default-features = false } 26 | tracing-opentelemetry = "0.18.0" 27 | sentry = { version ="0.29.1", default-features = false, features = ["rustls", "backtrace", "contexts", "panic", "tower", "reqwest"] } 28 | sentry-tower = { version = "0.29.1", features = ["http"] } 29 | sentry-tracing = "0.29.1" 30 | color-eyre = "0.6.2" 31 | -------------------------------------------------------------------------------- /web-axum/src/hobbs.rs: -------------------------------------------------------------------------------- 1 | use battlesnake_game_types::types::Move; 2 | use battlesnake_minimax::{dashmap::DashMap, types::types::SnakeIDGettableGame}; 3 | use battlesnake_rs::{HeadGettableGame, HealthGettableGame, Vector}; 4 | use fxhash::FxBuildHasher; 5 | use parking_lot::Mutex; 6 | 7 | use crate::*; 8 | 9 | #[derive(Debug)] 10 | #[allow(dead_code)] 11 | pub(crate) struct AppState { 12 | pub game_states: HashMap, 13 | } 14 | 15 | #[derive(Debug, Clone)] 16 | pub(crate) struct GameState { 17 | pub last_move: Option, 18 | pub id_map: HashMap, 19 | #[allow(dead_code)] 20 | pub score_map: Arc>, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub(crate) struct LastMoveState { 25 | pub last_return: MinMaxReturn, 26 | pub last_board: StandardCellBoard4Snakes11x11, 27 | pub turn: i32, 28 | } 29 | 30 | impl GameState { 31 | pub fn new(id_map: HashMap) -> Self { 32 | Self { 33 | last_move: None, 34 | id_map, 35 | score_map: Arc::new(DashMap::with_capacity_and_hasher( 36 | 10_000_000, 37 | Default::default(), 38 | )), 39 | } 40 | } 41 | } 42 | pub(crate) async fn route_hobbs_info() -> impl IntoResponse { 43 | Json(Factory {}.about()) 44 | } 45 | pub(crate) async fn route_hobbs_start( 46 | State(state): State>>, 47 | Json(game): Json, 48 | ) -> impl IntoResponse { 49 | let id_map = build_snake_id_map(&game); 50 | let mut state = state.lock(); 51 | state 52 | .game_states 53 | .insert(game.game.id, GameState::new(id_map)); 54 | StatusCode::NO_CONTENT 55 | } 56 | pub(crate) async fn route_hobbs_end( 57 | State(state): State>>, 58 | Json(game): Json, 59 | ) -> impl IntoResponse { 60 | let mut state = state.lock(); 61 | state.game_states.remove(&game.game.id); 62 | StatusCode::NO_CONTENT 63 | } 64 | 65 | pub(crate) async fn route_hobbs_move( 66 | State(state): State>>, 67 | Json(game): Json, 68 | ) -> impl IntoResponse { 69 | let game_info = game.game.clone(); 70 | let game_id = game_info.id.to_string(); 71 | let turn = game.turn; 72 | 73 | let name = "hovering-hobbs"; 74 | 75 | let options: SnakeOptions = SnakeOptions { 76 | network_latency_padding: Duration::from_millis(150), 77 | move_ordering: MoveOrdering::BestFirst, 78 | }; 79 | 80 | let game_state = { 81 | let state_guard = state.lock(); 82 | 83 | state_guard 84 | .game_states 85 | .get(&game_id) 86 | .expect("If we hit the start endpoint we should have a game state already") 87 | .clone() 88 | }; 89 | let last_move = &game_state.last_move; 90 | 91 | let game = StandardCellBoard4Snakes11x11::convert_from_game(game, &game_state.id_map) 92 | .expect("TODO: We need to work on our error handling"); 93 | 94 | let you_id = game.you_id(); 95 | 96 | let initial_return = if let Some(last_move) = last_move 97 | && last_move.turn == turn - 1 98 | { 99 | let last_board = &last_move.last_board; 100 | let previously_alive_snakes = game_state 101 | .id_map 102 | .values() 103 | .filter(|sid| last_board.is_alive(sid)); 104 | 105 | let previous_heads: HashMap<&SnakeId, _> = previously_alive_snakes 106 | .map(|sid| (sid, last_board.get_head_as_position(sid))) 107 | .collect(); 108 | 109 | let current_snake_ids = game.get_snake_ids(); 110 | let currently_alive_snakes = current_snake_ids.iter().filter(|sid| game.is_alive(sid)); 111 | let current_heads = currently_alive_snakes.map(|sid| (sid, game.get_head_as_position(sid))); 112 | 113 | let mut snake_moves = HashMap::new(); 114 | 115 | for (sid, head) in current_heads { 116 | let previous_head = previous_heads 117 | .get(sid) 118 | .expect("If you are alive now you better have had a head last turn"); 119 | let previous_head_vector = previous_head.to_vector(); 120 | let current_head_vector = head.to_vector(); 121 | 122 | let x_diff = current_head_vector.x - previous_head_vector.x; 123 | let x_diff = match x_diff { 124 | 10 => -1, 125 | -10 => 1, 126 | x => x, 127 | }; 128 | let y_diff = current_head_vector.y - previous_head_vector.y; 129 | let y_diff = match y_diff { 130 | 10 => -1, 131 | -10 => 1, 132 | x => x, 133 | }; 134 | 135 | let move_vector = Vector { 136 | x: x_diff, 137 | y: y_diff, 138 | }; 139 | 140 | let m = Move::from_vector(move_vector); 141 | 142 | snake_moves.insert(sid, m); 143 | } 144 | 145 | let mut current_return = last_move.last_return.clone(); 146 | 147 | while let Some(moving_snake_id) = current_return.moving_snake_id() 148 | && let Some(m) = snake_moves.remove(moving_snake_id) 149 | && let Some(next_return) = current_return.option_for_move(m) 150 | { 151 | current_return = next_return.clone(); 152 | } 153 | 154 | while let MinMaxReturn::Node { 155 | ref options, 156 | moving_snake_id, 157 | .. 158 | } = current_return 159 | && moving_snake_id == *you_id 160 | { 161 | let new_return = options[0].1.clone(); 162 | current_return = new_return; 163 | } 164 | 165 | Some(current_return) 166 | } else { 167 | None 168 | }; 169 | 170 | let score = &standard_score::; 171 | // let score = CachedScore::new(score, game_state.score_map); 172 | 173 | let my_id = game.you_id(); 174 | let snake = ParanoidMinimaxSnake::new(game, game_info, turn, score, name, options); 175 | 176 | let (_depth, scored) = 177 | spawn_blocking_with_tracing(move || snake.choose_move_inner(initial_return)) 178 | .await 179 | .unwrap(); 180 | 181 | let scored_options = scored.first_options_for_snake(my_id).unwrap(); 182 | let output = scored_options.first().unwrap().0; 183 | 184 | { 185 | let mut state = state.lock(); 186 | 187 | let game_state = state 188 | .game_states 189 | .get_mut(&game_id) 190 | .expect("If we hit the start endpoint we should have a game state already"); 191 | 192 | let last_move = LastMoveState { 193 | last_return: scored, 194 | last_board: game, 195 | turn, 196 | }; 197 | game_state.last_move = Some(last_move); 198 | } 199 | 200 | let output: MoveOutput = MoveOutput { 201 | r#move: format!("{output}"), 202 | shout: None, 203 | }; 204 | 205 | Json(output) 206 | } 207 | -------------------------------------------------------------------------------- /web-lambda/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-lambda" 3 | version = "0.1.0" 4 | authors = ["Corey Alexander "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | battlesnake-rs = { path = "../battlesnake-rs" } 11 | http = "0.2.4" 12 | lambda_http = "0.3.0" 13 | serde_json = "1.0.64" 14 | tokio = "1.5.0" 15 | tracing = "0.1.26" 16 | tracing-subscriber = "0.2.20" 17 | -------------------------------------------------------------------------------- /web-lambda/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPT=$(realpath "$0") 6 | SCRIPTPATH=$(dirname "$SCRIPT") 7 | REPO_ROOT_PATH="$SCRIPTPATH/../.." 8 | 9 | if [ "$1" == "release" ]; then 10 | BUILD_ARGS="--release" 11 | TARGET_DIR="release" 12 | else 13 | TARGET_DIR="debug" 14 | fi 15 | 16 | function musl-build() { 17 | pushd "$REPO_ROOT_PATH" 18 | # docker run \ 19 | # -v cargo-git:/home/rust/.cargo/git \ 20 | # -v cargo-registry:/home/rust/.cargo/registry \ 21 | # -v "$PWD":/home/rust/src \ 22 | # --rm -it ekidd/rust-musl-builder:nightly cargo build -p web-lambda $BUILD_ARGS 23 | cargo build --target x86_64-unknown-linux-musl -p web-lambda $BUILD_ARGS 24 | popd 25 | } 26 | 27 | function lambdaify() { 28 | TARGET="$1" 29 | 30 | TMP_DIR="$(mktemp -d)" 31 | cp "$REPO_ROOT_PATH/target/x86_64-unknown-linux-musl/$TARGET_DIR/$TARGET" "$TMP_DIR/bootstrap" 32 | 33 | mkdir -p "$REPO_ROOT_PATH/target/lambda/" 34 | zip -j "$REPO_ROOT_PATH/target/lambda/$TARGET.zip" "$TMP_DIR/bootstrap" 35 | rm -r "$TMP_DIR" 36 | } 37 | 38 | musl-build 39 | 40 | lambdaify "web-lambda" 41 | -------------------------------------------------------------------------------- /web-lambda/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPT=$(realpath "$0") 6 | SCRIPTPATH=$(dirname "$SCRIPT") 7 | REPO_ROOT_PATH="$SCRIPTPATH/../.." 8 | 9 | pushd "$REPO_ROOT_PATH/web-lambda" > /dev/null 10 | ./scripts/build.sh release 11 | 12 | sam deploy 13 | popd > /dev/null 14 | -------------------------------------------------------------------------------- /web-lambda/src/main.rs: -------------------------------------------------------------------------------- 1 | use lambda_http::{ 2 | handler, 3 | lambda_runtime::{self, Context, Error}, 4 | Request, 5 | }; 6 | 7 | use serde_json::json; 8 | 9 | use battlesnake_rs::{all_factories, BoxedFactory, Game}; 10 | 11 | use tracing_subscriber::EnvFilter; 12 | 13 | use std::sync::Arc; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<(), Error> { 17 | let env_filter = EnvFilter::from_default_env(); 18 | tracing_subscriber::fmt() 19 | .with_env_filter(env_filter) 20 | .json() 21 | .flatten_event(true) 22 | .init(); 23 | 24 | let factories: Vec<_> = all_factories().into_iter().map(Arc::new).collect(); 25 | 26 | lambda_runtime::run(handler(move |request: Request, context: Context| { 27 | let path = request.uri().path(); 28 | let path_parts: Vec<&str> = path.split('/').filter(|x| x != &"").collect(); 29 | let snake_name = path_parts.first().cloned(); 30 | let factory = factories 31 | .iter() 32 | .find(|s| snake_name == Some(&s.name())) 33 | .cloned(); 34 | 35 | api_move(factory, request, context) 36 | })) 37 | .await?; 38 | 39 | Ok(()) 40 | } 41 | 42 | async fn api_move( 43 | factory: Option>, 44 | request: Request, 45 | _context: Context, 46 | ) -> Result> { 47 | let factory = factory.ok_or("Snake name not found")?; 48 | let path = request.uri().path(); 49 | let path_parts: Vec<&str> = path.split('/').filter(|x| x != &"").collect(); 50 | let action = path_parts.get(1); 51 | 52 | let string_body = if let lambda_http::Body::Text(s) = request.body() { 53 | Some(s) 54 | } else { 55 | None 56 | }; 57 | 58 | match action { 59 | None => Ok(json!(factory.about())), 60 | Some(&"start") => Ok(json!("Nothing to do in start")), 61 | Some(&"end") | Some(&"move") => { 62 | let string_body = string_body.ok_or("Body was not a string")?; 63 | let state: Game = serde_json::from_str(string_body)?; 64 | let snake = factory.create_from_wire_game(state); 65 | 66 | match action { 67 | Some(&"end") => Ok(json!(snake.end())), 68 | Some(&"move") => Ok(serde_json::to_value(snake.make_move()?)?), 69 | _ => unreachable!("Nested matches mean this is impossible if bad code"), 70 | } 71 | } 72 | _ => Err("unknown-action".into()), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /web-lambda/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | battlesnake-web-lambda 5 | 6 | Sample SAM Template for battlesnake-web-lambda 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 1 12 | Environment: 13 | Variables: 14 | LIBHONEY_DATASET: !Ref HoneycombDataset 15 | LIBHONEY_API_KEY: !Ref HoneycombApiKey 16 | RUST_LOG: info 17 | 18 | Parameters: 19 | HoneycombApiKey: 20 | Type: String 21 | Description: API Key for Honeycomb 22 | HoneycombDataset: 23 | Type: String 24 | Description: Honeycomb Dataset to send to 25 | Default: battlesnake-stats 26 | 27 | 28 | Resources: 29 | Devin: 30 | Type: AWS::Serverless::Function 31 | Properties: 32 | CodeUri: "../target/lambda/web-lambda.zip" 33 | Handler: N/A 34 | Runtime: provided 35 | MemorySize: 256 36 | Timeout: 10 37 | Events: 38 | WebApiEvent: 39 | Type: HttpApi 40 | Properties: 41 | Path: '/devious-devin/{path+}' 42 | Method: Any 43 | Hobbs: 44 | Type: AWS::Serverless::Function 45 | Properties: 46 | CodeUri: "../target/lambda/web-lambda.zip" 47 | Handler: N/A 48 | Runtime: provided 49 | MemorySize: 256 50 | Timeout: 10 51 | Events: 52 | WebApiEvent: 53 | Type: HttpApi 54 | Properties: 55 | Path: '/hovering-hobbs/{path+}' 56 | Method: Any 57 | BattlesnakeFunction: 58 | Type: AWS::Serverless::Function 59 | Properties: 60 | CodeUri: "../target/lambda/web-lambda.zip" 61 | Handler: N/A 62 | Runtime: provided 63 | MemorySize: 256 64 | Timeout: 1 65 | Events: 66 | WebApiEvent: 67 | Type: HttpApi # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 68 | 69 | Outputs: 70 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 71 | # Find out more about other implicit resources you can reference within SAM 72 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 73 | HelloWorldApi: 74 | Description: "API Gateway endpoint URL for Prod stage for Hello World function" 75 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 76 | HelloWorldFunction: 77 | Description: "Hello World Lambda Function ARN" 78 | Value: !GetAtt BattlesnakeFunction.Arn 79 | HelloWorldFunctionIamRole: 80 | Description: "Implicit IAM Role created for Hello World function" 81 | Value: !GetAtt BattlesnakeFunctionRole.Arn 82 | -------------------------------------------------------------------------------- /web-rocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-rocket" 3 | version = "0.1.0" 4 | authors = ["Corey Alexander "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | battlesnake-rs = { path = "../battlesnake-rs" } 11 | rocket = "0.4.10" 12 | rocket_contrib = "0.4.10" 13 | serde = "1.0" 14 | serde_json = "1.0" 15 | serde_derive = "1.0" 16 | rand = "0.8" 17 | itertools = "0.10.0" 18 | debug_print = "1.0.0" 19 | rocket_cors = "0.5.2" 20 | tracing-subscriber = { version = "0.3.0", features = [ "json" ] } 21 | tracing = "0.1.26" 22 | -------------------------------------------------------------------------------- /web-rocket/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | 3 | #[macro_use] 4 | extern crate rocket; 5 | 6 | use tracing::Subscriber; 7 | use tracing_subscriber::layer::SubscriberExt; 8 | 9 | use rocket::http::Status; 10 | 11 | use battlesnake_rs::{all_factories, AboutMe, BoxedFactory, Game, MoveOutput}; 12 | 13 | use rocket::State; 14 | 15 | use rocket_contrib::json::Json; 16 | 17 | #[post("/<_snake>/start")] 18 | fn api_start(_snake: String) -> Status { 19 | Status::NoContent 20 | } 21 | 22 | #[post("//end", data = "")] 23 | fn api_end( 24 | snake: String, 25 | factories: State>, 26 | game_state: Json, 27 | ) -> Option { 28 | let snake_ai = factories 29 | .iter() 30 | .find(|s| s.name() == snake)? 31 | .create_from_wire_game(game_state.into_inner()); 32 | snake_ai.end(); 33 | 34 | Some(Status::NoContent) 35 | } 36 | 37 | #[post("//move", data = "")] 38 | fn api_move( 39 | snake: String, 40 | factories: State>, 41 | game_state: Json, 42 | ) -> Option> { 43 | let snake_ai = factories 44 | .iter() 45 | .find(|s| s.name() == snake)? 46 | .create_from_wire_game(game_state.into_inner()); 47 | let m = snake_ai.make_move().ok()?; 48 | 49 | Some(Json(m)) 50 | } 51 | 52 | #[get("/")] 53 | fn api_about(snake: String, factories: State>) -> Option> { 54 | let factory = factories.iter().find(|s| s.name() == snake)?; 55 | Some(Json(factory.about())) 56 | } 57 | 58 | fn main() { 59 | let subscriber: Box = if std::env::var("JSON_LOGS").is_ok() { 60 | Box::new( 61 | tracing_subscriber::registry::Registry::default() 62 | .with(tracing_subscriber::filter::LevelFilter::DEBUG) 63 | .with(tracing_subscriber::fmt::Layer::default().json()), 64 | ) 65 | } else { 66 | Box::new( 67 | tracing_subscriber::registry::Registry::default() 68 | .with(tracing_subscriber::filter::LevelFilter::DEBUG) 69 | .with(tracing_subscriber::fmt::Layer::default()), 70 | ) 71 | }; 72 | // let layer = if let Ok(_) = std::env::var("JSON_LOGS") { 73 | // tracing_subscriber::fmt::Layer::default().json() 74 | // } else { 75 | // tracing_subscriber::fmt::Layer::default() 76 | // }; 77 | 78 | tracing::subscriber::set_global_default(subscriber).expect("setting global default failed"); 79 | 80 | let cors = rocket_cors::CorsOptions::default().to_cors().unwrap(); 81 | 82 | rocket::ignite() 83 | .manage(all_factories()) 84 | .attach(cors) 85 | .mount("/", routes![api_start, api_end, api_move, api_about]) 86 | .launch(); 87 | } 88 | --------------------------------------------------------------------------------