organicomplex 0.7.0

Interactive complex-valued cellular automaton on 2D and 3D grids in search of that stuff - emergence, open-endedness, organicity etc.
/*
 * OrganiComplex
 *
 * Interactive complex-valued cellular automaton on 2D and 3D grids in search
 * of that stuff - emergence, open-endedness, organicity etc.
 * 
 * https://sunkware.org/organicomplex
 * 
 * mediator@sunkware.org
 * 
 * Copyright (c) 2026 Sunkware
 * 
 * OrganiComplex is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * OrganiComplex is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with OrganiComplex. If not, see <https://www.gnu.org/licenses/>.
 */

use serde::{
    Deserialize,
    Serialize
};

use std::fs::{
	self,
	File
};

mod control;
mod field;
mod neighbourhood;
mod render;

use crate::{
	base::{
		Complex,
		COMPLEX_ZERO,
		PrependErrorString
	},
	master::{
		Context,
		Mastermind,
		MSync,
		MTelemetry
	},
	sys::{
		Key,
		System
	}
};

use self::{
	control::MControl,
	field::Field,
	neighbourhood::Neighbourhood,
	render::MRender
};

const DEF_AMPLIFICATION: f64 = 1.0;
const DEF_NOISE: f64 = 0.01;
const DEF_SYNCHRONICITY: f64 = 1.0;

const FIELD_FILENAME: &str = "field.bin";
const NEIGHBOURHOOD_FILENAME: &str = "neighbourhood.bin";

const DEF_MAGLASS_RADIUS: i32 = 8;
const DEF_NBHOOD_RADIUS: i32 = 4;

const NUM_SPS_CYCLES: usize = 100;

const STATE_DIRNAME: &str = "automaton";
const STATE_FILENAME: &str = "automaton.json";

#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum EditMode {
	Field,
	Neighbourhood
}

#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum ColorMode {
	HueLum,
	Lum,
	Hue
}

#[derive(Serialize, Deserialize)]
pub struct Automaton {
	#[serde(skip, default="def_field")] pub field: Field,
	#[serde(skip, default="def_neighbourhood")] pub neighbourhood: Neighbourhood,

	pub amplification: f64,
    pub noise: f64,
	synchronicity: f64,

	pub edit_mode: EditMode,
	pub color_mode: ColorMode,

	pub pointer_field_u: i32,

	pub pointer_field_y: i32,
	pub pointer_field_x: i32,

	pub cell_size: i32,

	pub show_nbhood: bool,
	pub nbhood_radius: i32,
	pub nbhood_cell_size: i32,

	pub pointer_nbhood_y: i32,
	pub pointer_nbhood_x: i32,

	pub brush: Complex,
	pub brush_radius: i32,
	pub show_palette: bool,

	pub nbhood_du: isize,

	pub show_maglass: bool,
	pub maglass_radius: i32,
	pub maglass_cell_size: i32,

	pub show_field: bool,
	pub scale_lum: bool,

	pub manual_step: bool,

	pub show_settings_stats: bool,

	#[serde(skip, default="def_t_last_cycle")] pub t_last_cycle: i128,
	#[serde(skip, default="def_dur_cycles")] pub dur_cycles: Vec<i128>,
	#[serde(skip, default="def_i_cycle")] pub i_cycle: usize
}

fn def_field() -> Field {
    Field::new_default()
}

fn def_neighbourhood() -> Neighbourhood {
	Neighbourhood::new_default()
}

fn def_t_last_cycle() -> i128 {
	0
}

fn def_dur_cycles() -> Vec<i128> {
	vec![0i128; NUM_SPS_CYCLES]
}

fn def_i_cycle() -> usize {
	0
}

