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