factorio_blueprint/factorio-layout/src/layout.rs

614 lines
18 KiB
Rust

use factorio_core::{
pathfield::PathField,
prelude::*,
visualize::{Color, Symbol, Visualization, Visualize, image_grid},
};
use factorio_graph::priority_queue::binary_heap::FastBinaryHeap;
use factorio_pathfinding::belt_finding::{self, conflict_avoidance::ConflictAvoidance};
use rand::{Rng, seq::IndexedRandom};
use serde::{Deserialize, Serialize};
use std::{sync::atomic::AtomicU32, time::Instant};
static OUTFILEINDEX: AtomicU32 = AtomicU32::new(0);
pub struct GeneticAlgorithm<'a> {
problem: &'a Problem,
population: Vec<PathLayout<'a>>,
population_size: usize,
population_keep: usize,
population_new: usize,
}
impl<'a> GeneticAlgorithm<'a> {
pub fn new<R: Rng + ?Sized>(
problem: &'a Problem,
population_size: usize,
population_keep: usize,
population_new: usize,
rng: &mut R,
) -> GeneticAlgorithm<'a> {
let mut population = Vec::new();
let start = Instant::now();
let mut count: usize = 0;
while population.len() < population_size {
count += 1;
if let Some(p) = PathLayout::new(Layout::new(problem, rng)) {
population.push(p);
}
}
println!("Layouts accepted: {}/{}", population_size, count);
population.sort_by_key(|p| p.score());
println!(
"Best score: {}. Time: {:.2}s",
population[0].score(),
start.elapsed().as_secs_f32()
);
population[0].print_visualization();
GeneticAlgorithm {
problem,
population,
population_size,
population_keep,
population_new,
}
}
pub fn generation<R: Rng + ?Sized>(&mut self, rng: &mut R) {
let start_new = Instant::now();
for i in self.population_keep..(self.population_keep + self.population_new) {
loop {
if let Some(p) = PathLayout::new(Layout::new(self.problem, rng)) {
self.population[i] = p;
break;
}
}
}
let duration_new = start_new.elapsed();
let start_mutate = Instant::now();
for i in (self.population_keep + self.population_new)..self.population_size {
let j = i - (self.population_keep + self.population_new);
loop {
if let Some(p) =
PathLayout::new(self.population[j % self.population_keep].layout.mutate(rng))
{
self.population[i] = p;
break;
}
}
}
let duration_mutate = start_mutate.elapsed();
self.population.sort_by_key(|p| p.score());
println!(
"Best score: {}. Time new: {:.2}s. Time mutate: {:.2}s",
self.population[0].score(),
duration_new.as_secs_f32(),
duration_mutate.as_secs_f32()
);
self.population[0].print_visualization();
let v: Vec<_> = self.population.iter().map(|p| p.visualize()).collect();
let img = image_grid(&v, v[0].size().x as u32, v[0].size().y as u32, 5);
let mut file = std::fs::File::create("generation.png").unwrap();
img.write_to(&mut file, image::ImageFormat::Png).unwrap();
}
pub fn output_population(&self) {
println!("Population:");
for (i, p) in self.population.iter().enumerate() {
println!("{i:3}: {}", p.score());
p.print_visualization();
}
}
}
pub fn genetic_algorithm2<'a, R: Rng + ?Sized>(
problem: &'a Problem,
new_layouts: usize,
mutation_timeout: usize,
max_mutations: usize,
rng: &'_ mut R,
) -> PathLayout<'a> {
let mut m = (0..new_layouts)
.map(|_| valid_path_layout(problem, rng))
.min_by_key(|p| p.score())
.unwrap();
// m.print_visualization();
let mut last_improvement = 0;
let mut count = 0;
while last_improvement < mutation_timeout && count < max_mutations {
last_improvement += 1;
count += 1;
if let Some(p) = PathLayout::new(m.layout.mutate(rng)) {
if p.score() < m.score() {
m = p;
// println!("Step: {count}");
// m.print_visualization();
last_improvement = 0;
}
}
}
m
}
pub fn valid_path_layout<'a, R: Rng + ?Sized>(
problem: &'a Problem,
rng: &'_ mut R,
) -> PathLayout<'a> {
loop {
if let Some(p) = PathLayout::new(Layout::new(problem, rng)) {
return p;
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct MacroBlock {
pub(crate) size: Position,
pub(crate) input: Vec<Interface>,
pub(crate) output: Vec<Interface>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub(crate) struct Interface {
pub(crate) offset: Position,
pub(crate) dir: Direction,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub(crate) struct Connection {
pub(crate) startblock: usize,
pub(crate) startpoint: usize,
pub(crate) endblock: usize,
pub(crate) endpoint: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Problem {
pub(crate) size: Position,
pub(crate) blocks: Vec<MacroBlock>,
pub(crate) connections: Vec<Connection>,
}
// #[derive(Debug, Clone, Copy)]
// pub struct BlockHandle(usize);
#[derive(Debug, Clone)]
pub struct Layout<'a> {
pub(crate) problem: &'a Problem,
pub(crate) blocks: Vec<Block>,
}
pub struct PathLayout<'a> {
layout: Layout<'a>,
paths: Vec<Vec<PathField>>,
score: usize,
}
pub fn beltfinding_problem_from_layout(l: &Layout) -> belt_finding::Problem {
let mut p = belt_finding::Problem::new(l.problem.size.x as usize, l.problem.size.y as usize);
for b in &l.blocks {
let aabb = b.get_aabb();
p.set_blocked_range(
aabb.min().x as usize,
aabb.min().y as usize,
aabb.max().x as usize,
aabb.max().y as usize,
true,
);
}
for c in &l.problem.connections {
let start_transform = l.blocks[c.startblock].block_to_world();
let startpos = l.problem.blocks[c.startblock].output[c.startpoint]
.offset
.transform(start_transform);
let startdir = l.problem.blocks[c.startblock].output[c.startpoint]
.dir
.transform(start_transform);
let end_transform = l.blocks[c.endblock].block_to_world();
let endpos = l.problem.blocks[c.endblock].input[c.endpoint]
.offset
.transform(end_transform);
let enddir = l.problem.blocks[c.endblock].input[c.endpoint]
.dir
.transform(end_transform);
p.add_connection(
(startpos, startdir),
(endpos.in_direction(&enddir, -1), enddir),
);
}
p
}
impl<'a> PathLayout<'a> {
pub fn new(layout: Layout<'a>) -> Option<PathLayout<'a>> {
let mut p = beltfinding_problem_from_layout(&layout);
if !p.find_path::<FastBinaryHeap<_>>() {
return None;
}
let mut c = ConflictAvoidance::new(&p);
let start = std::time::Instant::now();
if !c.remove_all_conflicts(Some(std::time::Duration::from_secs(2))) {
if start.elapsed().as_secs_f32() > 0.5 {
// println!("Conflict avoidance: {:.2}", start.elapsed().as_secs_f32());
// c.print_visualization();
let file = std::fs::File::create(format!(
"out/{}.json",
OUTFILEINDEX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
))
.unwrap();
serde_json::to_writer(file, &p).unwrap();
// println!("Saved slow solve.");
}
return None;
}
let paths = c.get_paths().to_vec();
let score = paths
.iter()
.map(|path| path.iter().skip(1).map(|p| p.cost()).sum::<usize>())
.sum();
Some(PathLayout {
layout,
paths,
score,
})
}
pub fn score(&self) -> usize {
self.score
}
}
impl Visualize for PathLayout<'_> {
fn visualize(&self) -> factorio_core::visualize::Visualization {
let mut v = self.layout.visualize();
let offset = self.layout.blocks.len();
for (i, path) in self.paths.iter().enumerate() {
for p in &path[1..] {
match p {
PathField::Belt { pos, dir } => {
v.add_symbol(
*pos,
Symbol::Arrow(*dir),
Some(Color::index(i + offset)),
None,
);
}
PathField::Underground { pos, dir, len } => {
v.add_symbol(
*pos,
Symbol::ArrowEnter(*dir),
Some(Color::index(i + offset)),
None,
);
v.add_symbol(
pos.in_direction(dir, *len as i32),
Symbol::ArrowEnter(*dir),
Some(Color::index(i + offset)),
None,
);
}
}
}
}
v
}
}
impl Problem {
pub fn new(size: Position) -> Self {
Self {
size,
blocks: Vec::new(),
connections: Vec::new(),
}
}
// pub fn add_block(&mut self, size: Position) -> BlockHandle {
// self.blocks.push(Block {
// size,
// input: Vec::new(),
// output: Vec::new(),
// });
// BlockHandle(self.blocks.len() - 1)
// }
// pub fn add_connection(
// &mut self,
// starthandle: BlockHandle,
// startoffset: Position,
// endhandle: BlockHandle,
// endoffset: Position,
// ) {
// let startinterface = self.blocks[starthandle.0].output.len();
// let endinterface = self.blocks[endhandle.0].input.len();
// self.blocks[starthandle.0].output.push(Interface {
// offset: startoffset,
// target: (endhandle.0, endinterface),
// });
// self.blocks[endhandle.0].input.push(Interface {
// offset: endoffset,
// target: (starthandle.0, startinterface),
// })
// }
}
impl Layout<'_> {
/// Create a new valid layout
pub fn new<'a, R: Rng + ?Sized>(problem: &'a Problem, rng: &'_ mut R) -> Layout<'a> {
let mut blocks = Vec::new();
assert!(Self::place_block(problem, &mut blocks, rng));
let mut l = Layout { problem, blocks };
l.center();
l
}
fn place_block<R: Rng + ?Sized>(
problem: &'_ Problem,
blocks: &mut Vec<Block>,
rng: &'_ mut R,
) -> bool {
if problem.blocks.len() == blocks.len() {
return true;
}
let b = &problem.blocks[blocks.len()];
for _ in 0..1000 {
let dir = rng.random::<Direction>();
let pos = match dir {
Direction::Up => Position::new(
rng.random_range(0..=(problem.size.x - b.size.x)),
rng.random_range(0..=(problem.size.y - b.size.y)),
),
Direction::Right => Position::new(
rng.random_range((b.size.y - 1)..problem.size.x),
rng.random_range(0..=(problem.size.y - b.size.x)),
),
Direction::Down => Position::new(
rng.random_range((b.size.x - 1)..problem.size.x),
rng.random_range((b.size.y - 1)..problem.size.y),
),
Direction::Left => Position::new(
rng.random_range(0..=(problem.size.x - b.size.y)),
rng.random_range((b.size.x - 1)..problem.size.y),
),
};
let current = Block::new(pos, dir, problem.blocks[blocks.len()].size);
let current_aabb = current.get_aabb();
if blocks
.iter()
.all(|b| !AABB::collision(b.get_aabb(), current_aabb))
{
blocks.push(current);
if Self::place_block(problem, blocks, rng) {
return true;
}
blocks.pop();
}
}
false
}
pub fn get_aabb(&self) -> AABB {
self.blocks
.iter()
.map(|b| b.get_aabb())
.reduce(AABB::combine)
.expect("At least one block is required.")
}
pub fn center(&mut self) {
let aabb = self.get_aabb();
let rest = self.problem.size - aabb.size();
let new_min = Position::new(rest.x / 2, rest.y / 2);
let t = Transformation::new(Direction::Up, new_min - aabb.min());
for b in &mut self.blocks {
*b = b.transform(t);
}
}
/// Mutate existing layout, creating a valid layout
pub fn mutate<R: Rng + ?Sized>(&self, rng: &mut R) -> Self {
let mut s = self.clone();
#[allow(clippy::type_complexity)]
let r: &[(&dyn Fn(&mut Layout, &mut R) -> bool, _)] = &[
(&Self::mutate_replace::<R>, 30),
(&Self::mutate_flip::<R>, 50),
(&Self::mutate_jiggle::<R>, 160),
];
loop {
let p = r.choose_weighted(rng, |i| i.1).unwrap();
if p.0(&mut s, rng) && rng.random_bool(0.5) {
break;
}
}
s.center();
s
}
fn mutate_replace<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.random_range(0..layout.blocks.len());
let dir = rng.random::<Direction>();
let b = &layout.problem.blocks[i];
let pos = match dir {
Direction::Up => Position::new(
rng.random_range(0..=(layout.problem.size.x - b.size.x)),
rng.random_range(0..=(layout.problem.size.y - b.size.y)),
),
Direction::Right => Position::new(
rng.random_range((b.size.y - 1)..layout.problem.size.x),
rng.random_range(0..=(layout.problem.size.y - b.size.x)),
),
Direction::Down => Position::new(
rng.random_range((b.size.x - 1)..layout.problem.size.x),
rng.random_range((b.size.y - 1)..layout.problem.size.y),
),
Direction::Left => Position::new(
rng.random_range(0..=(layout.problem.size.x - b.size.y)),
rng.random_range((b.size.x - 1)..layout.problem.size.y),
),
};
let current = Block::new(pos, dir, b.size);
let current_aabb = current.get_aabb();
if layout
.blocks
.iter()
.enumerate()
.all(|(j, b)| j == i || !AABB::collision(b.get_aabb(), current_aabb))
{
layout.blocks[i] = current;
true
} else {
false
}
}
fn mutate_flip<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.random_range(0..layout.blocks.len());
let b = &mut layout.blocks[i];
let block = &layout.problem.blocks[i];
let new_pos = match b.dir() {
Direction::Up => b.pos() + block.size - Position::new(1, 1),
Direction::Right => b.pos() + Position::new(1 - block.size.y, block.size.x - 1),
Direction::Down => b.pos() - block.size + Position::new(1, 1),
Direction::Left => b.pos() + Position::new(block.size.y - 1, 1 - block.size.x),
};
let new_dir = b.dir().reverse();
*b = Block::new(new_pos, new_dir, b.size());
true
}
fn mutate_jiggle<R: Rng + ?Sized>(layout: &mut Layout, rng: &mut R) -> bool {
let i = rng.random_range(0..layout.blocks.len());
let dir = rng.random::<Direction>();
let step = [(1, 10), (2, 5), (3, 5)]
.choose_weighted(rng, |i| i.1)
.unwrap()
.0;
// let step = 1;
let b = &layout.blocks[i];
let current = Block::new(b.pos().in_direction(&dir, step), b.dir(), b.size());
let current_aabb = current.get_aabb();
if current_aabb.min().x < 0
|| current_aabb.min().y < 0
|| current_aabb.max().x >= layout.problem.size.x
|| current_aabb.max().y >= layout.problem.size.y
{
return false;
}
if layout
.blocks
.iter()
.enumerate()
.all(|(j, b)| j == i || !AABB::collision(b.get_aabb(), current_aabb))
{
layout.blocks[i] = current;
true
} else {
false
}
}
}
impl Visualize for Layout<'_> {
fn visualize(&self) -> Visualization {
let mut v = Visualization::new(self.problem.size);
for (i, (b, mb)) in self
.blocks
.iter()
.zip(self.problem.blocks.iter())
.enumerate()
{
let c = Color::index(i);
let aabb = b.get_aabb();
for x in aabb.min().x..=aabb.max().x {
for y in aabb.min().y..=aabb.max().y {
v.add_symbol(Position::new(x, y), Symbol::Block, Some(c), None);
}
}
v.add_symbol(b.pos(), Symbol::Char('X'), Some(c), None);
let transform = b.block_to_world();
for input in &mb.input {
v.add_symbol(
input.offset.transform(transform),
Symbol::Char('i'),
Some(c),
None,
);
}
for output in &mb.output {
v.add_symbol(
output.offset.transform(transform),
Symbol::Char('o'),
Some(c),
None,
);
}
}
v
}
}