impl Automaton {
	pub fn new_default(sys: &System) -> Self {
		let field = def_field();
		let (f_height, f_width) = (field.height(), field.width());

		Self{
			field,
			neighbourhood: def_neighbourhood(),
			amplification: DEF_AMPLIFICATION,
			noise: DEF_NOISE,
			synchronicity: DEF_SYNCHRONICITY,
			edit_mode: EditMode::Field,
			color_mode: ColorMode::HueLum,
			pointer_field_u: 0,
			pointer_field_y: 0,
			pointer_field_x: 0,
			cell_size: 1,
			show_nbhood: true,
			nbhood_radius: DEF_NBHOOD_RADIUS,
			nbhood_cell_size: (((f_height - 1 + f_width) as i32) / (2 * DEF_NBHOOD_RADIUS + 1)).max(1),
			pointer_nbhood_y: 0,
			pointer_nbhood_x: 0,
			brush: COMPLEX_ZERO,
			brush_radius: 4,
			show_palette: true,
			nbhood_du: 0,
			show_maglass: true,
			maglass_radius: DEF_MAGLASS_RADIUS,
			maglass_cell_size: ((sys.height() - ((f_height - 1 + f_width) as i32)) / (2 * DEF_MAGLASS_RADIUS + 1)).max(1),
			show_field: true,
			scale_lum: false,
			manual_step: false,
			show_settings_stats: true,
			t_last_cycle: def_t_last_cycle(),
			dur_cycles: def_dur_cycles(),
			i_cycle: def_i_cycle()
		}
	}

	pub fn count_new_cycle(&mut self, t: i128) {
		self.dur_cycles[self.i_cycle] = t - self.t_last_cycle;
		self.t_last_cycle = t;
		self.i_cycle = (self.i_cycle + 1) % NUM_SPS_CYCLES;
	}

	pub fn sps(&self) -> f64 {
		1e6 * (NUM_SPS_CYCLES as f64) / (self.dur_cycles.iter().sum::<i128>() as f64)
	}

	pub fn load(dirpath: impl ToString) -> Result<Self, String> {
		let dirpath = format!("{}/{}", dirpath.to_string(), STATE_DIRNAME);

		let state_filepath = format!("{}/{}", &dirpath, STATE_FILENAME);
		File::open(&state_filepath).map_err(|e| e.to_string())?;
		
       	let json = fs::read(&state_filepath).pre_err(format!("cannot read from '{}'", &state_filepath))?;
        
		let mut automaton: Self = serde_json::from_slice(&json).pre_err(format!("cannot deserialize (inflated) '{}' to state", &state_filepath))?;
		
		automaton.field = Field::load(& format!("{}/{}", &dirpath, FIELD_FILENAME))?;
		automaton.neighbourhood = Neighbourhood::load(& format!("{}/{}", &dirpath, NEIGHBOURHOOD_FILENAME))?;

        Ok(automaton)
    }

    pub fn save(&self, dirpath: impl ToString) -> Result<(), String> {
		let dirpath = format!("{}/{}", dirpath.to_string(), STATE_DIRNAME);

		fs::create_dir_all(&dirpath).unwrap_or(());

		self.field.save(& format!("{}/{}", dirpath, FIELD_FILENAME))?;
		self.neighbourhood.save(& format!("{}/{}", dirpath, NEIGHBOURHOOD_FILENAME))?;

		let state_filepath = format!("{}/{}", &dirpath, STATE_FILENAME);

        let json = serde_json::to_vec_pretty(self).pre_err("cannot serialize state")?;
        fs::write(&state_filepath, &json).pre_err(format!("cannot write to '{}'", &state_filepath))?; // no buffering, write all at once

        Ok(())
    }

}

pub fn play(ctx: &mut Context) -> Result<(), String> {
	ctx.lingua.load(&["automaton"])?;

	let mut mastermind = Mastermind::new();

	mastermind.add(vec![
		MControl::new(),
		MRender::new(),
		MTelemetry::new(),
		MSync::new(),
	]);

	mastermind.start(ctx)?;

	while (!ctx.ether.quit) && (!ctx.ether.replay) {

		mastermind.run(ctx)?;

		// Input handling... after sync

		if !ctx.ether.meta {
			// Menu
			if ctx.sys.poll_key(Key::Escape) {
				ctx.ether.meta = true;
			}
		}

        // More or less?..

    }

	mastermind.finish(ctx)?;

    Ok(())
}