initial commit

This commit is contained in:
mos 2024-10-10 13:43:15 +02:00
commit f34c06758f
14 changed files with 1141 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

385
Cargo.lock generated Normal file
View File

@ -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",
]

9
Cargo.toml Normal file
View File

@ -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"

19
LICENSE Normal file
View File

@ -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.

32
README.md Normal file
View File

@ -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).

3
map/map1 Normal file
View File

@ -0,0 +1,3 @@
XXXXXX
X@*.gX
XXXXXX

11
map/map2 Normal file
View File

@ -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..........

12
map/map3 Normal file
View File

@ -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

11
map/map4 Normal file
View File

@ -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..........

11
map/map5 Normal file
View File

@ -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

11
map/map6 Normal file
View File

@ -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

7
map/map7 Normal file
View File

@ -0,0 +1,7 @@
XXXXXXX
Xg@.X.X
X.*...X
X..*..X
X.g*..X
X.....X
XXXXXXX

564
src/lib.rs Normal file
View File

@ -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<Cell>,
}
impl Index<Pos> 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<Pos> 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<R: std::io::BufRead>(file: R) -> Result<Self, ReadError> {
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::<Vec<Result<_, _>>>()
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { size, player, map })
}
fn read_rle(map: String) -> Result<Self, ReadError> {
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::<Vec<String>>()
.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<Direction>,
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::<usize>() % Self::DIR_TABLE.len()]
}
fn with_dna(state: State, dna: Vec<Direction>) -> Self {
Self {
state,
dna,
score: 0.0,
dir: 0,
goal: Solution::Ok,
}
}
fn new(state: State, size: usize) -> Self {
let dna: Vec<Direction> = (0..size).map(|_| Self::chromo()).collect();
Self::with_dna(state, dna)
}
fn cross(&mut self, seed: &Self, cr: usize) {
let ratio = (rand::random::<usize>() % 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::<usize>() % max;
let nd = rand::random::<usize>() % len;
for x in 0..ni {
self.dna[(nd + x) % len] = Agent::chromo();
}
}
fn move_dna(&mut self) -> Result<Solution, Error> {
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<Agent>,
}
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<Agent> = (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::<Result<Vec<()>, _>>()?;
self.life.par_sort_by(|a, b| b.score.total_cmp(&a.score));
if self.life[0].goal != Solution::Win {
let epop: Vec<Agent> = (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<String>) -> 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<Agent> = Vec::new();
for _ in 0..config.gen_depth {
let mut pop = Population::new(&config, map.clone());
if let Some(agent) = || -> Result<Option<Agent>, 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(())
}

65
src/main.rs Normal file
View File

@ -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<String>,
}
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);
}
}