From f34c06758faf1da72a5b5326634735cc6115d5a2 Mon Sep 17 00:00:00 2001 From: mos Date: Thu, 10 Oct 2024 13:43:15 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 385 +++++++++++++++++++++++++++++++++++ Cargo.toml | 9 + LICENSE | 19 ++ README.md | 32 +++ map/map1 | 3 + map/map2 | 11 + map/map3 | 12 ++ map/map4 | 11 + map/map5 | 11 + map/map6 | 11 + map/map7 | 7 + src/lib.rs | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 65 ++++++ 14 files changed, 1141 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 map/map1 create mode 100644 map/map2 create mode 100644 map/map3 create mode 100644 map/map4 create mode 100644 map/map5 create mode 100644 map/map6 create mode 100644 map/map7 create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5586768 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,385 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bano" +version = "0.1.0" +dependencies = [ + "clap", + "rand", + "rayon", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0aee6f5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bano" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.19", features = ["derive"] } +rand = "0.8.5" +rayon = "1.10.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc71fc6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 mos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ddd9fa --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# bano + +Sokoban solver using genetic algorithms. + +## Description + +bano is a sokoban solver that uses a genetic algorithm to calculate the most +optimal solution for a given map. The heuristic approach covered by the tool +is twofold and can be described as follows: + +1. Place the boxes in the given goals in as fewer steps as possible. While + this is not guaranteed, it can be improved either through subsequent iterations + or decreasing the search space (controllable via -d/--ds argument). +2. If any box is placed in such a way that the agent can no longer move it, this + is considered a losing condition and it is no longer evaluated. The evaluation + is scored by calculating the individual box distances and the highest scoring + agents end up as a parent to newly bred agents. + +The approach described above is not computationally efficient compared to others and +has a tendency to "halt" with more complex problem spaces, therefore it is more suitable +for smaller levels. To avoid early convergence one can tune the parameters according to the +complexity of the layout. + +## Usage + +`bano --nt 4 -d 4096 -m 256 --ps 8192 < map` + +`bano '8X|4.1@*gX|8X'` + +## Maps + +Some of the maps in the map directory were manually altered and are taken from [here](https://github.com/begoon/sokoban-maps/blob/master/maps/sokoban-maps-60-plain.txt). diff --git a/map/map1 b/map/map1 new file mode 100644 index 0000000..b3c4c03 --- /dev/null +++ b/map/map1 @@ -0,0 +1,3 @@ +XXXXXX +X@*.gX +XXXXXX diff --git a/map/map2 b/map/map2 new file mode 100644 index 0000000..6e1d7ce --- /dev/null +++ b/map/map2 @@ -0,0 +1,11 @@ +....XXXXX............. +....X...X............. +....X*..X............. +..XXX..*XXX........... +..X..*..*.X........... +XXX.X.XXX.X.....XXXXXX +X...X.XXX.XXXXXXX..ggX +X.*..*.............ggX +XXXXX.XXXX.X@XXXX..ggX +....X......XXX..XXXXXX +....XXXXXXXX.......... diff --git a/map/map3 b/map/map3 new file mode 100644 index 0000000..48607b4 --- /dev/null +++ b/map/map3 @@ -0,0 +1,12 @@ +XXXXXXXXXXXXXXXXXXXXXXXX +X............XXXXXXXXXXX +X..*.......@.XXXXXXXXXXX +XXXXXXXXX..XXXXXXXXXXXXX +XXXXXXXXX...*.........gX +XXXXXXXXX......XXXXXXXXX +XXXXXXXXX...XXXXXXXXXXXX +XXXXXXXXX............g.X +XXXXXXXXX.....*........X +XXXXXXXXX........g.....X +XXXXXXXXX.......*....g.X +XXXXXXXXXXXXXXXXXXXXXXXX diff --git a/map/map4 b/map/map4 new file mode 100644 index 0000000..efa080c --- /dev/null +++ b/map/map4 @@ -0,0 +1,11 @@ +....XXXXX............. +....X...X............. +....X*..X............. +..XXX..*XXX........... +..X..*..*.X........... +XXX.......X.....XXXXXX +X.........XXXXXXX...gX +X.*.g*...............X +XXXXX.XXXX.X@XXXX....X +....X......XXX..XXXXXX +....XXXXXXXX.......... diff --git a/map/map5 b/map/map5 new file mode 100644 index 0000000..db1f66f --- /dev/null +++ b/map/map5 @@ -0,0 +1,11 @@ +......XXXXX +XXXXXXX.@XX +X.....*..XX +X...*XX.*XX +XX*XgggX.XX +.X.*ggg..XX +.X.Xg.gX.XX +.X...X.X*.X +.X*..*....X +.X..XXXXXXX +.XXXXXXXXXX diff --git a/map/map6 b/map/map6 new file mode 100644 index 0000000..f6c74f9 --- /dev/null +++ b/map/map6 @@ -0,0 +1,11 @@ +XXXXXXXXXXXX +X.g....XX@XX +X..*.......X +Xgg...*..*.X +Xg.......*.X +Xg....*....X +XXXX...X.*.X +...X.*.X...X +...X.......X +...X.gXX.g.X +...XXXXXXXXX diff --git a/map/map7 b/map/map7 new file mode 100644 index 0000000..e6771a4 --- /dev/null +++ b/map/map7 @@ -0,0 +1,7 @@ +XXXXXXX +Xg@.X.X +X.*...X +X..*..X +X.g*..X +X.....X +XXXXXXX diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..22abdf7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,564 @@ +use rayon::prelude::*; +use std::ops::{Add, Index, IndexMut, Neg}; + +pub enum ReadError { + BadSymbol, +} + +pub enum Error { + Read(ReadError), + BadParam(String), + BadMove(Pos), +} + +#[derive(PartialEq, Copy, Clone)] +enum Direction { + U, + D, + L, + R, +} + +#[derive(PartialEq, Copy, Clone)] +enum Cell { + Wall, + Box, + Goal(bool), + None, +} + +#[derive(PartialEq, Clone)] +enum Solution { + Ok, + Win, + None, +} + +#[derive(PartialEq, Default, Clone, Copy)] +pub struct Pos(isize, isize); + +impl Add for Pos { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0, self.1 + other.1) + } +} + +impl Neg for Pos { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0, -self.1) + } +} + +impl std::fmt::Display for Pos { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.0, self.1) + } +} + +impl std::fmt::Display for ReadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReadError::BadSymbol => write!(f, "bad symbol"), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Read(e) => write!(f, "{e}"), + Error::BadParam(s) => write!(f, "{s}"), + Error::BadMove(pos) => write!(f, "invalid move ({pos})"), + } + } +} + +#[derive(Clone, Default)] +struct Level { + size: Pos, + player: Pos, + map: Vec, +} + +impl Index for Level { + type Output = Cell; + + fn index(&self, pos: Pos) -> &Self::Output { + &self.map[((pos.1 * self.size.0) + pos.0) as usize] + } +} + +impl IndexMut for Level { + fn index_mut(&mut self, pos: Pos) -> &mut Self::Output { + &mut self.map[((pos.1 * self.size.0) + pos.0) as usize] + } +} + +impl std::fmt::Display for Level { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for i in 0..self.size.1 { + for j in 0..self.size.0 { + if Pos(j, i) == self.player { + write!(f, "@")?; + } else { + write!( + f, + "{}", + match self[Pos(j, i)] { + Cell::Wall => 'X', + Cell::Box => '*', + Cell::Goal(true) => '!', + Cell::Goal(false) => '.', + Cell::None => ' ', + } + )?; + } + } + writeln!(f)?; + } + Ok(()) + } +} + +impl Level { + fn read(file: R) -> Result { + let (mut player, mut size) = (Pos::default(), Pos::default()); + + let map = file + .lines() + .flat_map(|l| { + let l = l.unwrap(); + + if l.len() > size.0 as usize { + size.0 = l.len() as isize; + } + + size.1 += 1; + l.char_indices() + .map(|(i, c)| match c { + '@' => { + player.0 = i as isize; + player.1 = size.1 - 1; + Ok(Cell::None) + } + 'X' => Ok(Cell::Wall), + '*' => Ok(Cell::Box), + 'g' => Ok(Cell::Goal(false)), + '.' => Ok(Cell::None), + _ => Err(ReadError::BadSymbol), + }) + .collect::>>() + }) + .collect::, _>>()?; + Ok(Self { size, player, map }) + } + + fn read_rle(map: String) -> Result { + let mut n: usize = 1; + + Self::read( + map.chars() + .filter_map(|c| match c { + '0'..='9' => { + n = c.to_digit(10).unwrap() as usize; + None + } + '|' => Some("\n".into()), + _ => Some(c.to_string().repeat(n)), + }) + .collect::>() + .join("") + .as_bytes(), + ) + } +} + +#[derive(Clone, Default)] +struct State { + lvl: Level, +} + +impl State { + fn new(lvl: Level) -> Self { + State { lvl } + } + + fn toggle_box(&mut self, pos: Pos) { + self.lvl[pos] = if self.lvl[pos] == Cell::Goal(true) { + Cell::Goal(false) + } else { + Cell::None + }; + } + + fn move_pos(&mut self, pos: Pos) -> Result<(bool, bool), Error> { + let dpos = self.lvl.player + pos; + + if ((dpos.1 * self.lvl.size.0) + dpos.0) as usize >= self.lvl.map.len() { + return Err(Error::BadMove(dpos)); + } + + match self.lvl[dpos] { + Cell::None | Cell::Goal(false) => self.lvl.player = dpos, + Cell::Box | Cell::Goal(true) => { + let bpos = dpos + pos; + + match self.lvl[bpos] { + Cell::None => { + self.toggle_box(dpos); + self.lvl[bpos] = Cell::Box; + self.lvl.player = dpos; + return Ok((false, false)); + } + Cell::Goal(false) => { + self.toggle_box(dpos); + self.lvl[bpos] = Cell::Goal(true); + self.lvl.player = dpos; + return Ok((true, false)); + } + _ => return Ok((false, true)), + } + } + _ => return Ok((false, true)), + } + + Ok((false, false)) + } + + fn move_dir(&mut self, dir: Direction) -> Result<(bool, bool), Error> { + match dir { + Direction::U => self.move_pos(Pos(0, -1)), + Direction::D => self.move_pos(Pos(0, 1)), + Direction::L => self.move_pos(Pos(-1, 0)), + Direction::R => self.move_pos(Pos(1, 0)), + } + } + + fn test_box(&self, pos: Pos, bt: bool) -> bool { + let dir: &[Pos] = &[Pos(0, -1), Pos(0, 1), Pos(-1, 0), Pos(1, 0)]; + let mut mbc = 0; + + for i in dir { + match self.lvl[pos + *i] { + Cell::Goal(false) | Cell::None => { + let dpos = pos + -*i; + + match self.lvl[dpos] { + Cell::Goal(false) | Cell::None => mbc += 1, + Cell::Box => { + if bt { + mbc += self.test_box(dpos, false) as usize + } + } + _ => (), + } + } + _ => (), + } + } + + mbc != 0 + } + + fn dist_box(&self, pos: Pos) -> usize { + let mut d = 0; + + for i in 0..self.lvl.size.1 { + for j in 0..self.lvl.size.0 { + if self.lvl[Pos(j, i)] == Cell::Goal(false) { + d += ((j as i32 - pos.0 as i32).abs() + (i as i32 - pos.1 as i32).abs()) + as usize; + } + } + } + + d + } + + fn dist_boxes(&self) -> usize { + let (mut max, mut rb) = (0, 0); + + for i in 0..self.lvl.size.1 { + for j in 0..self.lvl.size.0 { + if self.lvl[Pos(j, i)] == Cell::Box { + max += self.dist_box(Pos(j, i)); + rb += 1; + } + } + } + + max * rb + } + + fn evaluate(&self) -> Solution { + let mut cond = Solution::Win; + + for i in 0..self.lvl.size.1 { + for j in 0..self.lvl.size.0 { + match self.lvl[Pos(j, i)] { + Cell::Goal(false) => cond = Solution::Ok, + Cell::Box => { + if !self.test_box(Pos(j, i), true) { + return Solution::None; + } + } + _ => (), + } + } + } + + cond + } +} + +#[derive(Clone)] +struct Agent { + state: State, + dna: Vec, + score: f32, + dir: usize, + goal: Solution, +} + +impl Agent { + const DIR_TABLE: &'static [Direction] = + &[Direction::U, Direction::D, Direction::L, Direction::R]; + + fn chromo() -> Direction { + Self::DIR_TABLE[rand::random::() % Self::DIR_TABLE.len()] + } + + fn with_dna(state: State, dna: Vec) -> Self { + Self { + state, + dna, + score: 0.0, + dir: 0, + goal: Solution::Ok, + } + } + + fn new(state: State, size: usize) -> Self { + let dna: Vec = (0..size).map(|_| Self::chromo()).collect(); + + Self::with_dna(state, dna) + } + + fn cross(&mut self, seed: &Self, cr: usize) { + let ratio = (rand::random::() % cr) + 1; + let len = self.dna.len(); + + self.dna[..len / ratio].copy_from_slice(&seed.dna[..len / ratio]); + } + + fn mutate(&mut self, max: usize) { + let len = self.dna.len(); + let ni = rand::random::() % max; + let nd = rand::random::() % len; + + for x in 0..ni { + self.dna[(nd + x) % len] = Agent::chromo(); + } + } + + fn move_dna(&mut self) -> Result { + if self.dir == self.dna.len() || self.goal == Solution::None { + Ok(Solution::None) + } else { + let (mb, hit) = self.state.move_dir(self.dna[self.dir])?; + + if mb { + self.score += 0.00021; + } + + if hit { + self.score -= 0.00080; + } + + self.dir += 1; + Ok(self.state.evaluate()) + } + } + + fn evaluate(&mut self) -> Result<(), Error> { + loop { + let player = self.state.lvl.player; + + match self.move_dna()? { + Solution::Ok => (), + Solution::Win => { + self.score += 100000.0; + self.goal = Solution::Win; + break; + } + Solution::None => { + self.score -= 0.0032; + self.goal = Solution::None; + break; + } + } + + if player == self.state.lvl.player { + self.score -= 0.0010; + } + } + + self.score -= 1024.0 + self.state.dist_boxes() as f32; + Ok(()) + } + + fn solution(&self) -> String { + (0..self.dir) + .map(|i| match self.dna[i] { + Direction::U => "U", + Direction::D => "D", + Direction::L => "L", + Direction::R => "R", + }) + .collect() + } +} + +struct Population { + gen: usize, + epop_max: usize, + mut_max: usize, + cross_ratio: usize, + map: Level, + life: Vec, +} + +pub struct Config { + pub num_threads: usize, + pub dna_size: usize, + pub pop_size: usize, + pub epop_max: usize, + pub mut_max: usize, + pub gen_max: usize, + pub gen_depth: usize, + pub cross_ratio: usize, + pub map_output: bool, +} + +impl Population { + fn new(config: &Config, map: Level) -> Self { + let life: Vec = (0..config.pop_size) + .map(|_| Agent::new(State::new(map.clone()), config.dna_size)) + .collect(); + + Self { + gen: 0, + epop_max: config.epop_max, + mut_max: config.mut_max, + cross_ratio: config.cross_ratio, + map, + life, + } + } + + fn step(&mut self) -> Result<(), Error> { + self.life + .par_iter_mut() + .map(|a| a.evaluate()) + .collect::, _>>()?; + self.life.par_sort_by(|a, b| b.score.total_cmp(&a.score)); + + if self.life[0].goal != Solution::Win { + let epop: Vec = (0..self.epop_max) + .map(|i| { + let mut a = self.life[i].clone(); + + a.cross( + if i % 2 == 0 { + &self.life[(i + 1) % self.epop_max] + } else { + &self.life[(i - 1) % self.epop_max] + }, + self.cross_ratio, + ); + a + }) + .collect(); + + for i in epop.len()..self.life.len() { + let mut next = Agent::with_dna( + State::new(self.map.clone()), + if i % 2 == 0 { + epop[i % self.epop_max].dna.clone() + } else { + epop[(i + 1) % self.epop_max].dna.clone() + }, + ); + + next.mutate(self.mut_max); + self.life[i] = next; + } + } + + self.gen += 1; + Ok(()) + } +} + +pub fn run(config: Config, rle: Option) -> Result<(), Error> { + rayon::ThreadPoolBuilder::new() + .num_threads(config.num_threads) + .build_global() + .unwrap(); + + if config.pop_size == 0 { + return Err(Error::BadParam("population size cannot be 0".into())); + } + + if config.mut_max > config.dna_size { + return Err(Error::BadParam("mutation length too large".into())); + } + + if config.cross_ratio == 0 { + return Err(Error::BadParam("crossover ratio cannot be 0".into())); + } + + let map = if let Some(rle) = rle { + Level::read_rle(rle) + } else { + Level::read(std::io::stdin().lock()) + } + .map_err(Error::Read)?; + + let mut agents: Vec = Vec::new(); + + for _ in 0..config.gen_depth { + let mut pop = Population::new(&config, map.clone()); + + if let Some(agent) = || -> Result, Error> { + while pop.gen < config.gen_max { + pop.step()?; + + if config.map_output { + print!("{}", pop.life[0].state.lvl); + } + + println!("gen {} score {}", pop.gen, pop.life[0].score); + if pop.life[0].goal == Solution::Win { + return Ok(Some(pop.life[0].clone())); + } + } + + Ok(None) + }()? { + agents.push(agent); + } + } + + agents.par_sort_by_key(|a| a.dir); + + if !agents.is_empty() { + println!("{}", agents[0].solution()); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2623f11 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,65 @@ +use clap::Parser; + +#[derive(Parser)] +struct Args { + /// Number of threads + #[arg(short, long, default_value_t = 2)] + nt: usize, + + /// DNA strand size (increases search space) + #[arg(short, long, default_value_t = 1024)] + ds: usize, + + /// Population size + #[arg(long, default_value_t = 1024)] + ps: usize, + + /// Elite population max size + #[arg(short, long, default_value_t = 128)] + em: usize, + + /// Maximum mutation length + #[arg(short, long, default_value_t = 32)] + mt: usize, + + /// Maximum ratio for crossover reproduction + #[arg(long, default_value_t = 341)] + cr: usize, + + /// Maximum generations until next iteration + #[arg(long, default_value_t = 1048576)] + gm: usize, + + /// Generation iteration depth + #[arg(long, default_value_t = 1)] + gd: usize, + + /// Print map for each generation + #[arg(short, default_value_t = false)] + verbose: bool, + + /// Run-length encoded map + map: Option, +} + +fn main() { + let args = Args::parse(); + + if let Err(e) = bano::run( + bano::Config { + num_threads: args.nt, + dna_size: args.ds, + pop_size: args.ps, + epop_max: args.em, + mut_max: args.mt, + gen_max: args.gm, + gen_depth: args.gd, + cross_ratio: args.cr, + map_output: args.verbose, + }, + args.map, + ) { + eprintln!("error: {e}"); + std::process::exit(1); + } +}