6f4fa6c967
- Makes methods fallible when they create textures. - Correctly handle window resize in fltk example. - TODO: tests - Closes #240
340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
#![deny(clippy::all)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
use log::{debug, error};
|
|
use pixels::{Error, Pixels, SurfaceTexture};
|
|
use winit::{
|
|
dpi::LogicalSize,
|
|
event::{Event, VirtualKeyCode},
|
|
event_loop::{ControlFlow, EventLoop},
|
|
window::WindowBuilder,
|
|
};
|
|
use winit_input_helper::WinitInputHelper;
|
|
|
|
const WIDTH: u32 = 400;
|
|
const HEIGHT: u32 = 300;
|
|
|
|
fn main() -> Result<(), Error> {
|
|
env_logger::init();
|
|
let event_loop = EventLoop::new();
|
|
let mut input = WinitInputHelper::new();
|
|
|
|
let window = {
|
|
let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64);
|
|
let scaled_size = LogicalSize::new(WIDTH as f64 * 3.0, HEIGHT as f64 * 3.0);
|
|
WindowBuilder::new()
|
|
.with_title("Conway's Game of Life")
|
|
.with_inner_size(scaled_size)
|
|
.with_min_inner_size(size)
|
|
.build(&event_loop)
|
|
.unwrap()
|
|
};
|
|
|
|
let mut pixels = {
|
|
let window_size = window.inner_size();
|
|
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
|
Pixels::new(WIDTH, HEIGHT, surface_texture)?
|
|
};
|
|
|
|
let mut life = ConwayGrid::new_random(WIDTH as usize, HEIGHT as usize);
|
|
let mut paused = false;
|
|
|
|
let mut draw_state: Option<bool> = None;
|
|
|
|
event_loop.run(move |event, _, control_flow| {
|
|
// The one and only event that winit_input_helper doesn't have for us...
|
|
if let Event::RedrawRequested(_) = event {
|
|
life.draw(pixels.get_frame_mut());
|
|
if let Err(err) = pixels.render() {
|
|
error!("pixels.render() failed: {}", err);
|
|
*control_flow = ControlFlow::Exit;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// For everything else, for let winit_input_helper collect events to build its state.
|
|
// It returns `true` when it is time to update our game state and request a redraw.
|
|
if input.update(&event) {
|
|
// Close events
|
|
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
|
|
*control_flow = ControlFlow::Exit;
|
|
return;
|
|
}
|
|
if input.key_pressed(VirtualKeyCode::P) {
|
|
paused = !paused;
|
|
}
|
|
if input.key_pressed_os(VirtualKeyCode::Space) {
|
|
// Space is frame-step, so ensure we're paused
|
|
paused = true;
|
|
}
|
|
if input.key_pressed(VirtualKeyCode::R) {
|
|
life.randomize();
|
|
}
|
|
// Handle mouse. This is a bit involved since support some simple
|
|
// line drawing (mostly because it makes nice looking patterns).
|
|
let (mouse_cell, mouse_prev_cell) = input
|
|
.mouse()
|
|
.map(|(mx, my)| {
|
|
let (dx, dy) = input.mouse_diff();
|
|
let prev_x = mx - dx;
|
|
let prev_y = my - dy;
|
|
|
|
let (mx_i, my_i) = pixels
|
|
.window_pos_to_pixel((mx, my))
|
|
.unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
|
|
|
|
let (px_i, py_i) = pixels
|
|
.window_pos_to_pixel((prev_x, prev_y))
|
|
.unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
|
|
|
|
(
|
|
(mx_i as isize, my_i as isize),
|
|
(px_i as isize, py_i as isize),
|
|
)
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if input.mouse_pressed(0) {
|
|
debug!("Mouse click at {mouse_cell:?}");
|
|
draw_state = Some(life.toggle(mouse_cell.0, mouse_cell.1));
|
|
} else if let Some(draw_alive) = draw_state {
|
|
let release = input.mouse_released(0);
|
|
let held = input.mouse_held(0);
|
|
debug!("Draw at {mouse_prev_cell:?} => {mouse_cell:?}");
|
|
debug!("Mouse held {held:?}, release {release:?}");
|
|
// If they either released (finishing the drawing) or are still
|
|
// in the middle of drawing, keep going.
|
|
if release || held {
|
|
debug!("Draw line of {draw_alive:?}");
|
|
life.set_line(
|
|
mouse_prev_cell.0,
|
|
mouse_prev_cell.1,
|
|
mouse_cell.0,
|
|
mouse_cell.1,
|
|
draw_alive,
|
|
);
|
|
}
|
|
// If they let go or are otherwise not clicking anymore, stop drawing.
|
|
if release || !held {
|
|
debug!("Draw end");
|
|
draw_state = None;
|
|
}
|
|
}
|
|
// Resize the window
|
|
if let Some(size) = input.window_resized() {
|
|
if let Err(err) = pixels.resize_surface(size.width, size.height) {
|
|
error!("pixels.resize_surface() failed: {err}");
|
|
*control_flow = ControlFlow::Exit;
|
|
return;
|
|
}
|
|
}
|
|
if !paused || input.key_pressed_os(VirtualKeyCode::Space) {
|
|
life.update();
|
|
}
|
|
window.request_redraw();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Generate a pseudorandom seed for the game's PRNG.
|
|
fn generate_seed() -> (u64, u64) {
|
|
use byteorder::{ByteOrder, NativeEndian};
|
|
use getrandom::getrandom;
|
|
|
|
let mut seed = [0_u8; 16];
|
|
|
|
getrandom(&mut seed).expect("failed to getrandom");
|
|
|
|
(
|
|
NativeEndian::read_u64(&seed[0..8]),
|
|
NativeEndian::read_u64(&seed[8..16]),
|
|
)
|
|
}
|
|
|
|
const BIRTH_RULE: [bool; 9] = [false, false, false, true, false, false, false, false, false];
|
|
const SURVIVE_RULE: [bool; 9] = [false, false, true, true, false, false, false, false, false];
|
|
const INITIAL_FILL: f32 = 0.3;
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct Cell {
|
|
alive: bool,
|
|
// Used for the trail effect. Always 255 if `self.alive` is true (We could
|
|
// use an enum for Cell, but it makes several functions slightly more
|
|
// complex, and doesn't actually make anything any simpler here, or save any
|
|
// memory, so we don't)
|
|
heat: u8,
|
|
}
|
|
|
|
impl Cell {
|
|
fn new(alive: bool) -> Self {
|
|
Self { alive, heat: 0 }
|
|
}
|
|
|
|
#[must_use]
|
|
fn update_neibs(self, n: usize) -> Self {
|
|
let next_alive = if self.alive {
|
|
SURVIVE_RULE[n]
|
|
} else {
|
|
BIRTH_RULE[n]
|
|
};
|
|
self.next_state(next_alive)
|
|
}
|
|
|
|
#[must_use]
|
|
fn next_state(mut self, alive: bool) -> Self {
|
|
self.alive = alive;
|
|
if self.alive {
|
|
self.heat = 255;
|
|
} else {
|
|
self.heat = self.heat.saturating_sub(1);
|
|
}
|
|
self
|
|
}
|
|
|
|
fn set_alive(&mut self, alive: bool) {
|
|
*self = self.next_state(alive);
|
|
}
|
|
|
|
fn cool_off(&mut self, decay: f32) {
|
|
if !self.alive {
|
|
let heat = (self.heat as f32 * decay).clamp(0.0, 255.0);
|
|
assert!(heat.is_finite());
|
|
self.heat = heat as u8;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct ConwayGrid {
|
|
cells: Vec<Cell>,
|
|
width: usize,
|
|
height: usize,
|
|
// Should always be the same size as `cells`. When updating, we read from
|
|
// `cells` and write to `scratch_cells`, then swap. Otherwise it's not in
|
|
// use, and `cells` should be updated directly.
|
|
scratch_cells: Vec<Cell>,
|
|
}
|
|
|
|
impl ConwayGrid {
|
|
fn new_empty(width: usize, height: usize) -> Self {
|
|
assert!(width != 0 && height != 0);
|
|
let size = width.checked_mul(height).expect("too big");
|
|
Self {
|
|
cells: vec![Cell::default(); size],
|
|
scratch_cells: vec![Cell::default(); size],
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
fn new_random(width: usize, height: usize) -> Self {
|
|
let mut result = Self::new_empty(width, height);
|
|
result.randomize();
|
|
result
|
|
}
|
|
|
|
fn randomize(&mut self) {
|
|
let mut rng: randomize::PCG32 = generate_seed().into();
|
|
for c in self.cells.iter_mut() {
|
|
let alive = randomize::f32_half_open_right(rng.next_u32()) > INITIAL_FILL;
|
|
*c = Cell::new(alive);
|
|
}
|
|
// run a few simulation iterations for aesthetics (If we don't, the
|
|
// noise is ugly)
|
|
for _ in 0..3 {
|
|
self.update();
|
|
}
|
|
// Smooth out noise in the heatmap that would remain for a while
|
|
for c in self.cells.iter_mut() {
|
|
c.cool_off(0.4);
|
|
}
|
|
}
|
|
|
|
fn count_neibs(&self, x: usize, y: usize) -> usize {
|
|
let (xm1, xp1) = if x == 0 {
|
|
(self.width - 1, x + 1)
|
|
} else if x == self.width - 1 {
|
|
(x - 1, 0)
|
|
} else {
|
|
(x - 1, x + 1)
|
|
};
|
|
let (ym1, yp1) = if y == 0 {
|
|
(self.height - 1, y + 1)
|
|
} else if y == self.height - 1 {
|
|
(y - 1, 0)
|
|
} else {
|
|
(y - 1, y + 1)
|
|
};
|
|
self.cells[xm1 + ym1 * self.width].alive as usize
|
|
+ self.cells[x + ym1 * self.width].alive as usize
|
|
+ self.cells[xp1 + ym1 * self.width].alive as usize
|
|
+ self.cells[xm1 + y * self.width].alive as usize
|
|
+ self.cells[xp1 + y * self.width].alive as usize
|
|
+ self.cells[xm1 + yp1 * self.width].alive as usize
|
|
+ self.cells[x + yp1 * self.width].alive as usize
|
|
+ self.cells[xp1 + yp1 * self.width].alive as usize
|
|
}
|
|
|
|
fn update(&mut self) {
|
|
for y in 0..self.height {
|
|
for x in 0..self.width {
|
|
let neibs = self.count_neibs(x, y);
|
|
let idx = x + y * self.width;
|
|
let next = self.cells[idx].update_neibs(neibs);
|
|
// Write into scratch_cells, since we're still reading from `self.cells`
|
|
self.scratch_cells[idx] = next;
|
|
}
|
|
}
|
|
std::mem::swap(&mut self.scratch_cells, &mut self.cells);
|
|
}
|
|
|
|
fn toggle(&mut self, x: isize, y: isize) -> bool {
|
|
if let Some(i) = self.grid_idx(x, y) {
|
|
let was_alive = self.cells[i].alive;
|
|
self.cells[i].set_alive(!was_alive);
|
|
!was_alive
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn draw(&self, screen: &mut [u8]) {
|
|
debug_assert_eq!(screen.len(), 4 * self.cells.len());
|
|
for (c, pix) in self.cells.iter().zip(screen.chunks_exact_mut(4)) {
|
|
let color = if c.alive {
|
|
[0, 0xff, 0xff, 0xff]
|
|
} else {
|
|
[0, 0, c.heat, 0xff]
|
|
};
|
|
pix.copy_from_slice(&color);
|
|
}
|
|
}
|
|
|
|
fn set_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, alive: bool) {
|
|
// probably should do sutherland-hodgeman if this were more serious.
|
|
// instead just clamp the start pos, and draw until moving towards the
|
|
// end pos takes us out of bounds.
|
|
let x0 = x0.clamp(0, self.width as isize);
|
|
let y0 = y0.clamp(0, self.height as isize);
|
|
for (x, y) in line_drawing::Bresenham::new((x0, y0), (x1, y1)) {
|
|
if let Some(i) = self.grid_idx(x, y) {
|
|
self.cells[i].set_alive(alive);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn grid_idx<I: std::convert::TryInto<usize>>(&self, x: I, y: I) -> Option<usize> {
|
|
if let (Ok(x), Ok(y)) = (x.try_into(), y.try_into()) {
|
|
if x < self.width && y < self.height {
|
|
Some(x + y * self.width)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|