init
This commit is contained in:
commit
9c96c9f828
11 changed files with 2239 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1749
Cargo.lock
generated
Normal file
1749
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal 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
22
fight_configs/300_000.ron
Normal 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
22
fight_configs/30_000.ron
Normal 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
22
fight_configs/default.ron
Normal 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
74
src/app.rs
Normal 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
66
src/builder.rs
Normal 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
56
src/main.rs
Normal 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
189
src/sim.rs
Normal 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
23
src/view.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue