Skip to content

Commit

Permalink
Added multiple ship types and increased movement realism
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathmagician8191 committed Sep 13, 2024
1 parent 79bb04a commit c520f39
Show file tree
Hide file tree
Showing 14 changed files with 587 additions and 152 deletions.
372 changes: 272 additions & 100 deletions enterprise/src/main.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions midway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ license.workspace = true
edition.workspace = true

[dependencies]
enum-iterator = "2.1.0"
rand = "0.8.5"
random-pick = "1.2.16"
254 changes: 202 additions & 52 deletions midway/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,115 @@
// Server for WW2 naval combat simulator

use rand::{thread_rng, Rng};
use stats::get_random_ship;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io::{stdin, BufRead, BufReader, Write};
use std::f32::consts::PI;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
use std::thread::{sleep, spawn};
use std::time::{Duration, Instant};

mod stats;

const PORT: u16 = 25565;

const TIME_ACCELERATION_FACTOR: f32 = 5.0;
const TPS: u32 = 60;

const COLOUR: &str = "f00";

const MAP_RADIUS: Option<f32> = Some(5000.0);
const KRAKEN_NAME: &str = "Kraken";

const WATER_VISCOSITY: f32 = 0.000_001;
const GRAVITY: f32 = 9.81;

#[derive(Clone)]
struct Ship {
coords: (f32, f32),
velocity: f32,
angle: f32,
helm: f32,
power: f32,
stats: ShipStats,
sunk: bool,
}

#[derive(Clone)]
struct ShipStats {
texture: usize,
length: f32,
mass: f32,
health: f32,
power: f32,
k: f32,
surface_area: f32,
froude_scale_factor: f32,
}

