Skip to content

Commit

Permalink
game: line of sight implemented
Browse files Browse the repository at this point in the history
closes #8

adjusted network messages to ensure consistency across clients but this needs cleaning up (do we need pos and positions, or maybe just an index?)
  • Loading branch information
lewis-weinberger committed Feb 11, 2023
1 parent 4f683dc commit ac81db1
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 27 deletions.
151 changes: 144 additions & 7 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ use rand::{
random, Rng,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use std::time::Duration;

// View-cone parameters
const LENGTH: i16 = 8;
const WIDTH: usize = 6;

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum Status {
Running,
Expand All @@ -15,7 +20,7 @@ pub enum Status {
Quit,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Tile {
Floor,
Wall,
Expand Down Expand Up @@ -47,7 +52,7 @@ pub struct Map {

impl Map {
pub fn new(len: usize) -> Self {
// TODO map generation
// TODO better map generation
let mut buf = vec![Tile::Floor; len * len];
for tile in buf.iter_mut() {
if random() {
Expand Down Expand Up @@ -96,6 +101,49 @@ impl Map {
y = random();
}
}

/// Shoot a line of sight
pub fn bresenham(&self, start: (i16, i16), end: (i16, i16)) -> LineOfSight {
let (mut x0, mut y0) = start;
let (mut x1, mut y1) = end;
let mut dx = x1 as f64 - x0 as f64;
let mut dy = y1 as f64 - y0 as f64;

// Rotate dimensions?
let mut rotated = false;
if dy.abs() > dx.abs() {
(x0, y0) = (y0, x0);
(x1, y1) = (y1, x1);
rotated = true;
}

let mut sign = 1;
if x0 > x1 {
sign = -1;
x0 *= -1;
x1 *= -1;
}
let ystep = if y0 < y1 { 1 } else { -1 };

dx = x1 as f64 - x0 as f64;
dy = (y1 as f64 - y0 as f64).abs();
let gradient = (dx, dy);
let error = (dx / 2.0).floor();

let current = (x0, y0);
let end = x1;

LineOfSight {
current,
end,
gradient,
error,
sign,
ystep,
rotated,
map: self,
}
}
}

impl From<&str> for Map {
Expand Down Expand Up @@ -134,6 +182,51 @@ impl<'a> Iterator for Tiles<'a> {
}
}

/// Line of sight across Map Tiles
pub struct LineOfSight<'a> {
current: (i16, i16),
end: i16,
gradient: (f64, f64),
error: f64,
sign: i16,
ystep: i16,
rotated: bool,
map: &'a Map,
}

impl<'a> Iterator for LineOfSight<'a> {
type Item = (Point, Tile);

fn next(&mut self) -> Option<Self::Item> {
let new = if self.rotated {
(self.current.1, self.sign * self.current.0)
} else {
(self.sign * self.current.0, self.current.1)
};

if self.current.0 > self.end {
return None;
}

let (dx, dy) = self.gradient;
self.current.0 += 1;
self.error -= dy;
if self.error < 0.0 {
self.current.1 += self.ystep;
self.error += dx;
}

if new.0 > 0 && new.1 > 0 {
if let Some(tile) = self.map.at(new.0 as usize, new.1 as usize) {
if tile == Tile::Floor {
return Some(((new.0 as u8, new.1 as u8), tile));
}
}
}
None
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum Direction {
Up,
Expand Down Expand Up @@ -262,6 +355,7 @@ impl Game {
turn,
defender,
pos,
positions: self.positions.clone(),
guards: self.guards.clone(),
quit,
}
Expand All @@ -274,6 +368,16 @@ impl Game {
self.quit = msg.quit;
}

/// Client-side turn processing
pub fn display<T: UserInterface>(&mut self, ui: &mut T, msg: &MsgToClient) -> Result<()> {
self.pos = msg.pos;
self.positions = msg.positions.clone();
self.guards = msg.guards.clone();
self.quit = msg.quit;
ui.display(self, msg.defender)?;
Ok(())
}

/// Client-side turn processing
pub fn play<T: UserInterface>(&mut self, defender: bool, ui: &mut T) -> Result<MsgToServer> {
ui.input(self, defender)?;
Expand All @@ -283,15 +387,47 @@ impl Game {
quit: self.quit,
})
}

/// Tiles within guard's line-of-sight
pub fn view_cone(&self, _guard: usize) -> Vec<(Point, Tile)> {
// TODO
vec![]
pub fn view_cone(&self, guard: usize) -> Vec<(Point, Tile)> {
let mut cone = HashSet::new();
let mut ends = HashSet::new();
if let Some((pos, dir)) = self.guards[guard] {
// Determine edge of cone
for i in 0..WIDTH {
match dir {
Direction::Up => {
ends.insert((pos.0 as i16 + i as i16, pos.1 as i16 - LENGTH));
ends.insert((pos.0 as i16 - i as i16, pos.1 as i16 - LENGTH));
}
Direction::Right => {
ends.insert((pos.0 as i16 + LENGTH, pos.1 as i16 + i as i16));
ends.insert((pos.0 as i16 + LENGTH, pos.1 as i16 - i as i16));
}
Direction::Down => {
ends.insert((pos.0 as i16 + i as i16, pos.1 as i16 + LENGTH));
ends.insert((pos.0 as i16 - i as i16, pos.1 as i16 + LENGTH));
}
Direction::Left => {
ends.insert((pos.0 as i16 - LENGTH, pos.1 as i16 + i as i16));
ends.insert((pos.0 as i16 - LENGTH, pos.1 as i16 - i as i16));
}
}
}

// For each end-point shoot a line-of-sight
for end in ends.iter() {
for p in self.map.bresenham((pos.0 as i16, pos.1 as i16), *end) {
cone.insert(p);
}
}
}
Vec::from_iter(cone)
}

/// Is the player visible?
pub fn visible(&self) -> bool {
if let Some(player) = self.pos {
pub fn visible(&self, player: usize) -> bool {
if let Some(player) = self.positions[player] {
for i in 0..self.guards.len() {
for (view, _) in self.view_cone(i).iter() {
if *view == player {
Expand All @@ -311,6 +447,7 @@ impl Game {
if let Some(tile) = self.map.at(x2 as usize, y2 as usize) {
if tile == Tile::Floor {
self.pos = Some((x2 as u8, y2 as u8));
self.positions[self.player] = self.pos;
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub struct MsgToClient {
pub defender: bool,
// Player's position (if not defender and if alive)
pub pos: Option<Point>,
// Players' positions (if alive)
// TODO: cleanup redundant info
pub positions: Vec<Option<Point>>,
// Guards' positions (if alive)
pub guards: Vec<Option<(Point, Direction)>>,
// Game finished?
Expand Down Expand Up @@ -176,7 +179,7 @@ impl<T: UserInterface> Client<T> {
loop {
// Receive update from server
let msg: MsgToClient = deserialize_from(&self.stream)?;
self.ui.display(&self.game, msg.defender)?;
self.game.display(&mut self.ui, &msg)?;
if msg.quit != Status::Running {
quit = msg.quit;
break;
Expand Down
43 changes: 24 additions & 19 deletions src/ui/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::io::{self, Stdout, Write};
use std::ops::Drop;
use std::time::{Duration, Instant};

const TIMEOUT: u64 = 200;
const TIMEOUT: u64 = 300;
const ACTIONS: isize = 5;

pub struct Terminal {
Expand Down Expand Up @@ -152,34 +152,39 @@ impl UserInterface for Terminal {
}
}

// Display guards
for (i, (pos, _dir)) in game.guards.iter().flatten().enumerate() {
if let Some((x, y)) = self.map_to_display(*pos) {
let g = if defender && i == self.guard {
"G".yellow()
} else {
"G".cyan()
};
queue!(self.stdout, MoveTo(x, y), PrintStyledContent(g))?;
}

// View cones
for (pos, tile) in game.view_cone(i).iter() {
if let Some((x, y)) = self.map_to_display(*pos) {
queue!(
self.stdout,
MoveTo(x, y),
PrintStyledContent(tile.to_string().red())
PrintStyledContent(tile.to_string().on_red())
)?;
}
}

// Display guards
if let Some((x, y)) = self.map_to_display(*pos) {
let g = if defender && i == self.guard {
"G".yellow()
} else {
"G".cyan()
};
queue!(self.stdout, MoveTo(x, y), PrintStyledContent(g))?;
}
}

// Display player
if !defender || game.visible() {
if let Some(pos) = game.pos {
if let Some((x, y)) = self.map_to_display(pos) {
queue!(self.stdout, MoveTo(x, y), PrintStyledContent("A".magenta()))?;
// Display players
for (i, pos) in game.positions.iter().flatten().enumerate() {
if !defender || game.visible(i) {
if let Some((x, y)) = self.map_to_display(*pos) {
let p = if i != game.player {
"A".white()
} else {
"A".magenta()
};
queue!(self.stdout, MoveTo(x, y), PrintStyledContent(p))?;
}
}
}
Expand Down Expand Up @@ -255,8 +260,8 @@ impl UserInterface for Terminal {
}
}

// Check for attacker elimination
if game.visible() {
// Check for attacker elimination (on their turn only)
if game.visible(game.player) {
detected -= 1;
}
if detected == 0 {
Expand Down

0 comments on commit ac81db1

Please sign in to comment.