This commit is contained in:
JOLIMAITRE Matthieu 2022-09-04 01:12:34 +02:00
commit 9c96c9f828
11 changed files with 2239 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1749
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "pixel_fight_rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.50"
clap = { version = "3.2.20", features = ["derive"] }
rand = "0.8.5"
rayon = "1.5.3"
ron = "0.8.0"
serde = { version = "1.0.144", features = ["derive"] }
speedy2d = "1.8.0"

22
fight_configs/300_000.ron Normal file
View file

@ -0,0 +1,22 @@
(
teams: [
(
color: (255, 0, 0),
position: (0.0, 0.0),
radius: 100.0,
count: 100000,
),
(
color: (0, 255, 0),
position: (0.0, 1000.0),
radius: 100.0,
count: 100000,
),
(
color: (0, 0, 255),
position: (800.0, 500.0),
radius: 100.0,
count: 100000,
),
],
)

22
fight_configs/30_000.ron Normal file
View file

@ -0,0 +1,22 @@
(
teams: [
Team (
color: (255, 0, 0),
position: (0.0, 0.0),
radius: 100.0,
count: 10000,
),
Team (
color: (0, 255, 0),
position: (0.0, 1000.0),
radius: 100.0,
count: 10000,
),
Team (
color: (0, 0, 255),
position: (800.0, 500.0),
radius: 100.0,
count: 10000,
),
],
)

22
fight_configs/default.ron Normal file
View file

@ -0,0 +1,22 @@
(
teams: [
(
color: (255, 0, 0),
position: (0.0, 0.0),
radius: 100.0,
count: 1000,
),
(
color: (0, 255, 0),
position: (0.0, 1000.0),
radius: 100.0,
count: 1000,
),
(
color: (0, 0, 255),
position: (800.0, 500.0),
radius: 100.0,
count: 1000,
),
],
)

74
src/app.rs Normal file
View file

@ -0,0 +1,74 @@
use std::time::Instant;
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::WindowHandler};
use crate::{Simulation, View};
#[derive(Debug)]
pub struct App {
sim: Simulation,
view: View,
last_tick: Instant,
}
impl App {
pub fn new(sim: Simulation) -> Self {
Self {
sim,
view: View::default(),
last_tick: Instant::now(),
}
}
pub fn tick(&mut self) {
let now = Instant::now();
let delta = now.duration_since(self.last_tick);
self.sim.tick(delta);
self.last_tick = now;
}
}
const UNIT_WIDTH: f32 = 4.;
const UNIT_SIZE: Vec2 = Vec2 {
x: UNIT_WIDTH,
y: UNIT_WIDTH,
};
impl WindowHandler for App {
fn on_draw(
&mut self,
helper: &mut speedy2d::window::WindowHelper<()>,
graphics: &mut speedy2d::Graphics2D,
) {
self.tick();
graphics.clear_screen(Color::from_hex_rgb(0x202020));
for unit in self.sim.units() {
if !unit.is_alive() {
continue;
}
let screen_position = unit.position() - self.view.origin();
let tl = screen_position - (UNIT_SIZE / 2.);
let br = screen_position + (UNIT_SIZE / 2.);
graphics.draw_rectangle(Rectangle::new(tl, br), *unit.color(self.sim.teams()))
}
helper.request_redraw();
}
fn on_keyboard_char(
&mut self,
_h: &mut speedy2d::window::WindowHelper<()>,
unicode_codepoint: char,
) {
const MOVEMENT_STEP: f32 = 100.;
match unicode_codepoint {
'z' => self.view.displace((0., -MOVEMENT_STEP).into()),
'q' => self.view.displace((-MOVEMENT_STEP, 0.).into()),
's' => self.view.displace((0., MOVEMENT_STEP).into()),
'd' => self.view.displace((MOVEMENT_STEP, 0.).into()),
_ => (),
}
}
}

66
src/builder.rs Normal file
View file

