use std::{f32::consts::PI, f64::consts::TAU}; use macroquad::{prelude::*, rand::gen_range}; use crate::{ asteroids::Asteroid, nn::{ActivationFunc, NN}, HEIGHT, WIDTH, }; const NUM_KEYS: usize = 4; const INPUTS_PER_ASTEROID: usize = 4; const NUM_ASTEROIDS: usize = 1; const INPUTS_FOR_SHIP: usize = 2; const VALUES_PER_MEMORY: usize = 1; const NUM_MEMORIES: usize = 0; #[derive(Default)] pub struct Player { pub pos: Vec2, vel: Vec2, acc: f32, pub dir: Vec2, rot: f32, drag: f32, bullets: Vec, asteroids: Vec>, inputs: Vec, pub outputs: Vec, last_shot: u32, shot_interval: u32, pub brain: Option, alive: bool, pub lifespan: u32, pub shots: u32, memory: std::collections::VecDeque, } impl Player { pub fn new( config: Option>, mut_rate: Option, activ: Option, ) -> Self { Self { brain: match config { Some(mut c) => { c.retain(|&x| x != 0); // Number of inputs c.insert( 0, (INPUTS_PER_ASTEROID * NUM_ASTEROIDS) + INPUTS_FOR_SHIP + (VALUES_PER_MEMORY * NUM_MEMORIES), ); // Number of outputs c.push( NUM_KEYS + if NUM_MEMORIES > 0 { VALUES_PER_MEMORY } else { 0 }, ); Some(NN::new(c, mut_rate.unwrap(), activ.unwrap())) } _ => None, }, dir: vec2(0., -1.), rot: 1.5 * PI, // Change scaling when passing inputs if this is changed drag: 0.001, shot_interval: 18, alive: true, shots: 4, // 4 outputs, 1 for memory outputs: vec![0.; NUM_KEYS + VALUES_PER_MEMORY], memory: vec![0.; VALUES_PER_MEMORY * NUM_MEMORIES].into(), ..Default::default() } } pub fn check_player_collision(&mut self, asteroid: &Asteroid) -> bool { // Save the asteroid to our asteroids self.asteroids.push(Some(asteroid.clone())); if asteroid.check_collision(self.pos, 8.) || self.lifespan > 3600 && self.brain.is_some() { self.alive = false; return true; } false } pub fn consider_asteroids(pos: Vec2, asteroids: &mut Vec>) { // Consider the closest asteroids first asteroids.sort_by_key(|ast| match ast { None => i32::MAX, Some(ast) => (dist_wrapping(ast.pos, pos, ast.radius) * 100.) as i32, }); // Cull if there are too may asteroids *asteroids = asteroids.iter().cloned().take(NUM_ASTEROIDS).collect(); // Insert if there are not enought asteroids if asteroids.len() < NUM_ASTEROIDS { for _ in 0..NUM_ASTEROIDS - asteroids.len() { asteroids.push(None); } } assert_eq!(asteroids.len(), NUM_ASTEROIDS); } pub fn check_bullet_collisions(&mut self, asteroid: &mut Asteroid) -> bool { for bullet in &mut self.bullets { if asteroid.check_collision(bullet.pos, 0.) { asteroid.alive = false; bullet.alive = false; return true; } } false } pub fn update(&mut self) { self.lifespan += 1; self.last_shot += 1; self.acc = 0.; self.outputs = vec![0.; 4]; let mut keys = vec![false; 4]; self.inputs = vec![]; // Insert all the asteroid data Self::consider_asteroids(self.pos, &mut self.asteroids); for ast in &self.asteroids { if let Some(ast) = ast { self.inputs.extend_from_slice(&[ // Distance to asteroid dist_wrapping(ast.pos, self.pos, ast.radius), // Angle to asteroid self.dir.angle_between(ast.pos - self.pos), // Asteroid velocity x (ast.vel - self.vel).x * 0.6, // Asteroid velocity y (ast.vel - self.vel).y * 0.6, ]); } else { self.inputs.extend_from_slice(&[0., 0., 0., 0.]); } } assert_eq!(self.inputs.len(), NUM_ASTEROIDS * INPUTS_PER_ASTEROID); // Insert the ship data self.inputs.push(self.rot / TAU as f32); self.inputs.push( (self.shot_interval as f32 - self.last_shot as f32).max(0.) / self.shot_interval as f32, ); // Insert the memories for memory in &self.memory { self.inputs.push(memory.min(1.).max(-1.)); } // Run the brain if let Some(brain) = &self.brain { assert_eq!(self.inputs.len(), brain.config[0] - 1); self.outputs = brain.feed_forward(&self.inputs); if NUM_MEMORIES > 0 { self.memory.push_back(self.outputs[self.outputs.len() - 1]); self.memory.pop_front(); } keys = self .outputs .iter() .map(|&x| { x > if brain.activ_func == ActivationFunc::Sigmoid { 0.85 } else { 0. } }) .collect(); } if keys[0] || self.brain.is_none() && is_key_down(KeyCode::Right) { // RIGHT self.rot = (self.rot + 0.1 + TAU as f32) % TAU as f32; self.dir = vec2(self.rot.cos(), self.rot.sin()); } if keys[1] || self.brain.is_none() && is_key_down(KeyCode::Left) { // LEFT self.rot = (self.rot - 0.1 + TAU as f32) % TAU as f32; self.dir = vec2(self.rot.cos(), self.rot.sin()); } if keys[2] || self.brain.is_none() && is_key_down(KeyCode::Up) { // THROTTLE self.acc = 0.14; } if keys[3] || self.brain.is_none() && is_key_down(KeyCode::Space) { if self.last_shot > self.shot_interval { self.last_shot = 0; self.shots += 1; self.bullets.push(Bullet { pos: self.pos + self.dir * 20., vel: self.dir * 8.5 + self.vel, alive: true, travelled: Vec2::new(0., 0.), }); } } self.vel += self.acc * self.dir - self.drag * self.vel.length() * self.vel; self.pos += self.vel; if self.pos.x.abs() > WIDTH * 0.5 + 10. { self.pos.x *= -1.; } if self.pos.y.abs() > HEIGHT * 0.5 + 10. { self.pos.y *= -1.; } for bullet in &mut self.bullets { bullet.update(); } self.bullets.retain(|b| b.alive); self.asteroids = vec![]; } pub fn draw(&self, color: Color, debug: bool) { let p1 = self.pos + self.dir * 20.; let p2 = self.pos + self.dir.rotate(vec2(-18., -12.667)); let p3 = self.pos + self.dir.rotate(vec2(-18., 12.667)); let p4 = self.pos + self.dir.rotate(vec2(-10., -10.)); let p5 = self.pos + self.dir.rotate(vec2(-10., 10.)); let p6 = self.pos + self.dir * -25.; let p7 = self.pos + self.dir.rotate(vec2(-10., -6.)); let p8 = self.pos + self.dir.rotate(vec2(-10., 6.)); draw_line(p1.x, p1.y, p2.x, p2.y, 2., color); draw_line(p1.x, p1.y, p3.x, p3.y, 2., color); draw_line(p4.x, p4.y, p5.x, p5.y, 2., color); if self.acc > 0. && gen_range(0., 1.) < 0.4 { draw_triangle_lines(p6, p7, p8, 2., color); } if debug { let mut debug_asteroids = self.asteroids.clone(); Self::consider_asteroids(self.pos, &mut debug_asteroids); for asteroid in &debug_asteroids { if let Some(ast) = asteroid { draw_circle_lines(ast.pos.x, ast.pos.y, ast.radius, 1., RED); // let p = self.pos // + self.dir.rotate(Vec2::from_angle(self.asteroid_data[0].1)) // * self.asteroid_data[0].0 // * WIDTH; draw_line(self.pos.x, self.pos.y, ast.pos.x, ast.pos.y, 1., RED); } } // Draw raycasts // for (i, r) in self.raycasts.iter().enumerate() { // let dir = Vec2::from_angle(PI / 4. * i as f32).rotate(self.dir); // draw_line( // self.pos.x, // self.pos.y, // self.pos.x + dir.x * 100. / r, // self.pos.y + dir.y * 100. / r, // 1., // GRAY, // ); // } } for bullet in &self.bullets { bullet.draw(color); } } pub fn draw_brain(&self, width: f32, height: f32, bias: bool) { if let Some(brain) = &self.brain { brain.draw(width, height, &self.inputs, &self.outputs, bias); } } } struct Bullet { pos: Vec2, vel: Vec2, alive: bool, travelled: Vec2, } impl Bullet { fn update(&mut self) { self.pos += self.vel; if self.pos.x.abs() > WIDTH * 0.5 { self.pos.x *= -1.; } if self.pos.y.abs() > HEIGHT * 0.5 { self.pos.y *= -1.; } self.travelled += self.vel; if self.travelled.length() >= (WIDTH * WIDTH + HEIGHT * HEIGHT).sqrt() / 2. { self.alive = false; } } fn draw(&self, c: Color) { draw_circle(self.pos.x, self.pos.y, 2., Color::new(c.r, c.g, c.b, 0.9)); } } // Distance in a toroidal space: // https://blog.demofox.org/2017/10/01/calculating-the-distance-between-points-in-wrap-around-toroidal-space/ fn dist_wrapping(a: Vec2, b: Vec2, r: f32) -> f32 { let mut dx = (a.x - b.x).abs(); let mut dy = (a.y - b.y).abs(); if dx > (WIDTH as f32 / 2.) { dx = WIDTH as f32 - dx; } if dy > (HEIGHT as f32 / 2.) { dy = HEIGHT as f32 - dy; } ((dx * dx + dy * dy).sqrt() - r) / (WIDTH * WIDTH + HEIGHT * HEIGHT).sqrt() }