impl Ship {
fn new(coords: (f32, f32), angle: f32) -> Self {
fn new() -> Self {
let mut rng = thread_rng();
let angle = rng.gen_range(0.0..(2.0 * PI));
let distance = rng.gen_range(0.0..1000.0);
let x = distance * angle.cos();
let y = distance * angle.sin();
let stats = get_random_ship();
Self {
coords,
coords: (x, y),
velocity: 0.0,
angle,
angle: 0.0,
helm: 0.0,
power: 0.0,
stats,
sunk: false,
}
}

fn step(&mut self) {
self.angle += self.helm * 0.01;
self.velocity = 0.997 * self.velocity + 0.004 * self.power;
self.coords.0 += self.velocity * self.angle.sin();
self.coords.1 -= self.velocity * self.angle.cos();
fn step(&mut self, delta_t: f32) {
self.angle += delta_t * self.helm * self.velocity / self.stats.length;
let mut net_power = self.power * self.stats.power;
let reynolds_number = self.stats.length * self.velocity.abs() / WATER_VISCOSITY;
let c_f = 0.075 / (reynolds_number.log10() - 2.0).powi(2);
let c_v = c_f * (1.0 + self.stats.k);
let froude_number = self.velocity / (GRAVITY * self.stats.length).sqrt();
let c_w = self.stats.froude_scale_factor * froude_number.powi(6);
let c_total = c_v + c_w;
let r_total = c_total * 0.5 * self.velocity.powi(2) * self.stats.surface_area;
net_power -= r_total * self.velocity;
let new_energy =
0.5 * self.stats.mass * self.velocity * self.velocity.abs() + net_power * delta_t;
self.velocity = match new_energy.total_cmp(&0.0) {
Ordering::Greater => (2.0 * new_energy / self.stats.mass).sqrt(),
Ordering::Less => -(-2.0 * new_energy / self.stats.mass).sqrt(),
Ordering::Equal => 0.0,
};
self.coords.0 += self.velocity * delta_t * self.angle.sin();
self.coords.1 -= self.velocity * delta_t * self.angle.cos();
}

fn distance_from_origin(&self) -> f32 {
(self.coords.0.powi(2) + self.coords.1.powi(2)).sqrt()
}

fn distance(&self, other: &Self) -> f32 {
let x_distance = self.coords.0 - other.coords.0;
let y_distance = self.coords.1 - other.coords.1;
(x_distance.powi(2) + y_distance.powi(2)).sqrt()
}

fn damage(&mut self, amount: f32) {
self.stats.health -= amount;
if self.stats.health <= 0.0 {
self.sunk = true;
}
}
}

enum ClientMessage {
Sail(f32, f32),
Anchor,
}

struct ClientData {
Expand All @@ -51,12 +119,12 @@ struct ClientData {
}

impl ClientData {
fn new(stream: TcpStream, rx: Receiver<ClientMessage>, ship: Ship) -> Self {
const fn new(stream: TcpStream, rx: Receiver<ClientMessage>, ship: Ship) -> Self {
Self { stream, rx, ship }
}
}

fn process_joining(tx: Sender<(TcpStream, Receiver<ClientMessage>, String)>) {
fn process_joining(tx: &Sender<(TcpStream, Receiver<ClientMessage>, String)>) {
let listener = TcpListener::bind(format!("0.0.0.0:{PORT}"))
.unwrap_or_else(|_| panic!("Failed to bind to port {PORT}"));

Expand All @@ -74,39 +142,37 @@ fn process_joining(tx: Sender<(TcpStream, Receiver<ClientMessage>, String)>) {
continue;
}
let mut words = buf.split_whitespace();
match words.next() {
Some("ship") => match words.next() {
Some(name) => name,
None => {
println!("Invalid input");
continue;
}
},
_ => {
if let Some("ship") = words.next() {
if let Some(name) = words.next() {
name
} else {
println!("Invalid input");
continue;
}
} else {
println!("Invalid input");
continue;
}
} else {
println!("Invalid input");
continue;
};
let (tx2, rx) = channel();
spawn(|| process_client(stream, tx2));
spawn(move || process_client(stream, &tx2));
if tx.send((stream_clone, rx, name.to_owned())).is_ok() {
println!("{address} connected as {name}");
} else {
// The game has started
// The server has crashed or something
return;
}
}
}

fn process_client(mut stream: BufReader<TcpStream>, tx: Sender<ClientMessage>) -> Option<()> {
fn process_client(mut stream: BufReader<TcpStream>, tx: &Sender<ClientMessage>) -> Option<()> {
let mut buf = String::new();
while let Ok(chars) = stream.read_line(&mut buf) {
if chars == 0 {
None?
None?;
}
let mut words = buf.split_whitespace();
match words.next() {
Expand All @@ -115,41 +181,52 @@ fn process_client(mut stream: BufReader<TcpStream>, tx: Sender<ClientMessage>) -
let helm = words.next().and_then(|w| w.parse().ok())?;
tx.send(ClientMessage::Sail(power, helm)).ok()?;
}
Some("anchor") => tx.send(ClientMessage::Anchor).ok()?,
_ => todo!(),
}
buf.clear();
}
None
}

fn handle_join(
connections: &mut HashMap<String, ClientData>,
mut stream: TcpStream,
rx: Receiver<ClientMessage>,
name: String,
) {
let address = stream
.peer_addr()
.map(|x| x.to_string())
.unwrap_or("unknown".to_owned());
println!("{address} joined as {name}");
let ship = Ship::new();
if let Some(radius) = MAP_RADIUS {
stream
.write_all(format!("radius {radius}\n").as_bytes())
.ok();
}
let client = ClientData::new(stream, rx, ship);
connections.insert(name, client);
}

fn main() {
let (tx, rx) = channel();
spawn(|| process_joining(tx));
let mut buf = String::new();
stdin()
.lock()
.read_line(&mut buf)
.expect("Failed to wait for input");
println!("Starting game");
spawn(move || process_joining(&tx));
let mut connections = HashMap::new();
for (stream, rx, name) in rx.try_iter() {
let address = stream
.peer_addr()
.map(|x| x.to_string())
.unwrap_or("unknown".to_owned());
println!("{address} joined as {name}");
let mut rng = thread_rng();
let x = rng.gen_range(-100.0..100.0);
let y = rng.gen_range(-100.0..100.0);
let ship = Ship::new((x, y), 0.0);
let client = ClientData::new(stream, rx, ship);
connections.insert(name, client);
}
let (stream, rx_2, name) = rx.recv().expect("Could not start server");
handle_join(&mut connections, stream, rx_2, name);
let delay = Duration::from_secs(1) / TPS;
let delta_t = TIME_ACCELERATION_FACTOR / TPS as f32;
let mut kraken: Option<Ship> = None;
loop {
let start = Instant::now();
for _ in 0..TPS {
let start = Instant::now();
// Process newly joining clients
for (stream, rx, name) in rx.try_iter() {
handle_join(&mut connections, stream, rx, name);
}
let mut disconnected = Vec::new();
// get updates from clients
for (name, connection) in &mut connections {
Expand All @@ -159,6 +236,11 @@ fn main() {
connection.ship.power = power;
connection.ship.helm = helm;
}
Ok(ClientMessage::Anchor) => {
if connection.ship.velocity < 1.5 {
connection.ship.velocity = 0.0;
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
println!("{name} has disconnected");
Expand All @@ -172,26 +254,94 @@ fn main() {
connections.remove(name);
}
for name in disconnected {
for (_, connection) in &mut connections {
for connection in connections.values_mut() {
connection
.stream
.write_all(format!("sunk {name}\n").as_bytes())
.ok();
}
}
let mut ships = Vec::new();
let mut kraken_targets = 0;
let mut names_to_remove = Vec::new();
for (name, connection) in &mut connections {
connection.ship.step();
if connection.ship.sunk {
continue;
}
if let Some(ref kraken) = kraken {
if kraken.distance(&connection.ship) < 100.0 {
connection.ship.velocity = 0.0;
kraken_targets += 1;
connection.ship.damage(20.0 * delta_t);
if connection.ship.sunk {
names_to_remove.push(name.clone());
continue;
}
}
}
connection.ship.step(delta_t);
if let Some(radius) = MAP_RADIUS {
if kraken.is_none() && connection.ship.distance_from_origin() > radius {
let mut rng = thread_rng();
let angle = rng.gen_range(0.0..(2.0 * PI));
let distance = rng.gen_range(40.0..80.0);
let x = connection.ship.coords.0 + distance * angle.cos();
let y = connection.ship.coords.1 + distance * angle.sin();
let stats = ShipStats {
texture: 8,
length: 60.0,
mass: 1000.0,
health: 1000.0,
power: 1000.0,
k: 0.0,
surface_area: 1000.0,
froude_scale_factor: 2.2,
};
kraken = Some(Ship {
coords: (x, y),
velocity: 0.0,
angle: 0.0,
helm: 0.0,
power: 0.0,
stats,
sunk: false,
});
connection.ship.velocity = 0.0;
kraken_targets += 1;
}
}
ships.push((name.clone(), connection.ship.clone()));
}
for name in names_to_remove {
for connection in connections.values_mut() {
connection
.stream
.write_all(format!("sunk {name}\n").as_bytes())
.ok();
}
}
if let Some(ref kraken_ship) = kraken {
if kraken_targets == 0 || kraken_ship.sunk {
kraken = None;
let message = format!("sunk {KRAKEN_NAME}\n");
for connection in connections.values_mut() {
connection.stream.write_all(message.as_bytes()).ok();
}
}
}
if let Some(ref kraken) = kraken {
ships.push((KRAKEN_NAME.to_string(), kraken.clone()));
}
for (name, ship) in ships {
let (x, y) = ship.coords;
let angle = ship.angle;
for (_, connection2) in &mut connections {
connection2
.stream
.write_all(format!("ship {name} {x} {y} {angle}\n").as_bytes())
.ok();
let velocity = ship.velocity;
let texture = ship.stats.texture;
let size = ship.stats.length;
let health = ship.stats.health / ship.stats.mass;
let message = format!("ship {name} {x} {y} {angle} {velocity} {size} {texture} {COLOUR} {health}\n");
for connection2 in connections.values_mut() {
connection2.stream.write_all(message.as_bytes()).ok();
}
}
if connections.is_empty() {
Expand Down
Loading

0 comments on commit c520f39

Please sign in to comment.