@ -0,0 +1,66 @@
use serde::{Deserialize, Serialize};
use speedy2d::color::Color;
use crate::Simulation;
#[derive(Debug, Serialize, Deserialize)]
pub struct Team {
color: (u8, u8, u8),
position: (f32, f32),
radius: f32,
count: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Descriptor {
teams: Vec<Team>,
}
pub fn example_descriptor() -> Descriptor {
Descriptor {
teams: vec![
Team {
color: (255, 0, 0),
count: 1000,
position: (0., 0.),
radius: 100.,
},
Team {
color: (0, 255, 0),
count: 1000,
position: (0., 1000.),
radius: 100.,
},
Team {
color: (0, 0, 255),
count: 1000,
position: (800., 500.),
radius: 100.,
},
],
}
}
impl Descriptor {
pub fn unwrap(self) -> Vec<crate::Team> {
let Self { teams } = self;
teams
.into_iter()
.map(
|Team {
color: (r, g, b),
position,
count,
radius,
}| {
crate::Team::new(Color::from_int_rgb(r, g, b), position.into(), radius, count)
},
)
.collect()
}
}
pub fn build(descr: Descriptor) -> Simulation {
let teams = descr.unwrap();
Simulation::new(teams)
}

56
src/main.rs Normal file
View file

@ -0,0 +1,56 @@
use std::fs;
use clap::{Parser, Subcommand};
use ron::ser::PrettyConfig;
use speedy2d::Window;
mod sim;
pub use sim::{Simulation, Team};
mod view;
pub use view::View;
mod builder;
pub use builder::{build, example_descriptor};
mod app;
pub use app::App;
#[derive(Parser, Debug)]
/// Simple program for simulating pixel fights
pub struct Params {
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Dumps an example fight configuration to stdout.
Example,
/// Runs a fight from a configuration file.
Run {
/// Path to an example fight configuration file.
path: String,
},
}
fn main() {
let args: Params = Params::parse();
match args.command {
Command::Example => {
let example = example_descriptor();
let serialized = ron::ser::to_string_pretty(&example, PrettyConfig::default()).unwrap();
println!("{serialized}");
}
Command::Run { path } => {
let content = fs::read_to_string(path).unwrap();
let deserialized = ron::from_str(&content).unwrap();
let sim = build(deserialized);
let window = Window::new_centered("Pixel fight /rs", (800, 600)).unwrap();
window.run_loop(App::new(sim));
}
}
}

189
src/sim.rs Normal file
View file

@ -0,0 +1,189 @@
use std::{f32::consts::PI, time::Duration};
use rand::{prelude::IteratorRandom, random};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use speedy2d::{color::Color, dimen::Vec2};
#[derive(Debug)]
pub struct Team {
initial_unit_count: usize,
initial_position: Vec2,
initial_radius: f32,
color: Color,
}
impl Team {
pub fn new(
color: Color,
initial_position: Vec2,
initial_radius: f32,
initial_unit_count: usize,
) -> Self {
Self {
color,
initial_position,
initial_radius,
initial_unit_count,
}
}
}
pub enum UnitAction {
Displace(usize, Vec2),
SetTarget(usize, Option<usize>),
Kill(usize, usize),
}
#[derive(Debug, Clone)]
pub struct Unit {
id: usize,
team_id: usize,
position: Vec2,
target_id: Option<usize>,
alive: bool,
speed: f32,
}
fn rand_speed() -> f32 {
Unit::SPEED * (1. + (random::<f32>() * 2. - 1.) * Unit::SPEED_RANDOMNESS)
}
impl Unit {
pub fn new(team_id: usize, id: usize, position: Vec2) -> Self {
Self {
id,
alive: true,
position,
target_id: None,
team_id,
speed: rand_speed(),
}
}
pub fn displace(&mut self, movement: Vec2) {
self.position = self.position + movement;
}
pub fn set_target(&mut self, target: Option<usize>) {
self.target_id = target;
}
pub fn kill(&mut self) {
self.alive = false;
}
pub fn tick(&self, units: &[Unit], delta: Duration) -> Option<UnitAction> {
self.is_alive().then(|| {
if let Some(target_id) = self.target_id {
let other = &units[target_id];
if self.is_in_range(other) {
UnitAction::Kill(self.id, other.id)
} else {
UnitAction::Displace(
self.id,
self.direction_to(other) * self.speed * delta.as_secs_f32(),
)
}
} else {
let target = units
.iter()
.filter(|u| (u.team_id != self.team_id) && u.is_alive())
.map(|u| u.id)
.collect::<Box<[usize]>>()
.iter()
.choose(&mut rand::thread_rng())
.cloned();
UnitAction::SetTarget(self.id, target)
}
})
}
pub fn position(&self) -> &Vec2 {
&self.position
}
pub fn color<'a>(&self, teams: &'a [Team]) -> &'a Color {
&teams[self.team_id].color
}
pub fn is_alive(&self) -> bool {
self.alive
}
const REACH: f32 = 10.;
const SPEED: f32 = 20.;
const SPEED_RANDOMNESS: f32 = 0.5;
fn is_in_range(&self, other: &Self) -> bool {
(self.position - other.position).magnitude_squared() < (Self::REACH.powf(2.))
}
fn direction_to(&self, other: &Self) -> Vec2 {
(other.position - self.position)
.normalize()
.unwrap_or_else(|| (0., 0.).into())
}
}
fn random_nearby_pos(center: Vec2, radius: f32) -> Vec2 {
let Vec2 { x, y } = center;
let angle = rand::random::<f32>() * 2. * PI;
let module = rand::random::<f32>() * radius;
Vec2 {
x: x + angle.cos() * module,
y: y + angle.sin() * module,
}
}
#[derive(Debug)]
pub struct Simulation {
teams: Vec<Team>,
units: Vec<Unit>,
}
impl Simulation {
pub fn new(desired_teams: impl IntoIterator<Item = Team>) -> Self {
let mut teams = vec![];
let mut units = vec![];
for team in desired_teams.into_iter() {
for _ in 0..team.initial_unit_count {
let position = random_nearby_pos(team.initial_position, team.initial_radius);
let unit = Unit::new(teams.len(), units.len(), position);
units.push(unit);
}
teams.push(team);
}
Self { teams, units }
}
pub fn tick(&mut self, delta: Duration) {
let actions = self
.units
.par_iter()
.map(|unit| unit.tick(&self.units, delta))
.collect::<Vec<_>>();
for action in actions.into_iter().flatten() {
{
match action {
UnitAction::Displace(id, movement) => self.units[id].displace(movement),
UnitAction::SetTarget(id, target) => self.units[id].set_target(target),
UnitAction::Kill(id, target) => {
self.units[id].set_target(None);
self.units[target].kill()
}
}
}
}
}
pub fn units(&self) -> &[Unit] {
&self.units
}
pub fn teams(&self) -> &[Team] {
&self.teams
}
}

23
src/view.rs Normal file
View file

@ -0,0 +1,23 @@
use speedy2d::dimen::Vec2;
#[derive(Debug)]
pub struct View {
origin: Vec2,
}
impl View {
pub fn origin(&self) -> &Vec2 {
&self.origin
}
pub fn displace(&mut self, movement: Vec2) {
self.origin = self.origin + movement;
}
}
impl Default for View {
fn default() -> Self {
let origin = (0., 0.).into();
Self { origin }
}
}