diff --git a/battle/src/battle.rs b/battle/src/battle.rs index 09e94b5..61ab29e 100644 --- a/battle/src/battle.rs +++ b/battle/src/battle.rs @@ -12,8 +12,6 @@ pub struct Battle { p2_rng: Pcg64Mcg, garbage_rng: Pcg64Mcg, pub time: u32, - multiplier: f32, - margin_time: Option, pub replay: Replay } @@ -38,18 +36,11 @@ impl Battle { player_1, player_2, p1_rng, p2_rng, garbage_rng, time: 0, - margin_time: p1_config.margin_time, - multiplier: 1.0, } } pub fn update(&mut self, p1: Controller, p2: Controller) -> BattleUpdate { self.time += 1; - if let Some(margin_time) = self.margin_time { - if self.time >= margin_time && (self.time - margin_time) % 1800 == 0 { - self.multiplier += 0.5; - } - } self.replay.updates.push_back((p1, p2)); @@ -58,12 +49,12 @@ impl Battle { for event in &p1_events { if let &Event::GarbageSent(amt) = event { - self.player_2.garbage_queue += (amt as f32 * self.multiplier) as u32; + self.player_2.garbage_queue += amt; } } for event in &p2_events { if let &Event::GarbageSent(amt) = event { - self.player_1.garbage_queue += (amt as f32 * self.multiplier) as u32; + self.player_1.garbage_queue += amt; } } @@ -76,8 +67,7 @@ impl Battle { events: p2_events, garbage_queue: self.player_2.garbage_queue }, - time: self.time, - attack_multiplier: self.multiplier + time: self.time } } } @@ -86,8 +76,7 @@ impl Battle { pub struct BattleUpdate { pub player_1: PlayerUpdate, pub player_2: PlayerUpdate, - pub time: u32, - pub attack_multiplier: f32 + pub time: u32 } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/battle/src/lib.rs b/battle/src/lib.rs index dfb6961..8a5f145 100644 --- a/battle/src/lib.rs +++ b/battle/src/lib.rs @@ -17,7 +17,6 @@ pub struct GameConfig { pub auto_repeat_rate: u32, pub soft_drop_speed: u32, pub lock_delay: u32, - pub margin_time: Option, /// Measured in 1/100 of a tick pub gravity: i32, @@ -37,7 +36,6 @@ impl Default for GameConfig { auto_repeat_rate: 2, soft_drop_speed: 2, lock_delay: 30, - margin_time: None, gravity: 4500, next_queue_size: 5, max_garbage_add: 10, @@ -56,7 +54,6 @@ impl GameConfig { auto_repeat_rate: 0, soft_drop_speed: 0, lock_delay: 30, - margin_time: None, gravity: 4500, next_queue_size: 5, max_garbage_add: 20, diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 022af52..4954531 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -1,23 +1,24 @@ [package] name = "gui" version = "0.1.0" -authors = ["MinusKelvin "] +authors = ["MinusKelvin "] edition = "2018" default-run = "gui" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -arrayvec = "0.4.11" -ggez = "0.5.1" +game-util = { git = "https://github.com/MinusKelvin/game-util-rs" } +cold-clear = { path = "../bot" } libtetris = { path = "../libtetris" } battle = { path = "../battle" } -cold-clear = { path = "../bot" } -serde = "1" -bincode = "1" +arrayvec = "0.4.11" +rand = "0.7.3" +gilrs = { version = "0.7.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.8.11" +bincode = "1.2" libflate = "0.1" -serde_yaml = "0.8" -rand = "0.7.0" -rand_pcg = "0.2.0" -enumset = "0.4.0" -winit = { version = "0.19", features = ["serde"] } \ No newline at end of file + +[build-dependencies] +build-utils = { git = "https://github.com/MinusKelvin/game-util-rs" } diff --git a/gui/build.rs b/gui/build.rs new file mode 100644 index 0000000..deeaf7c --- /dev/null +++ b/gui/build.rs @@ -0,0 +1,6 @@ +use std::env; +use std::path::Path; + +fn main() { + build_utils::gen_sprites("sprites", Path::new(&env::var("OUT_DIR").unwrap()), 1024); +} \ No newline at end of file diff --git a/gui/resources/sprites.png b/gui/resources/sprites.png deleted file mode 100644 index 18c5401..0000000 Binary files a/gui/resources/sprites.png and /dev/null differ diff --git a/gui/sprites/blank.png b/gui/sprites/blank.png new file mode 100644 index 0000000..fa81e78 Binary files /dev/null and b/gui/sprites/blank.png differ diff --git a/gui/sprites/filled.png b/gui/sprites/filled.png new file mode 100644 index 0000000..0c33ae8 Binary files /dev/null and b/gui/sprites/filled.png differ diff --git a/gui/sprites/garbage_bar.png b/gui/sprites/garbage_bar.png new file mode 100644 index 0000000..1dfb501 Binary files /dev/null and b/gui/sprites/garbage_bar.png differ diff --git a/gui/sprites/ghost.png b/gui/sprites/ghost.png new file mode 100644 index 0000000..1ef328f Binary files /dev/null and b/gui/sprites/ghost.png differ diff --git a/gui/sprites/line_clear.0.png b/gui/sprites/line_clear.0.png new file mode 100644 index 0000000..18c932b Binary files /dev/null and b/gui/sprites/line_clear.0.png differ diff --git a/gui/sprites/line_clear.1.png b/gui/sprites/line_clear.1.png new file mode 100644 index 0000000..7ef26b5 Binary files /dev/null and b/gui/sprites/line_clear.1.png differ diff --git a/gui/sprites/line_clear.10.png b/gui/sprites/line_clear.10.png new file mode 100644 index 0000000..bae8b12 Binary files /dev/null and b/gui/sprites/line_clear.10.png differ diff --git a/gui/sprites/line_clear.11.png b/gui/sprites/line_clear.11.png new file mode 100644 index 0000000..d4539c6 Binary files /dev/null and b/gui/sprites/line_clear.11.png differ diff --git a/gui/sprites/line_clear.12.png b/gui/sprites/line_clear.12.png new file mode 100644 index 0000000..936747b Binary files /dev/null and b/gui/sprites/line_clear.12.png differ diff --git a/gui/sprites/line_clear.13.png b/gui/sprites/line_clear.13.png new file mode 100644 index 0000000..05932bf Binary files /dev/null and b/gui/sprites/line_clear.13.png differ diff --git a/gui/sprites/line_clear.14.png b/gui/sprites/line_clear.14.png new file mode 100644 index 0000000..cb83112 Binary files /dev/null and b/gui/sprites/line_clear.14.png differ diff --git a/gui/sprites/line_clear.15.png b/gui/sprites/line_clear.15.png new file mode 100644 index 0000000..3aea8df Binary files /dev/null and b/gui/sprites/line_clear.15.png differ diff --git a/gui/sprites/line_clear.16.png b/gui/sprites/line_clear.16.png new file mode 100644 index 0000000..7f5833e Binary files /dev/null and b/gui/sprites/line_clear.16.png differ diff --git a/gui/sprites/line_clear.17.png b/gui/sprites/line_clear.17.png new file mode 100644 index 0000000..e8d6284 Binary files /dev/null and b/gui/sprites/line_clear.17.png differ diff --git a/gui/sprites/line_clear.18.png b/gui/sprites/line_clear.18.png new file mode 100644 index 0000000..6cc54f3 Binary files /dev/null and b/gui/sprites/line_clear.18.png differ diff --git a/gui/sprites/line_clear.19.png b/gui/sprites/line_clear.19.png new file mode 100644 index 0000000..ff0a0a3 Binary files /dev/null and b/gui/sprites/line_clear.19.png differ diff --git a/gui/sprites/line_clear.2.png b/gui/sprites/line_clear.2.png new file mode 100644 index 0000000..7191067 Binary files /dev/null and b/gui/sprites/line_clear.2.png differ diff --git a/gui/sprites/line_clear.20.png b/gui/sprites/line_clear.20.png new file mode 100644 index 0000000..821ce4e Binary files /dev/null and b/gui/sprites/line_clear.20.png differ diff --git a/gui/sprites/line_clear.21.png b/gui/sprites/line_clear.21.png new file mode 100644 index 0000000..9a45884 Binary files /dev/null and b/gui/sprites/line_clear.21.png differ diff --git a/gui/sprites/line_clear.22.png b/gui/sprites/line_clear.22.png new file mode 100644 index 0000000..789abc3 Binary files /dev/null and b/gui/sprites/line_clear.22.png differ diff --git a/gui/sprites/line_clear.23.png b/gui/sprites/line_clear.23.png new file mode 100644 index 0000000..b33d6c9 Binary files /dev/null and b/gui/sprites/line_clear.23.png differ diff --git a/gui/sprites/line_clear.24.png b/gui/sprites/line_clear.24.png new file mode 100644 index 0000000..5c945f5 Binary files /dev/null and b/gui/sprites/line_clear.24.png differ diff --git a/gui/sprites/line_clear.25.png b/gui/sprites/line_clear.25.png new file mode 100644 index 0000000..dbd3656 Binary files /dev/null and b/gui/sprites/line_clear.25.png differ diff --git a/gui/sprites/line_clear.26.png b/gui/sprites/line_clear.26.png new file mode 100644 index 0000000..e1842f7 Binary files /dev/null and b/gui/sprites/line_clear.26.png differ diff --git a/gui/sprites/line_clear.27.png b/gui/sprites/line_clear.27.png new file mode 100644 index 0000000..6382faa Binary files /dev/null and b/gui/sprites/line_clear.27.png differ diff --git a/gui/sprites/line_clear.28.png b/gui/sprites/line_clear.28.png new file mode 100644 index 0000000..608c80c Binary files /dev/null and b/gui/sprites/line_clear.28.png differ diff --git a/gui/sprites/line_clear.29.png b/gui/sprites/line_clear.29.png new file mode 100644 index 0000000..9f61e58 Binary files /dev/null and b/gui/sprites/line_clear.29.png differ diff --git a/gui/sprites/line_clear.3.png b/gui/sprites/line_clear.3.png new file mode 100644 index 0000000..aedab58 Binary files /dev/null and b/gui/sprites/line_clear.3.png differ diff --git a/gui/sprites/line_clear.30.png b/gui/sprites/line_clear.30.png new file mode 100644 index 0000000..331f9dc Binary files /dev/null and b/gui/sprites/line_clear.30.png differ diff --git a/gui/sprites/line_clear.31.png b/gui/sprites/line_clear.31.png new file mode 100644 index 0000000..be93944 Binary files /dev/null and b/gui/sprites/line_clear.31.png differ diff --git a/gui/sprites/line_clear.32.png b/gui/sprites/line_clear.32.png new file mode 100644 index 0000000..ad3a605 Binary files /dev/null and b/gui/sprites/line_clear.32.png differ diff --git a/gui/sprites/line_clear.33.png b/gui/sprites/line_clear.33.png new file mode 100644 index 0000000..bcf2eb0 Binary files /dev/null and b/gui/sprites/line_clear.33.png differ diff --git a/gui/sprites/line_clear.34.png b/gui/sprites/line_clear.34.png new file mode 100644 index 0000000..2644c7c Binary files /dev/null and b/gui/sprites/line_clear.34.png differ diff --git a/gui/sprites/line_clear.4.png b/gui/sprites/line_clear.4.png new file mode 100644 index 0000000..d2ed283 Binary files /dev/null and b/gui/sprites/line_clear.4.png differ diff --git a/gui/sprites/line_clear.5.png b/gui/sprites/line_clear.5.png new file mode 100644 index 0000000..1f9b97a Binary files /dev/null and b/gui/sprites/line_clear.5.png differ diff --git a/gui/sprites/line_clear.6.png b/gui/sprites/line_clear.6.png new file mode 100644 index 0000000..fe42d5f Binary files /dev/null and b/gui/sprites/line_clear.6.png differ diff --git a/gui/sprites/line_clear.7.png b/gui/sprites/line_clear.7.png new file mode 100644 index 0000000..9e523a7 Binary files /dev/null and b/gui/sprites/line_clear.7.png differ diff --git a/gui/sprites/line_clear.8.png b/gui/sprites/line_clear.8.png new file mode 100644 index 0000000..1404bc1 Binary files /dev/null and b/gui/sprites/line_clear.8.png differ diff --git a/gui/sprites/line_clear.9.png b/gui/sprites/line_clear.9.png new file mode 100644 index 0000000..5391771 Binary files /dev/null and b/gui/sprites/line_clear.9.png differ diff --git a/gui/sprites/piece.0.png b/gui/sprites/piece.0.png new file mode 100644 index 0000000..3896660 Binary files /dev/null and b/gui/sprites/piece.0.png differ diff --git a/gui/sprites/piece.1.png b/gui/sprites/piece.1.png new file mode 100644 index 0000000..080e844 Binary files /dev/null and b/gui/sprites/piece.1.png differ diff --git a/gui/sprites/piece.2.png b/gui/sprites/piece.2.png new file mode 100644 index 0000000..50a9a9e Binary files /dev/null and b/gui/sprites/piece.2.png differ diff --git a/gui/sprites/piece.3.png b/gui/sprites/piece.3.png new file mode 100644 index 0000000..dddb091 Binary files /dev/null and b/gui/sprites/piece.3.png differ diff --git a/gui/sprites/piece.4.png b/gui/sprites/piece.4.png new file mode 100644 index 0000000..af0b630 Binary files /dev/null and b/gui/sprites/piece.4.png differ diff --git a/gui/sprites/piece.5.png b/gui/sprites/piece.5.png new file mode 100644 index 0000000..e1c3f1a Binary files /dev/null and b/gui/sprites/piece.5.png differ diff --git a/gui/sprites/piece.6.png b/gui/sprites/piece.6.png new file mode 100644 index 0000000..3f60940 Binary files /dev/null and b/gui/sprites/piece.6.png differ diff --git a/gui/sprites/plan.0.png b/gui/sprites/plan.0.png new file mode 100644 index 0000000..cbfc904 Binary files /dev/null and b/gui/sprites/plan.0.png differ diff --git a/gui/sprites/plan.1.png b/gui/sprites/plan.1.png new file mode 100644 index 0000000..55897b2 Binary files /dev/null and b/gui/sprites/plan.1.png differ diff --git a/gui/sprites/plan.10.png b/gui/sprites/plan.10.png new file mode 100644 index 0000000..9dd609c Binary files /dev/null and b/gui/sprites/plan.10.png differ diff --git a/gui/sprites/plan.11.png b/gui/sprites/plan.11.png new file mode 100644 index 0000000..dab165e Binary files /dev/null and b/gui/sprites/plan.11.png differ diff --git a/gui/sprites/plan.12.png b/gui/sprites/plan.12.png new file mode 100644 index 0000000..64d1aea Binary files /dev/null and b/gui/sprites/plan.12.png differ diff --git a/gui/sprites/plan.13.png b/gui/sprites/plan.13.png new file mode 100644 index 0000000..1f51f50 Binary files /dev/null and b/gui/sprites/plan.13.png differ diff --git a/gui/sprites/plan.14.png b/gui/sprites/plan.14.png new file mode 100644 index 0000000..2e78c17 Binary files /dev/null and b/gui/sprites/plan.14.png differ diff --git a/gui/sprites/plan.15.png b/gui/sprites/plan.15.png new file mode 100644 index 0000000..779ae2b Binary files /dev/null and b/gui/sprites/plan.15.png differ diff --git a/gui/sprites/plan.2.png b/gui/sprites/plan.2.png new file mode 100644 index 0000000..d161e15 Binary files /dev/null and b/gui/sprites/plan.2.png differ diff --git a/gui/sprites/plan.3.png b/gui/sprites/plan.3.png new file mode 100644 index 0000000..6ddbc8a Binary files /dev/null and b/gui/sprites/plan.3.png differ diff --git a/gui/sprites/plan.4.png b/gui/sprites/plan.4.png new file mode 100644 index 0000000..e31da0c Binary files /dev/null and b/gui/sprites/plan.4.png differ diff --git a/gui/sprites/plan.5.png b/gui/sprites/plan.5.png new file mode 100644 index 0000000..135ece5 Binary files /dev/null and b/gui/sprites/plan.5.png differ diff --git a/gui/sprites/plan.6.png b/gui/sprites/plan.6.png new file mode 100644 index 0000000..e3e2989 Binary files /dev/null and b/gui/sprites/plan.6.png differ diff --git a/gui/sprites/plan.7.png b/gui/sprites/plan.7.png new file mode 100644 index 0000000..5f4ab4b Binary files /dev/null and b/gui/sprites/plan.7.png differ diff --git a/gui/sprites/plan.8.png b/gui/sprites/plan.8.png new file mode 100644 index 0000000..a7912c6 Binary files /dev/null and b/gui/sprites/plan.8.png differ diff --git a/gui/sprites/plan.9.png b/gui/sprites/plan.9.png new file mode 100644 index 0000000..412736a Binary files /dev/null and b/gui/sprites/plan.9.png differ diff --git a/gui/src/battle_ui.rs b/gui/src/battle_ui.rs new file mode 100644 index 0000000..9556d3c --- /dev/null +++ b/gui/src/battle_ui.rs @@ -0,0 +1,70 @@ +use game_util::prelude::*; +use battle::{ Battle, BattleUpdate }; +use crate::player_draw::PlayerDrawState; +use crate::res::Resources; + +pub struct BattleUi { + player_1_graphics: PlayerDrawState, + player_2_graphics: PlayerDrawState, + time: u32 +} + +impl BattleUi { + pub fn new(battle: &Battle, p1_name: String, p2_name: String) -> Self { + BattleUi { + player_1_graphics: PlayerDrawState::new(battle.player_1.board.next_queue(), p1_name), + player_2_graphics: PlayerDrawState::new(battle.player_2.board.next_queue(), p2_name), + time: 0 + } + } + + pub fn update( + &mut self, + res: &mut Resources, + update: BattleUpdate, + p1_info_update: Option, + p2_info_update: Option + ) { + for event in update.player_1.events.iter().chain(update.player_2.events.iter()) { + use battle::Event::*; + match event { + PieceMoved | SoftDropped | PieceRotated => { + if res.move_sound_sink.len() <= 1 { + res.move_sound_sink.append(res.move_sound.sound()); + } + } + PiecePlaced { hard_drop_distance, locked, .. } => { + if hard_drop_distance.is_some() { + res.hard_drop.play(); + } + if locked.placement_kind.is_clear() { + res.line_clear.play(); + } + } + _ => {} + } + } + + self.player_1_graphics.update(update.player_1, p1_info_update, update.time); + self.player_2_graphics.update(update.player_2, p2_info_update, update.time); + self.time = update.time; + } + + pub fn draw(&self, res: &mut Resources) { + res.text.draw_text( + &format!("{}:{:02}", self.time / 60 / 60, self.time / 60 % 60), + 20.0, 1.5, + game_util::Alignment::Center, + [0xFF; 4], 1.0, 0 + ); + + self.player_1_graphics.draw(res, 0.0+1.0); + self.player_2_graphics.draw(res, 20.0+1.0); + + res.sprite_batch.render(Transform3D::ortho( + 0.0, 40.0, + 0.0, 23.0, + -1.0, 1.0 + )); + } +} \ No newline at end of file diff --git a/gui/src/common.rs b/gui/src/common.rs deleted file mode 100644 index 64e9caf..0000000 --- a/gui/src/common.rs +++ /dev/null @@ -1,518 +0,0 @@ -use ggez::{ Context, GameResult }; -use ggez::graphics::*; -use ggez::graphics::spritebatch::SpriteBatch; -use std::collections::VecDeque; -use libtetris::*; -use battle::{ PlayerUpdate, Event }; -use arrayvec::ArrayVec; -use crate::interface::text; -use rand::prelude::*; - -pub struct BoardDrawState { - board: ArrayVec<[ColoredRow; 40]>, - state: State, - statistics: Statistics, - garbage_queue: u32, - dead: bool, - hold_piece: Option, - next_queue: VecDeque, - game_time: u32, - combo_splash: Option<(u32, u32)>, - back_to_back_splash: Option, - clear_splash: Option<(&'static str, u32)>, - name: String, - hard_drop_particles: Option<(u32, Vec<(f32, f32, f32)>)>, - info: Option -} - -enum State { - Falling(FallingPiece, FallingPiece), - LineClearAnimation(ArrayVec<[i32; 4]>, i32), - Delay -} - -impl BoardDrawState { - pub fn new(queue: impl IntoIterator, name: String) -> Self { - BoardDrawState { - board: ArrayVec::from([*ColoredRow::EMPTY; 40]), - state: State::Delay, - statistics: Statistics::default(), - garbage_queue: 0, - dead: false, - hold_piece: None, - next_queue: queue.into_iter().collect(), - game_time: 0, - combo_splash: None, - back_to_back_splash: None, - clear_splash: None, - hard_drop_particles: None, - name, - info: None - } - } - - pub fn update( - &mut self, update: PlayerUpdate, info_update: Option, time: u32 - ) { - self.garbage_queue = update.garbage_queue; - self.info = info_update.or(self.info.take()); - self.game_time = time; - if let State::LineClearAnimation(_, ref mut frames) = self.state { - *frames += 1; - } - if let Some((_, timer)) = &mut self.combo_splash { - if *timer == 0 { - self.combo_splash = None; - } else { - *timer -= 1; - } - } - if let Some(timer) = &mut self.back_to_back_splash { - if *timer == 0 { - self.back_to_back_splash = None; - } else { - *timer -= 1; - } - } - if let Some((_, timer)) = &mut self.clear_splash { - if *timer == 0 { - self.clear_splash = None; - } else { - *timer -= 1; - } - } - if let Some((timer, particles)) = &mut self.hard_drop_particles { - if *timer == 0 { - self.hard_drop_particles = None; - } else { - *timer -= 1; - let t = *timer as f32 / 10.0; - for (x, y, factor) in particles { - *x += *factor * t / 5.0; - *y += t*t * (1.0 - factor.abs()); - } - } - } - for event in &update.events { - match event { - Event::PiecePlaced { piece, locked, hard_drop_distance } => { - self.statistics.update(&locked); - if hard_drop_distance.is_some() { - let mut particles = vec![]; - for &(x, y, _) in &piece.cells() { - if y == 0 || self.board[y as usize - 1].get(x as usize) { - for i in 0..5 { - let r: f32 = thread_rng().gen(); - particles.push((x as f32+r, y as f32, r-0.5)); - let r = i as f32 / 4.0; - particles.push((x as f32+r, y as f32, r-0.5)); - } - } - } - self.hard_drop_particles = Some((5, particles)); - } - for &(x, y, _) in &piece.cells() { - self.board[y as usize].set(x as usize, piece.kind.0.color()); - } - if locked.cleared_lines.is_empty() { - self.state = State::Delay; - } else { - self.state = State::LineClearAnimation(locked.cleared_lines.clone(), 0); - } - if locked.b2b { - self.back_to_back_splash = Some(75); - } - let combo = locked.combo.unwrap_or(0); - if combo > 0 { - self.combo_splash = Some((combo, 75)); - } - if locked.perfect_clear { - self.clear_splash = Some(("Perfect Clear", 135)); - self.back_to_back_splash = None; - } else if locked.placement_kind.is_hard() { - self.clear_splash = Some((locked.placement_kind.name(), 75)); - } - } - Event::PieceHeld(piece) => { - self.hold_piece = Some(*piece); - self.state = State::Delay; - } - Event::PieceSpawned { new_in_queue } => { - self.next_queue.push_back(*new_in_queue); - self.next_queue.pop_front(); - } - Event::PieceFalling(piece, ghost) => { - self.state = State::Falling(*piece, *ghost); - } - Event::EndOfLineClearDelay => { - self.state = State::Delay; - self.board.retain(|row| !row.is_full()); - while !self.board.is_full() { - self.board.push(*ColoredRow::EMPTY); - } - } - Event::GarbageAdded(columns) => { - self.board.truncate(40 - columns.len()); - for &col in columns { - let mut row = *ColoredRow::EMPTY; - for x in 0..10 { - if x != col { - row.set(x, CellColor::Garbage); - } - } - self.board.insert(0, row); - } - } - Event::GameOver => self.dead = true, - _ => {} - } - } - } - - pub fn draw( - &self, - ctx: &mut Context, - sprites: &mut SpriteBatch, - particle_mesh: &mut MeshBuilder, - text_x: f32, - scale: f32 - ) -> GameResult { - // Draw the playfield - for y in 0..21 { - for x in 0..10 { - sprites.add(draw_tile( - x as i32+3, y as i32, if self.board[y].cell_color(x) == CellColor::Empty - { 0 } else { 1 }, - 0, cell_color_to_color({ - let color = self.board[y].cell_color(x); - if self.dead && color != CellColor::Empty { - CellColor::Unclearable - } else { - color - } - }) - )); - } - } - // Draw hard drop particle effects - if let Some((timer, particles)) = &self.hard_drop_particles { - for &(x, y, factor) in particles { - particle_mesh.circle( - DrawMode::Fill(Default::default()), - [x+3.0,20.25-y], - (factor/20.0+0.05) * (*timer + 1) as f32 / 5.0, - 0.1/scale, - WHITE - ); - } - } - // Draw either the falling piece or the line clear animation - match self.state { - State::Falling(piece, ghost) => { - for &(x,y,_) in &ghost.cells() { - sprites.add(draw_tile( - x+3, y, 2, 0, cell_color_to_color(piece.kind.0.color()) - )); - } - for &(x,y,_) in &piece.cells() { - sprites.add(draw_tile( - x+3, y, 1, 0, cell_color_to_color(piece.kind.0.color()) - )); - } - } - State::LineClearAnimation(ref lines, frame) => { - let frame_x = frame.min(35) / 12; - let frame_y = frame.min(35) % 12; - for &y in lines { - sprites.add(draw_tile( - 3, y, frame_x*3+3, frame_y, WHITE - )); - sprites.add(draw_tile( - 12, y, frame_x*3+5, frame_y, WHITE - )); - for x in 1..9 { - sprites.add(draw_tile( - x+3, y, frame_x*3+4, frame_y, WHITE - )); - } - } - } - _ => {} - } - // Draw hold piece and next queue - if let Some(piece) = self.hold_piece { - draw_piece_preview(sprites, 0, 18, piece); - } - for (i, &piece) in self.next_queue.iter().enumerate() { - draw_piece_preview(sprites, 13, 18 - (i*2) as i32, piece); - } - // Draw the pending garbage bar - if self.garbage_queue > 0 { - particle_mesh.rectangle( - DrawMode::Fill(FillOptions::tolerance(0.1 / scale)), - Rect { - x: 13.0, - w: 0.15, - y: 20.25 - self.garbage_queue as f32, - h: self.garbage_queue as f32 - }, - Color::from_rgb(255, 32, 32) - ); - } - queue_text( - ctx, &text("Hold", scale, 3.0*scale), [text_x, scale*0.25], None - ); - queue_text( - ctx, &text("Next", scale, 3.0*scale), [text_x+13.0*scale, scale*0.25], None - ); - queue_text( - ctx, &text("Statistics", scale*0.75, 4.0*scale), [text_x-1.0*scale, scale*3.0], None - ); - // Prepare statistics text - let seconds = self.game_time as f32 / 60.0; - let lines = vec![ - ("Pieces", format!("{}", self.statistics.pieces)), - ("PPS", format!("{:.1}", self.statistics.pieces as f32 / seconds)), - ("Lines", format!("{}", self.statistics.lines)), - ("Attack", format!("{}", self.statistics.attack)), - ("APM", format!("{:.1}", self.statistics.attack as f32 / seconds * 60.0)), - ("APP", format!("{:.3}", self.statistics.attack as f32 / self.statistics.pieces as f32)), - ("Max Ren", format!("{}", self.statistics.max_combo)), - ("Single", format!("{}", self.statistics.singles)), - ("Double", format!("{}", self.statistics.doubles)), - ("Triple", format!("{}", self.statistics.triples)), - ("Tetris", format!("{}", self.statistics.tetrises)), - // ("Mini T0", format!("{}", self.statistics.mini_tspin_zeros)), - // ("Mini T1", format!("{}", self.statistics.mini_tspin_singles)), - // ("Mini T2", format!("{}", self.statistics.mini_tspin_doubles)), - ("T-Spin 0", format!("{}", self.statistics.tspin_zeros)), - ("T-Spin 1", format!("{}", self.statistics.tspin_singles)), - ("T-Spin 2", format!("{}", self.statistics.tspin_doubles)), - ("T-Spin 3", format!("{}", self.statistics.tspin_triples)), - ("Perfect", format!("{}", self.statistics.perfect_clears)) - ]; - // Draw statistics text - let mut y = 3.75*scale; - for (label, stat) in lines { - queue_text( - ctx, &text(label, scale*0.66, 0.0), [text_x-0.75*scale, y], None - ); - queue_text( - ctx, &text(stat, scale*0.66, -3.5*scale), [text_x-0.75*scale, y], None - ); - y += scale * 0.66; - } - // Draw player name - let y = (self.next_queue.len() as f32 * 2.0 + 1.0) * scale; - queue_text( - ctx, &text(&*self.name, scale*0.66, 3.5*scale), [text_x+13.25*scale, y], None - ); - if let Some(ref info) = self.info { - // Draw bot information - let y = 12.0 * scale; - queue_text( - ctx, &text("Depth", scale*0.66, 0.0), [text_x-0.75*scale, y + 2.0*scale], None - ); - queue_text( - ctx, - &text(format!("{}", info.depth), scale*0.66, -3.5*scale), - [text_x-0.75*scale, y + 2.0*scale], - None - ); - queue_text( - ctx, &text("Nodes", scale*0.66, 0.0), [text_x-0.75*scale, y + 2.7*scale], None - ); - queue_text( - ctx, - &text(format!("{}", info.nodes), scale*0.66, -3.5*scale), - [text_x-0.75*scale, y + 2.7*scale], - None - ); - queue_text( - ctx, &text("O. Rank", scale*0.66, 0.0), [text_x-0.75*scale, y + 3.4*scale], None - ); - queue_text( - ctx, - &text(format!("{}", info.original_rank), scale*0.66, -3.5*scale), - [text_x-0.75*scale, y + 3.4*scale], - None - ); - // Draw plan description - queue_text( - ctx, &text("Plan:", scale*0.66, 0.0), [text_x-0.75*scale, y + 4.1*scale], None - ); - let mut y = y + 4.1*scale; - let mut x = text_x-0.75*scale; - let mut has_pc = false; - let mut has_send = false; - for (_, lock) in &info.plan { - x += 1.3 * scale; - if x > text_x+2.25*scale { - x = text_x-0.75*scale; - y += 0.7 * scale; - } - queue_text( - ctx, - &text( - if lock.perfect_clear { - has_pc = true; - "PC" - } else { - if lock.placement_kind.is_hard() && lock.placement_kind.is_clear() { - has_send = true; - } - lock.placement_kind.short_name() - }, - scale*0.66, 0.0 - ), - [x, y], - None - ) - } - // Draw plan visualization - if has_send || has_pc { - let mut y_map = [0; 40]; - for i in 0..40 { - y_map[i] = i as i32; - } - for (placement, lock) in &info.plan { - for &(x, y, d) in &placement.cells() { - let (tx, ty) = dir_to_tile(d); - sprites.add(draw_tile( - x+3, y_map[y as usize], - tx, ty, - cell_color_to_color(placement.kind.0.color()) - )); - } - let mut new_map = [0; 40]; - let mut j = 0; - for i in 0..40 { - if !lock.cleared_lines.contains(&i) { - new_map[j] = y_map[i as usize]; - j += 1; - } - } - y_map = new_map; - - if !has_pc && lock.placement_kind.is_hard() && lock.placement_kind.is_clear() - || lock.perfect_clear { - break - } - } - } - } - // Draw clear info stuff - if let Some(timer) = self.back_to_back_splash { - queue_text( - ctx, - &text("Back-To-Back", scale, 0.0), - [text_x+3.5*scale, 20.7*scale], - Some(Color::new(1.0, 1.0, 1.0, timer.min(15) as f32 / 15.0)) - ); - } - if let Some((combo, timer)) = self.combo_splash { - queue_text( - ctx, - &text(format!("{} Combo", combo), scale, -9.0*scale), - [text_x+3.5*scale, 20.7*scale], - Some(Color::new(1.0, 1.0, 1.0, timer.min(15) as f32 / 15.0)) - ); - } - if let Some((txt, timer)) = self.clear_splash { - queue_text( - ctx, - &text(txt, scale, 9.0*scale), - [text_x+3.5*scale, 21.6*scale], - Some(Color::new(1.0, 1.0, 1.0, timer.min(15) as f32 / 15.0)) - ); - } - Ok(()) - } -} - -fn cell_color_to_color(cell_color: CellColor) -> Color { - match cell_color { - CellColor::Empty => WHITE, - CellColor::Garbage => Color::from_rgb(160, 160, 160), - CellColor::Unclearable => Color::from_rgb(64, 64, 64), - CellColor::Z => Color::from_rgb(255, 32, 32), - CellColor::S => Color::from_rgb(32, 255, 32), - CellColor::O => Color::from_rgb(255, 255, 32), - CellColor::L => Color::from_rgb(255, 143, 32), - CellColor::J => Color::from_rgb(96, 96, 255), - CellColor::I => Color::from_rgb(32, 255, 255), - CellColor::T => Color::from_rgb(143, 32, 255) - } -} - -fn tile(x: i32, y: i32) -> Rect { - Rect { - x: x as f32 * (85.0/1024.0) + 1.0/1024.0, - y: y as f32 * (85.0/1024.0) + 1.0/1024.0, - h: 83.0/1024.0, - w: 83.0/1024.0 - } -} - -fn draw_piece_preview(sprites: &mut SpriteBatch, x: i32, y: i32, piece: Piece) { - let ty = match piece { - Piece::I => 1, - Piece::O => 2, - Piece::T => 3, - Piece::L => 4, - Piece::J => 5, - Piece::S => 6, - Piece::Z => 7 - }; - let color = cell_color_to_color(piece.color()); - for dx in 0..3 { - if dx != 1 && piece == Piece::O { continue } - sprites.add(draw_tile(x+dx, y, dx, ty, color)); - } -} - -fn draw_tile(x: i32, y: i32, tx: i32, ty: i32, color: Color) -> DrawParam { - DrawParam::new() - .dest([x as f32, (20-y) as f32 - 0.75]) - .src(tile(tx, ty)) - .color(color) - .scale([SPRITE_SCALE, SPRITE_SCALE]) -} - -const SPRITE_SCALE: f32 = 1.0/83.0; - -fn dir_to_tile(dir: enumset::EnumSet) -> (i32, i32) { - use libtetris::Direction::*; - use enumset::EnumSet; - if dir == EnumSet::only(Up) { - (2, 10) - } else if dir == EnumSet::only(Down) { - (2, 8) - } else if dir == EnumSet::only(Left) { - (2, 11) - } else if dir == EnumSet::only(Right) { - (0, 11) - } else if dir == Left | Right { - (1, 11) - } else if dir == Up | Down { - (2, 9) - } else if dir == Left | Up { - (1, 10) - } else if dir == Left | Down { - (1, 9) - } else if dir == Right | Up { - (0, 10) - } else if dir == Right | Down { - (0, 9) - } else if dir == Left | Right | Up { - (0, 8) - } else if dir == Left | Right | Down { - (1, 8) - } else if dir == Up | Down | Left { - (2, 2) - } else if dir == Up | Down | Right { - (0, 2) - } else { - (2, 0) - } -} diff --git a/gui/src/font/LICENSE_OFL.txt b/gui/src/font/LICENSE_OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/gui/src/font/LICENSE_OFL.txt @@ -0,0 +1,92 @@ +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/gui/src/font/NotoSerif-Regular.ttf b/gui/src/font/NotoSerif-Regular.ttf new file mode 100644 index 0000000..5a89b2f Binary files /dev/null and b/gui/src/font/NotoSerif-Regular.ttf differ diff --git a/gui/src/input.rs b/gui/src/input.rs index 3d9f38d..1fef349 100644 --- a/gui/src/input.rs +++ b/gui/src/input.rs @@ -1,13 +1,12 @@ -use ggez::Context; -use ggez::input::keyboard::{ KeyCode, is_key_pressed }; -use ggez::input::gamepad::Gamepad; -use ggez::event::{ Button, Axis }; use libtetris::*; use battle::{ Event, PieceMoveExecutor }; +use game_util::glutin::VirtualKeyCode; +use gilrs::{ Gamepad, Axis, Button }; use serde::{ Serialize, Deserialize }; +use std::collections::HashSet; pub trait InputSource { - fn controller(&mut self, ctx: &Context, gamepad: Option) -> Controller; + fn controller(&self, keys: &HashSet, gamepad: Option) -> Controller; fn update( &mut self, board: &Board, events: &[Event], incoming: u32 ) -> Option; @@ -30,7 +29,7 @@ impl BotInput { } impl InputSource for BotInput { - fn controller(&mut self, _: &Context, _: Option) -> Controller { + fn controller(&self, _keys: &HashSet, _gamepad: Option) -> Controller { self.controller } @@ -74,49 +73,38 @@ impl InputSource for BotInput { #[derive(Copy, Clone, Serialize, Deserialize, Default, Debug)] pub struct UserInput { - keyboard: KeyboardConfig, - gamepad: GamepadConfig, + keyboard: Config, + gamepad: Config, } #[derive(Copy, Clone, Serialize, Deserialize, Debug)] -struct KeyboardConfig { - left: KeyCode, - right: KeyCode, - rotate_left: KeyCode, - rotate_right: KeyCode, - hard_drop: KeyCode, - soft_drop: KeyCode, - hold: KeyCode +struct Config { + left: T, + right: T, + rotate_left: T, + rotate_right: T, + hard_drop: T, + soft_drop: T, + hold: T } -#[derive(Copy, Clone, Serialize, Deserialize, Debug)] -struct GamepadConfig { - left: GamepadControl, - right: GamepadControl, - rotate_left: GamepadControl, - rotate_right: GamepadControl, - hard_drop: GamepadControl, - soft_drop: GamepadControl, - hold: GamepadControl -} - -impl Default for KeyboardConfig { +impl Default for Config { fn default() -> Self { - KeyboardConfig { - left: KeyCode::Left, - right: KeyCode::Right, - rotate_left: KeyCode::Z, - rotate_right: KeyCode::X, - hard_drop: KeyCode::Space, - soft_drop: KeyCode::Down, - hold: KeyCode::C, + Config { + left: VirtualKeyCode::Left, + right: VirtualKeyCode::Right, + rotate_left: VirtualKeyCode::Z, + rotate_right: VirtualKeyCode::X, + hard_drop: VirtualKeyCode::Space, + soft_drop: VirtualKeyCode::Down, + hold: VirtualKeyCode::C, } } } -impl Default for GamepadConfig { +impl Default for Config { fn default() -> Self { - GamepadConfig { + Config { left: GamepadControl::Button(Button::DPadLeft), right: GamepadControl::Button(Button::DPadRight), rotate_left: GamepadControl::Button(Button::South), @@ -129,15 +117,29 @@ impl Default for GamepadConfig { } impl InputSource for UserInput { - fn controller(&mut self, ctx: &Context, c: Option) -> Controller { + fn controller(&self, keys: &HashSet, gamepad: Option) -> Controller { Controller { - left: read_input(ctx, c, self.keyboard.left, self.gamepad.left), - right: read_input(ctx, c, self.keyboard.right, self.gamepad.right), - rotate_left: read_input(ctx, c, self.keyboard.rotate_left, self.gamepad.rotate_left), - rotate_right: read_input(ctx, c, self.keyboard.rotate_right, self.gamepad.rotate_right), - hard_drop: read_input(ctx, c, self.keyboard.hard_drop, self.gamepad.hard_drop), - soft_drop: read_input(ctx, c, self.keyboard.soft_drop, self.gamepad.soft_drop), - hold: read_input(ctx, c, self.keyboard.hold, self.gamepad.hold), + left: self.read_input( + keys, gamepad, self.keyboard.left, self.gamepad.left + ), + right: self.read_input( + keys, gamepad, self.keyboard.right, self.gamepad.right + ), + rotate_left: self.read_input( + keys, gamepad, self.keyboard.rotate_left, self.gamepad.rotate_left + ), + rotate_right: self.read_input( + keys, gamepad, self.keyboard.rotate_right, self.gamepad.rotate_right + ), + hard_drop: self.read_input( + keys, gamepad, self.keyboard.hard_drop, self.gamepad.hard_drop + ), + soft_drop: self.read_input( + keys, gamepad, self.keyboard.soft_drop, self.gamepad.soft_drop + ), + hold: self.read_input( + keys, gamepad, self.keyboard.hold, self.gamepad.hold + ), } } @@ -146,61 +148,23 @@ impl InputSource for UserInput { } } -fn read_input( - ctx: &Context, controller: Option, keyboard: KeyCode, gamepad: GamepadControl -) -> bool { - is_key_pressed(ctx, keyboard) || controller.map_or(false, |c| match gamepad { - GamepadControl::Button(button) => c.is_pressed(button), - GamepadControl::PositiveAxis(axis) => c.value(axis) > 0.5, - GamepadControl::NegativeAxis(axis) => c.value(axis) < -0.5, - }) +impl UserInput { + fn read_input( + &self, + keys: &HashSet, controller: Option, + keyboard: VirtualKeyCode, gamepad: GamepadControl + ) -> bool { + keys.contains(&keyboard) || controller.map_or(false, |c| match gamepad { + GamepadControl::Button(button) => c.is_pressed(button), + GamepadControl::PositiveAxis(axis) => c.value(axis) > 0.5, + GamepadControl::NegativeAxis(axis) => c.value(axis) < -0.5, + }) + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] enum GamepadControl { - #[serde(with = "ButtonDef")] Button(Button), - #[serde(with = "AxisDef")] NegativeAxis(Axis), - #[serde(with = "AxisDef")] PositiveAxis(Axis) -} - -#[derive(Serialize, Deserialize)] -#[serde(remote = "Button")] -enum ButtonDef { - South, - East, - North, - West, - C, - Z, - LeftTrigger, - LeftTrigger2, - RightTrigger, - RightTrigger2, - Select, - Start, - Mode, - LeftThumb, - RightThumb, - DPadUp, - DPadDown, - DPadLeft, - DPadRight, - Unknown -} - -#[derive(Serialize, Deserialize)] -#[serde(remote = "Axis")] -enum AxisDef { - LeftStickX, - LeftStickY, - LeftZ, - RightStickX, - RightStickY, - RightZ, - DPadX, - DPadY, - Unknown } \ No newline at end of file diff --git a/gui/src/interface.rs b/gui/src/interface.rs deleted file mode 100644 index 4371a70..0000000 --- a/gui/src/interface.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::common::BoardDrawState; -use crate::Resources; -use ggez::{ Context, GameResult }; -use ggez::audio::SoundSource; -use ggez::graphics::*; -use battle::{ BattleUpdate, Battle }; - -pub struct Gui { - player_1_graphics: BoardDrawState, - player_2_graphics: BoardDrawState, - time: u32, - multiplier: f32, - move_sound_play: u32 -} - -impl Gui { - pub fn new(battle: &Battle, p1_name: String, p2_name: String) -> Self { - Gui { - player_1_graphics: BoardDrawState::new(battle.player_1.board.next_queue(), p1_name), - player_2_graphics: BoardDrawState::new(battle.player_2.board.next_queue(), p2_name), - time: 0, - multiplier: 1.0, - move_sound_play: 0 - } - } - - pub fn update( - &mut self, - update: BattleUpdate, - p1_info_update: Option, - p2_info_update: Option, - res: &mut Resources - ) -> GameResult { - for event in update.player_1.events.iter().chain(update.player_2.events.iter()) { - use battle::Event::*; - match event { - PieceMoved | SoftDropped | PieceRotated => if self.move_sound_play == 0 { - if let Some(move_sound) = &mut res.move_sound { - move_sound.play_detached()?; - } - self.move_sound_play = 2; - } - // StackTouched => self.stack_touched.play_detached()?, - // PieceTSpined => self.tspin.play_detached()?, - PiecePlaced { hard_drop_distance, locked, .. } => { - if hard_drop_distance.is_some() { - if let Some(hard_drop) = &mut res.hard_drop { - hard_drop.play_detached()?; - } - } - if locked.placement_kind.is_clear() { - if let Some(line_clear) = &mut res.line_clear { - line_clear.play_detached()?; - } - } - } - _ => {} - } - } - if self.move_sound_play != 0 { - self.move_sound_play -= 1; - } - - self.player_1_graphics.update(update.player_1, p1_info_update, update.time); - self.player_2_graphics.update(update.player_2, p2_info_update, update.time); - self.time = update.time; - self.multiplier = update.attack_multiplier; - - Ok(()) - } - - pub fn draw( - &mut self, ctx: &mut Context, res: &mut Resources, scale: f32, center: f32 - ) -> GameResult<()> { - push_transform(ctx, Some(DrawParam::new() - .scale([scale, scale]) - .dest([center - 17.5 * scale, 0.0]) - .to_matrix())); - apply_transformations(ctx)?; - - res.sprites.clear(); - let mut mesh = MeshBuilder::new(); - self.player_1_graphics.draw(ctx, &mut res.sprites, &mut mesh, center - 17.5*scale, scale)?; - draw(ctx, &res.sprites, DrawParam::default())?; - if let Ok(mesh) = mesh.build(ctx) { - draw(ctx, &mesh, DrawParam::default())?; - } - - pop_transform(ctx); - - push_transform(ctx, Some(DrawParam::new() - .scale([scale, scale]) - .dest([center + 1.5*scale, 0.0]) - .to_matrix())); - apply_transformations(ctx)?; - - let mut mesh = MeshBuilder::new(); - res.sprites.clear(); - self.player_2_graphics.draw(ctx, &mut res.sprites, &mut mesh, center+1.5*scale, scale)?; - draw(ctx, &res.sprites, DrawParam::default())?; - if let Ok(mesh) = mesh.build(ctx) { - draw(ctx, &mesh, DrawParam::default())?; - } - - pop_transform(ctx); - - queue_text( - ctx, - &text( - format!("{}:{:02}", self.time / 60 / 60, self.time / 60 % 60), - scale*1.5, 8.0*scale - ), - [center-4.0*scale, 20.6*scale], - None - ); - if self.multiplier != 1.0 { - queue_text( - ctx, - &text(format!("Margin Time: x{:.1}", self.multiplier), scale*1.0, 8.0*scale), - [center-4.0*scale, 21.9*scale], - None - ); - } - - apply_transformations(ctx)?; - draw_queued_text( - ctx, DrawParam::new(), None, FilterMode::Linear - )?; - - Ok(()) - } -} - -/// Returns (scale, center) -pub fn setup_graphics(ctx: &mut Context) -> GameResult<(f32, f32)> { - clear(ctx, BLACK); - let dpi = window(ctx).get_hidpi_factor() as f32; - let size = drawable_size(ctx); - let size = (size.0 * dpi, size.1 * dpi); - let center = size.0 / 2.0; - let scale = size.1 / 23.0; - set_screen_coordinates(ctx, Rect { - x: 0.0, y: 0.0, w: size.0, h: size.1 - })?; - - Ok((scale, center)) -} - -pub fn text(s: impl Into, ts: f32, width: f32) -> Text { - let mut text = Text::new(s); - text.set_font(Default::default(), Scale::uniform(ts*0.75)); - if width != 0.0 { - if width < 0.0 { - text.set_bounds([-width, 1230.0], Align::Right); - } else { - text.set_bounds([width, 1230.0], Align::Center); - } - } - text -} \ No newline at end of file diff --git a/gui/src/local.rs b/gui/src/local.rs deleted file mode 100644 index 6684888..0000000 --- a/gui/src/local.rs +++ /dev/null @@ -1,217 +0,0 @@ -use ggez::event::EventHandler; -use ggez::{ Context, GameResult }; -use ggez::graphics; -use ggez::timer; -use ggez::input::gamepad::{ GamepadId, gamepad }; -use libtetris::Board; -use battle::{ Battle, GameConfig }; -use crate::interface::{ Gui, text }; -use crate::Resources; -use rand::prelude::*; -use crate::input::InputSource; -use crate::replay::InfoReplay; -use std::collections::VecDeque; -use libflate::deflate; - -type InputFactory = dyn Fn(Board) -> (Box, String); - -pub struct LocalGame<'a> { - gui: Gui, - battle: Battle, - p1_input_factory: Box, - p2_input_factory: Box, - p1_input: Box, - p2_input: Box, - p1_wins: u32, - p2_wins: u32, - p1_info_updates: VecDeque>, - p2_info_updates: VecDeque>, - state: State, - resources: &'a mut Resources, - p1_config: GameConfig, - p2_config: GameConfig, - gamepad: Option -} - -enum State { - Playing, - GameOver(u32), - Starting(u32) -} - -impl<'a> LocalGame<'a> { - pub fn new( - resources: &'a mut Resources, - p1: Box, - p2: Box, - p1_config: GameConfig, p2_config: GameConfig - ) -> Self { - let mut battle = Battle::new( - p1_config, p2_config, thread_rng().gen(), thread_rng().gen(), thread_rng().gen() - ); - let (p1_input, p1_name) = p1(battle.player_1.board.to_compressed()); - let (p2_input, p2_name) = p2(battle.player_2.board.to_compressed()); - battle.replay.p1_name = p1_name.clone(); - battle.replay.p2_name = p2_name.clone(); - LocalGame { - p1_input, p2_input, - p1_input_factory: p1, - p2_input_factory: p2, - gui: Gui::new(&battle, p1_name, p2_name), - battle, - p1_wins: 0, - p2_wins: 0, - p1_info_updates: VecDeque::new(), - p2_info_updates: VecDeque::new(), - state: State::Starting(180), - resources, - p1_config, p2_config, - gamepad: None - } - } -} - -impl EventHandler for LocalGame<'_> { - fn update(&mut self, ctx: &mut Context) -> GameResult { - while timer::check_update_time(ctx, 60) { - let do_update = match self.state { - State::GameOver(0) => { - let mut encoder = deflate::Encoder::new( - std::fs::File::create("replay.dat" - ).unwrap()); - bincode::serialize_into( - &mut encoder, - &InfoReplay { - replay: self.battle.replay.clone(), - p1_info_updates: self.p1_info_updates.clone(), - p2_info_updates: self.p2_info_updates.clone() - } - ).unwrap(); - encoder.finish().unwrap(); - - // Don't catch up after pause due to replay saving - while timer::check_update_time(ctx, 60) {} - - self.battle = Battle::new( - self.p1_config, self.p2_config, - thread_rng().gen(), thread_rng().gen(), thread_rng().gen() - ); - - let (p1_input, p1_name) = (self.p1_input_factory)( - self.battle.player_1.board.to_compressed() - ); - let (p2_input, p2_name) = (self.p2_input_factory)( - self.battle.player_2.board.to_compressed() - ); - - self.gui = Gui::new(&self.battle, p1_name.clone(), p2_name.clone()); - self.p1_input = p1_input; - self.p2_input = p2_input; - self.battle.replay.p1_name = p1_name; - self.battle.replay.p2_name = p2_name; - - self.p1_info_updates.clear(); - self.p2_info_updates.clear(); - - self.state = State::Starting(180); - false - } - State::GameOver(ref mut delay) => { - *delay -= 1; - true - } - State::Starting(0) => { - self.state = State::Playing; - true - } - State::Starting(ref mut delay) => { - *delay -= 1; - false - } - State::Playing => true - }; - - if do_update { - let gamepad = self.gamepad.map(|id| gamepad(ctx, id)); - let p1_controller = self.p1_input.controller(ctx, gamepad); - let p2_controller = self.p2_input.controller(ctx, gamepad); - - let update = self.battle.update(p1_controller, p2_controller); - - let p1_info_update = self.p1_input.update( - &self.battle.player_1.board, &update.player_1.events, - self.battle.player_1.garbage_queue - ); - let p2_info_update = self.p2_input.update( - &self.battle.player_2.board, &update.player_2.events, - self.battle.player_2.garbage_queue - ); - - self.p1_info_updates.push_back(p1_info_update.clone()); - self.p2_info_updates.push_back(p2_info_update.clone()); - - if let State::Playing = self.state { - for event in &update.player_1.events { - use battle::Event::*; - match event { - GameOver => { - self.p2_wins += 1; - self.state = State::GameOver(300); - } - _ => {} - } - } - for event in &update.player_2.events { - use battle::Event::*; - match event { - GameOver => { - self.p1_wins += 1; - self.state = State::GameOver(300); - } - _ => {} - } - } - } - - self.gui.update(update, p1_info_update, p2_info_update, self.resources)?; - } - } - - Ok(()) - } - - fn draw(&mut self, ctx: &mut Context) -> GameResult { - let (scale, center) = crate::interface::setup_graphics(ctx)?; - - graphics::queue_text( - ctx, - &text(format!("{} - {}", self.p1_wins, self.p2_wins), scale*2.0, 6.0*scale), - [center-3.0*scale, 19.0*scale], - None - ); - - if let State::Starting(t) = self.state { - let txt = text(format!("{}", t / 60 + 1), scale * 4.0, 10.0*scale); - graphics::queue_text(ctx, &txt, [center-14.5*scale, 9.0*scale], None); - graphics::queue_text(ctx, &txt, [center+4.5*scale, 9.0*scale], None); - } - - self.gui.draw(ctx, self.resources, scale, center)?; - - graphics::set_window_title(ctx, &format!("Cold Clear (FPS: {:.0})", ggez::timer::fps(ctx))); - - graphics::present(ctx) - } - - fn gamepad_button_down_event( - &mut self, _: &mut Context, _: ggez::event::Button, id: GamepadId - ) { - self.gamepad.get_or_insert(id); - } - - fn gamepad_axis_event( - &mut self, _: &mut Context, _: ggez::event::Axis, _: f32, id: GamepadId - ) { - self.gamepad.get_or_insert(id); - } -} diff --git a/gui/src/main.rs b/gui/src/main.rs index c7e108b..0d81a49 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,29 +1,115 @@ #![windows_subsystem = "windows"] -use ggez::ContextBuilder; -use ggez::event; -use ggez::graphics::{ Image }; -use ggez::graphics::spritebatch::SpriteBatch; -use ggez::audio; +use game_util::prelude::*; +use game_util::GameloopCommand; +use game_util::glutin::*; +use game_util::glutin::dpi::LogicalSize; +use gilrs::{ Gilrs, Gamepad, GamepadId }; use battle::GameConfig; -use serde::{ Serialize, Deserialize }; +use std::collections::HashSet; -mod common; -mod local; -mod input; -mod interface; +mod player_draw; +mod battle_ui; +mod res; +mod realtime; mod replay; +mod input; -use local::LocalGame; +use realtime::RealtimeGame; use replay::ReplayGame; -use crate::input::UserInput; -pub struct Resources { - sprites: SpriteBatch, +struct CCGui<'a> { + context: &'a WindowedContext, + lsize: LogicalSize, + res: res::Resources, + state: Box, + gilrs: Gilrs, + keys: HashSet, + p1: Option, + p2: Option +} + +impl game_util::Game for CCGui<'_> { + fn update(&mut self) -> GameloopCommand { + let gilrs = &self.gilrs; + let p1 = self.p1.map(|id| gilrs.gamepad(id)); + let p2 = self.p2.map(|id| gilrs.gamepad(id)); + if let Some(new_state) = self.state.update(&mut self.res, &self.keys, p1, p2) { + self.state = new_state; + } + GameloopCommand::Continue + } + + fn render(&mut self, _: f64, smooth_delta: f64) { + while let Some(event) = self.gilrs.next_event() { + match event.event { + gilrs::EventType::Connected => if self.p1.is_none() { + self.p1 = Some(event.id); + } else if self.p2.is_none() { + self.p2 = Some(event.id); + } + gilrs::EventType::Disconnected => if self.p1 == Some(event.id) { + self.p1 = None; + } else if self.p2 == Some(event.id) { + self.p2 = None; + } + _ => {} + } + } + + let dpi = self.context.window().get_hidpi_factor(); + const TARGET_ASPECT: f64 = 40.0 / 23.0; + let vp = if self.lsize.width / self.lsize.height < TARGET_ASPECT { + LogicalSize::new(self.lsize.width, self.lsize.width / TARGET_ASPECT) + } else { + LogicalSize::new(self.lsize.height * TARGET_ASPECT, self.lsize.height) + }; + self.res.text.dpi = (dpi * vp.width / 40.0) as f32; + + unsafe { + let (rw, rh): (u32, _) = self.lsize.to_physical(dpi).into(); + let (rw, rh) = (rw as i32, rh as i32); + let (w, h): (u32, _) = vp.to_physical(dpi).into(); + let (w, h) = (w as i32, h as i32); + + gl::Viewport((rw - w) / 2, (rh - h) / 2, w, h); + gl::ClearBufferfv(gl::COLOR, 0, [0.0f32; 4].as_ptr()); + } + + self.state.render(&mut self.res); + + self.res.text.render(); + + self.context.window().set_title( + &format!("Cold Clear (FPS: {:.0})", 1.0/smooth_delta) + ); + + self.context.swap_buffers().unwrap(); + } - move_sound: Option, - hard_drop: Option, - line_clear: Option + fn event(&mut self, event: WindowEvent, _: WindowId) -> GameloopCommand { + if let Some(new_state) = self.state.event(&mut self.res, &event) { + self.state = new_state; + } + match event { + WindowEvent::CloseRequested => return GameloopCommand::Exit, + WindowEvent::Resized(new_size) => { + self.lsize = new_size; + self.context.resize(new_size.to_physical( + self.context.window().get_hidpi_factor() + )); + } + WindowEvent::KeyboardInput { input, .. } => if let Some(k) = input.virtual_keycode { + if input.state == ElementState::Pressed { + self.keys.insert(k); + } else { + self.keys.remove(&k); + } + } + _ => {} + } + GameloopCommand::Continue + } } fn main() { @@ -48,69 +134,63 @@ fn main() { return } - let mut cb = ContextBuilder::new("cold-clear", "MinusKelvin"); - if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { - let mut path = std::path::PathBuf::from(manifest_dir); - path.push("resources"); - println!("Adding path {:?}", path); - cb = cb.add_resource_path(path); + let mut events = EventsLoop::new(); + + let (context, lsize) = game_util::create_context( + WindowBuilder::new() + .with_title("Cold Clear") + .with_dimensions((1280.0, 720.0).into()), + 0, true, &mut events + ); + + unsafe { + gl::Enable(gl::BLEND); + gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); } - let (mut ctx, mut events) = cb - .window_setup(ggez::conf::WindowSetup { - title: "Cold Clear".to_owned(), - ..Default::default() - }) - .window_mode(ggez::conf::WindowMode { - width: 1024.0, - height: 576.0, - resizable: true, - ..Default::default() - }) - .build().unwrap(); - - let mut resources = Resources { - sprites: SpriteBatch::new(Image::new(&mut ctx, "/sprites.png").unwrap()), - move_sound: audio::Source::new(&mut ctx, "/move.ogg").or_else(|e| { - eprintln!("Error loading sound effect for movement: {}", e); - Err(e) - }).ok(), - hard_drop: audio::Source::new(&mut ctx, "/hard-drop.ogg").or_else(|e| { - eprintln!("Error loading sound effect for hard drop: {}", e); - Err(e) - }).ok(), - line_clear: audio::Source::new(&mut ctx, "/line-clear.ogg").or_else(|e| { - eprintln!("Error loading sound effect for line clear: {}", e); - Err(e) - }).ok(), - }; + let Options { p1, p2 } = read_options().unwrap_or_else(|e| { + eprintln!("An error occured while loading options.yaml: {}", e); + Options::default() + }); + let p1_game_config = p1.game; + let p2_game_config = p2.game; - match replay_file { - Some(file) => { - let mut replay_game = ReplayGame::new(&mut resources, file); - event::run(&mut ctx, &mut events, &mut replay_game).unwrap(); - } - None => { - let Options { - p1_config, p2_config - } = match read_options() { - Ok(options) => options, - Err(e) => { - eprintln!("An error occured while loading options: {}", e); - Options::default() - } - }; - let p1_game_config = p1_config.game; - let p2_game_config = p2_config.game; - let mut local_game = LocalGame::new( - &mut resources, - Box::new(move |board| p1_config.to_player(board)), - Box::new(move |board| p2_config.to_player(board)), + let gilrs = Gilrs::new().unwrap(); + let mut gamepads = gilrs.gamepads(); + + let mut game = CCGui { + context: &context, + lsize, + res: res::Resources::load(), + state: match replay_file { + Some(f) => Box::new(ReplayGame::new(f)), + None => Box::new(RealtimeGame::new( + Box::new(move |board| p1.to_player(board)), + Box::new(move |board| p2.to_player(board)), p1_game_config, p2_game_config - ); - event::run(&mut ctx, &mut events, &mut local_game).unwrap(); - } - } + )) + }, + p1: gamepads.next().map(|(id, _)| id), + p2: gamepads.next().map(|(id, _)| id), + gilrs, + keys: HashSet::new() + }; + + game_util::gameloop(&mut events, &mut game, 60.0, true); +} + +trait State { + fn update( + &mut self, + res: &mut res::Resources, + keys: &HashSet, + p1: Option, + p2: Option + ) -> Option>; + fn render(&mut self, res: &mut res::Resources); + fn event( + &mut self, _res: &mut res::Resources, _event: &WindowEvent + ) -> Option> { None } } fn read_options() -> Result> { @@ -130,28 +210,29 @@ fn read_options() -> Result> { #[derive(Serialize, Deserialize, Clone, Debug)] struct Options { - #[serde(rename = "p1")] - p1_config: PlayerConfig, - #[serde(rename = "p2")] - p2_config: PlayerConfig + p1: PlayerConfig, + p2: PlayerConfig } + impl Default for Options { fn default() -> Self { - let mut p2_config = PlayerConfig::default(); - p2_config.is_bot = true; + let mut p2 = PlayerConfig::default(); + p2.is_bot = true; Options { - p1_config: PlayerConfig::default(), - p2_config + p1: PlayerConfig::default(), + p2 } } } + #[derive(Serialize, Deserialize, Clone, Debug, Default)] struct PlayerConfig { - controls: UserInput, + controls: input::UserInput, game: GameConfig, is_bot: bool, bot_config: BotConfig } + impl PlayerConfig { pub fn to_player(&self, board: libtetris::Board) -> (Box, String) { use cold_clear::evaluation::Evaluator; @@ -168,6 +249,7 @@ impl PlayerConfig { } } } + #[derive(Serialize, Deserialize, Clone, Debug, Default)] struct BotConfig { weights: cold_clear::evaluation::Standard, diff --git a/gui/src/player_draw.rs b/gui/src/player_draw.rs new file mode 100644 index 0000000..4450f12 --- /dev/null +++ b/gui/src/player_draw.rs @@ -0,0 +1,353 @@ +use game_util::prelude::*; +use game_util::Alignment; +use std::collections::VecDeque; +use libtetris::*; +use battle::{ PlayerUpdate, Event }; +use arrayvec::ArrayVec; +use crate::res::Resources; + +pub struct PlayerDrawState { + board: ArrayVec<[ColoredRow; 40]>, + state: State, + statistics: Statistics, + garbage_queue: u32, + dead: bool, + hold_piece: Option, + next_queue: VecDeque, + game_time: u32, + combo_splash: Option<(u32, u32)>, + back_to_back_splash: Option, + clear_splash: Option<(&'static str, u32)>, + name: String, + info: Option +} + +enum State { + Falling(FallingPiece, FallingPiece), + LineClearAnimation(ArrayVec<[i32; 4]>, i32), + Delay +} + +impl PlayerDrawState { + pub fn new(queue: impl IntoIterator, name: String) -> Self { + PlayerDrawState { + board: ArrayVec::from([*ColoredRow::EMPTY; 40]), + state: State::Delay, + statistics: Statistics::default(), + garbage_queue: 0, + dead: false, + hold_piece: None, + next_queue: queue.into_iter().collect(), + game_time: 0, + combo_splash: None, + back_to_back_splash: None, + clear_splash: None, + name, + info: None + } + } + + pub fn update( + &mut self, update: PlayerUpdate, info_update: Option, time: u32 + ) { + self.garbage_queue = update.garbage_queue; + self.info = info_update.or(self.info.take()); + self.game_time = time; + if let State::LineClearAnimation(_, ref mut frames) = self.state { + *frames += 1; + } + if let Some((_, timer)) = &mut self.combo_splash { + if *timer == 0 { + self.combo_splash = None; + } else { + *timer -= 1; + } + } + if let Some(timer) = &mut self.back_to_back_splash { + if *timer == 0 { + self.back_to_back_splash = None; + } else { + *timer -= 1; + } + } + if let Some((_, timer)) = &mut self.clear_splash { + if *timer == 0 { + self.clear_splash = None; + } else { + *timer -= 1; + } + } + for event in &update.events { + match event { + Event::PiecePlaced { piece, locked, .. } => { + self.statistics.update(&locked); + for &(x, y, _) in &piece.cells() { + self.board[y as usize].set(x as usize, piece.kind.0.color()); + } + if locked.cleared_lines.is_empty() { + self.state = State::Delay; + } else { + self.state = State::LineClearAnimation(locked.cleared_lines.clone(), 0); + } + if locked.b2b { + self.back_to_back_splash = Some(75); + } + let combo = locked.combo.unwrap_or(0); + if combo > 0 { + self.combo_splash = Some((combo, 75)); + } + if locked.perfect_clear { + self.clear_splash = Some(("Perfect Clear", 135)); + self.back_to_back_splash = None; + } else if locked.placement_kind.is_hard() { + self.clear_splash = Some((locked.placement_kind.name(), 75)); + } + } + Event::PieceHeld(piece) => { + self.hold_piece = Some(*piece); + self.state = State::Delay; + } + Event::PieceSpawned { new_in_queue } => { + self.next_queue.push_back(*new_in_queue); + self.next_queue.pop_front(); + } + Event::PieceFalling(piece, ghost) => { + self.state = State::Falling(*piece, *ghost); + } + Event::EndOfLineClearDelay => { + self.state = State::Delay; + self.board.retain(|row| !row.is_full()); + while !self.board.is_full() { + self.board.push(*ColoredRow::EMPTY); + } + } + Event::GarbageAdded(columns) => { + self.board.truncate(40 - columns.len()); + for &col in columns { + let mut row = *ColoredRow::EMPTY; + for x in 0..10 { + if x != col { + row.set(x, CellColor::Garbage); + } + } + self.board.insert(0, row); + } + } + Event::GameOver => self.dead = true, + _ => {} + } + } + } + + pub fn draw(&self, res: &mut Resources, offset_x: f32) { + // Draw the playfield + for y in 0..21 { + for x in 0..10 { + let cell_color = self.board[y].cell_color(x); + res.sprite_batch.draw( + if cell_color == CellColor::Empty { + &res.sprites.blank + } else { + &res.sprites.filled + }, + point2(offset_x + x as f32 + 4.0, y as f32 + 3.25), + cell_color_to_color(if self.dead && cell_color != CellColor::Empty { + CellColor::Unclearable + } else { + cell_color + }) + ); + } + } + + // Draw either the falling piece or the line clear animation + match self.state { + State::Falling(piece, ghost) => { + for &(x,y,_) in &ghost.cells() { + res.sprite_batch.draw( + &res.sprites.ghost, + point2(offset_x + x as f32 + 4.0, y as f32 + 3.25), + cell_color_to_color(piece.kind.0.color()) + ); + } + for &(x,y,_) in &piece.cells() { + res.sprite_batch.draw( + &res.sprites.filled, + point2(offset_x + x as f32 + 4.0, y as f32 + 3.25), + cell_color_to_color(piece.kind.0.color()) + ); + } + } + State::LineClearAnimation(ref lines, frame) => { + let frame = (frame as usize).min(res.sprites.line_clear.len() - 1); + for &y in lines { + res.sprite_batch.draw( + &res.sprites.line_clear[frame], + point2(offset_x + 8.5, y as f32 + 3.25), + [0xFF; 4] + ); + } + } + _ => {} + } + + // Draw pending garbage bar + for y in 0..self.garbage_queue { + let w = res.sprites.garbage_bar.real_size.width / res.sprite_batch.pixels_per_unit; + res.sprite_batch.draw( + &res.sprites.garbage_bar, + point2(offset_x + 13.5 + w / 2.0, y as f32 + 3.25), + [0xFF; 4] + ); + } + + // Draw hold piece and next queue + res.text.draw_text("Hold", offset_x + 2.0, 21.85, Alignment::Center, [0xFF; 4], 0.7, 0); + if let Some(piece) = self.hold_piece { + res.sprite_batch.draw( + &res.sprites.piece[piece as usize], + point2(offset_x + 2.0, 20.75), + cell_color_to_color(piece.color()) + ); + } + res.text.draw_text("Next", offset_x + 15.0, 21.85, Alignment::Center, [0xFF; 4], 0.7, 0); + for (i, &piece) in self.next_queue.iter().enumerate() { + res.sprite_batch.draw( + &res.sprites.piece[piece as usize], + point2(offset_x + 15.0, 20.75 - 2.0 * i as f32), + cell_color_to_color(piece.color()) + ); + } + + // Draw statistics + res.text.draw_text( + "Statistics", offset_x + 1.75, 19.1, Alignment::Center, [0xFF; 4], 0.6, 0 + ); + let seconds = self.game_time as f32 / 60.0; + let mut lines = vec![ + ("Pieces", format!("{}", self.statistics.pieces)), + ("PPS", format!("{:.1}", self.statistics.pieces as f32 / seconds)), + ("Lines", format!("{}", self.statistics.lines)), + ("Attack", format!("{}", self.statistics.attack)), + ("APM", format!("{:.1}", self.statistics.attack as f32 / seconds * 60.0)), + ("APP", format!("{:.3}", self.statistics.attack as f32 / self.statistics.pieces as f32)), + ("Max Ren", format!("{}", self.statistics.max_combo)), + ("Single", format!("{}", self.statistics.singles)), + ("Double", format!("{}", self.statistics.doubles)), + ("Triple", format!("{}", self.statistics.triples)), + ("Tetris", format!("{}", self.statistics.tetrises)), + // ("Mini T0", format!("{}", self.statistics.mini_tspin_zeros)), + // ("Mini T1", format!("{}", self.statistics.mini_tspin_singles)), + // ("Mini T2", format!("{}", self.statistics.mini_tspin_doubles)), + ("T-Spin 0", format!("{}", self.statistics.tspin_zeros)), + ("T-Spin 1", format!("{}", self.statistics.tspin_singles)), + ("T-Spin 2", format!("{}", self.statistics.tspin_doubles)), + ("T-Spin 3", format!("{}", self.statistics.tspin_triples)), + ("Perfect", format!("{}", self.statistics.perfect_clears)) + ]; + if let Some(ref info) = self.info { + // Bot info + lines.push(("", "".to_owned())); + lines.push(("Depth", format!("{}", info.depth))); + lines.push(("Nodes", format!("{}", info.nodes))); + lines.push(("O. Rank", format!("{}", info.original_rank))); + } + let mut labels = String::new(); + let mut values = String::new(); + for (label, value) in lines { + labels.push_str(label); + labels.push('\n'); + values.push_str(&value); + values.push('\n'); + } + res.text.draw_text(&labels, offset_x+0.2, 18.4, Alignment::Left, [0xFF; 4], 0.45, 0); + res.text.draw_text(&values, offset_x+3.3, 18.4, Alignment::Right, [0xFF; 4], 0.45, 0); + + if let Some(ref info) = self.info { + let mut has_pc = false; + for (_, l) in &info.plan { + if l.perfect_clear { + has_pc = true; + } + } + + // Draw bot plan + let mut y_map = [0; 40]; + for i in 0..40 { + y_map[i] = i as i32; + } + for (placement, lock) in &info.plan { + for &(x, y, d) in &placement.cells() { + res.sprite_batch.draw( + &res.sprites.plan[d.to_bits() as usize], + point2(offset_x + x as f32 + 4.0, y_map[y as usize] as f32 + 3.25), + cell_color_to_color(placement.kind.0.color()) + ); + } + let mut new_map = [0; 40]; + let mut j = 0; + for i in 0..40 { + if !lock.cleared_lines.contains(&i) { + new_map[j] = y_map[i as usize]; + j += 1; + } + } + y_map = new_map; + + if !has_pc && lock.placement_kind.is_hard() && lock.placement_kind.is_clear() + || lock.perfect_clear { + break + } + } + } + + // Draw player name + res.text.draw_text( + &self.name, + offset_x + 15.0, 21.0 - 2.0 * self.next_queue.len() as f32, + Alignment::Center, + [0xFF; 4], 0.45, 0 + ); + + // Draw clear info stuff + if let Some(timer) = self.back_to_back_splash { + res.text.draw_text( + "Back-To-Back", + offset_x + 4.0, 1.65, + Alignment::Left, + [0xFF, 0xFF, 0xFF, (timer.min(15) * 0xFF / 15) as u8], 0.75, 0 + ); + } + if let Some((combo, timer)) = self.combo_splash { + res.text.draw_text( + &format!("{} Combo", combo), + offset_x + 13.0, 1.65, + Alignment::Right, + [0xFF, 0xFF, 0xFF, (timer.min(15) * 0xFF / 15) as u8], 0.75, 0 + ); + } + if let Some((txt, timer)) = self.clear_splash { + res.text.draw_text( + txt, + offset_x + 8.5, 0.65, + Alignment::Center, + [0xFF, 0xFF, 0xFF, (timer.min(15) * 0xFF / 15) as u8], 0.75, 0 + ); + } + } +} + +fn cell_color_to_color(cell_color: CellColor) -> [u8; 4] { + match cell_color { + CellColor::Empty => [0xFF, 0xFF, 0xFF, 0xFF], + CellColor::Garbage => [160, 160, 160, 0xFF], + CellColor::Unclearable => [64, 64, 64, 0xFF], + CellColor::Z => [255, 32, 32, 0xFF], + CellColor::S => [32, 255, 32, 0xFF], + CellColor::O => [255, 255, 32, 0xFF], + CellColor::L => [255, 143, 32, 0xFF], + CellColor::J => [96, 96, 255, 0xFF], + CellColor::I => [32, 255, 255, 0xFF], + CellColor::T => [143, 32, 255, 0xFF] + } +} \ No newline at end of file diff --git a/gui/src/realtime.rs b/gui/src/realtime.rs new file mode 100644 index 0000000..9f4b1b3 --- /dev/null +++ b/gui/src/realtime.rs @@ -0,0 +1,200 @@ +use game_util::glutin::VirtualKeyCode; +use battle::{ Battle, GameConfig }; +use libtetris::Board; +use std::collections::{ HashSet, VecDeque }; +use rand::prelude::*; +use gilrs::Gamepad; +use crate::res::Resources; +use crate::battle_ui::BattleUi; +use crate::input::InputSource; +use crate::replay::InfoReplay; + +type InputFactory = dyn Fn(Board) -> (Box, String); + +pub struct RealtimeGame { + ui: BattleUi, + battle: Battle, + p1_input_factory: Box, + p2_input_factory: Box, + p1_input: Box, + p2_input: Box, + p1_wins: u32, + p2_wins: u32, + p1_info_updates: VecDeque>, + p2_info_updates: VecDeque>, + state: State, + p1_config: GameConfig, + p2_config: GameConfig, +} + +enum State { + Playing, + GameOver(u32), + Starting(u32) +} + +impl RealtimeGame { + pub fn new( + p1: Box, + p2: Box, + p1_config: GameConfig, + p2_config: GameConfig + ) -> Self { + let mut battle = Battle::new( + p1_config, p2_config, thread_rng().gen(), thread_rng().gen(), thread_rng().gen() + ); + let (p1_input, p1_name) = p1(battle.player_1.board.to_compressed()); + let (p2_input, p2_name) = p2(battle.player_2.board.to_compressed()); + battle.replay.p1_name = p1_name.clone(); + battle.replay.p2_name = p2_name.clone(); + RealtimeGame { + ui: BattleUi::new(&battle, p1_name, p2_name), + battle, + p1_input_factory: p1, + p2_input_factory: p2, + p1_input, p2_input, + p1_wins: 0, + p2_wins: 0, + p1_info_updates: VecDeque::new(), + p2_info_updates: VecDeque::new(), + state: State::Starting(180), + p1_config, p2_config + } + } +} + +impl crate::State for RealtimeGame { + fn update( + &mut self, + res: &mut Resources, + keys: &HashSet, + p1: Option, + p2: Option + ) -> Option> { + let do_update = match self.state { + State::GameOver(0) => { + let mut encoder = libflate::deflate::Encoder::new( + std::fs::File::create("replay.dat" + ).unwrap()); + bincode::serialize_into( + &mut encoder, + &InfoReplay { + replay: self.battle.replay.clone(), + p1_info_updates: self.p1_info_updates.clone(), + p2_info_updates: self.p2_info_updates.clone() + } + ).unwrap(); + encoder.finish().unwrap(); + + self.battle = Battle::new( + self.p1_config, self.p2_config, + thread_rng().gen(), thread_rng().gen(), thread_rng().gen() + ); + + let (p1_input, p1_name) = (self.p1_input_factory)( + self.battle.player_1.board.to_compressed() + ); + let (p2_input, p2_name) = (self.p2_input_factory)( + self.battle.player_2.board.to_compressed() + ); + + self.ui = BattleUi::new(&self.battle, p1_name.clone(), p2_name.clone()); + self.p1_input = p1_input; + self.p2_input = p2_input; + self.battle.replay.p1_name = p1_name; + self.battle.replay.p2_name = p2_name; + + self.p1_info_updates.clear(); + self.p2_info_updates.clear(); + + self.state = State::Starting(180); + false + } + State::GameOver(ref mut delay) => { + *delay -= 1; + true + } + State::Starting(0) => { + self.state = State::Playing; + true + } + State::Starting(ref mut delay) => { + *delay -= 1; + false + } + State::Playing => true + }; + + if do_update { + let p1_controller = self.p1_input.controller(keys, p1); + let p2_controller = self.p2_input.controller(keys, p2.or(p1)); + + let update = self.battle.update(p1_controller, p2_controller); + + let p1_info_update = self.p1_input.update( + &self.battle.player_1.board, &update.player_1.events, + self.battle.player_1.garbage_queue + ); + let p2_info_update = self.p2_input.update( + &self.battle.player_2.board, &update.player_2.events, + self.battle.player_2.garbage_queue + ); + + self.p1_info_updates.push_back(p1_info_update.clone()); + self.p2_info_updates.push_back(p2_info_update.clone()); + + if let State::Playing = self.state { + for event in &update.player_1.events { + use battle::Event::*; + match event { + GameOver => { + self.p2_wins += 1; + self.state = State::GameOver(300); + } + _ => {} + } + } + for event in &update.player_2.events { + use battle::Event::*; + match event { + GameOver => { + self.p1_wins += 1; + self.state = State::GameOver(300); + } + _ => {} + } + } + } + + self.ui.update(res, update, p1_info_update, p2_info_update); + } + + None + } + + fn render(&mut self, res: &mut Resources) { + res.text.draw_text( + &format!("{} - {}", self.p1_wins, self.p2_wins), + 20.0, 3.0, + game_util::Alignment::Center, + [0xFF; 4], 1.5, 0 + ); + + if let State::Starting(timer) = self.state { + res.text.draw_text( + &format!("{}", timer / 60 + 1), + 9.5, 12.25, + game_util::Alignment::Center, + [0xFF; 4], 3.0, 0 + ); + res.text.draw_text( + &format!("{}", timer / 60 + 1), + 29.5, 12.25, + game_util::Alignment::Center, + [0xFF; 4], 3.0, 0 + ); + } + + self.ui.draw(res); + } +} \ No newline at end of file diff --git a/gui/src/replay.rs b/gui/src/replay.rs index f6f6e97..977bb2c 100644 --- a/gui/src/replay.rs +++ b/gui/src/replay.rs @@ -1,32 +1,32 @@ -use ggez::event::EventHandler; -use ggez::{ Context, GameResult }; -use ggez::timer; -use ggez::graphics; -use crate::interface::{ Gui, setup_graphics, text }; -use crate::Resources; -use battle::{ Battle, Replay }; -use libtetris::Controller; -use std::collections::VecDeque; +use battle::Replay; use serde::{ Serialize, Deserialize }; -use libflate::deflate; +use std::collections::{ HashSet, VecDeque }; +use std::path::PathBuf; +use std::fs::File; +use battle::Battle; +use libtetris::Controller; +use gilrs::Gamepad; +use game_util::glutin::VirtualKeyCode; +use crate::battle_ui::BattleUi; +use crate::res::Resources; -pub struct ReplayGame<'a, P> { - gui: Gui, +pub struct ReplayGame { + ui: BattleUi, battle: Battle, + file: PathBuf, updates: VecDeque<(Controller, Controller)>, p1_info_updates: VecDeque>, p2_info_updates: VecDeque>, - start_delay: u32, - resources: &'a mut Resources, - file: P + start_delay: u32 } -impl<'a, P: AsRef + Clone> ReplayGame<'a, P> { - pub fn new(resources: &'a mut Resources, file: P) -> Self { +impl ReplayGame { + pub fn new(file: impl Into) -> Self { + let file = file.into(); let InfoReplay { replay, p1_info_updates, p2_info_updates } = bincode::deserialize_from( - deflate::Decoder::new(std::fs::File::open(file.clone()).unwrap()) + libflate::deflate::Decoder::new(File::open(&file).unwrap()) ).unwrap(); let battle = Battle::new( replay.p1_config, replay.p2_config, @@ -34,82 +34,83 @@ impl<'a, P: AsRef + Clone> ReplayGame<'a, P> { replay.garbage_seed ); ReplayGame { - gui: Gui::new(&battle, replay.p1_name, replay.p2_name), + ui: BattleUi::new(&battle, replay.p1_name, replay.p2_name), battle, updates: replay.updates, - p1_info_updates: p1_info_updates, - p2_info_updates: p2_info_updates, + p1_info_updates, p2_info_updates, start_delay: 500, - resources, file } } } -impl + Clone> EventHandler for ReplayGame<'_, P> { - fn update(&mut self, ctx: &mut Context) -> GameResult { - while timer::check_update_time(ctx, 60) { - if self.start_delay == 0 { - if let Some( - (p1_controller, p2_controller) - ) = self.updates.pop_front() { - let update = self.battle.update(p1_controller, p2_controller); - self.gui.update( - update, - self.p1_info_updates.pop_front().and_then(|x| x), - self.p2_info_updates.pop_front().and_then(|x| x), - self.resources - )?; - } else { - let replay; - loop { - match std::fs::File::open(self.file.clone()) { - Ok(f) => { - match bincode::deserialize_from(deflate::Decoder::new(f)) { - Ok(r) => { - replay = r; - break - } - Err(_) => {} +impl crate::State for ReplayGame { + fn update( + &mut self, + res: &mut Resources, + _keys: &HashSet, + _p1: Option, + _p2: Option + ) -> Option> { + if self.start_delay == 0 { + if let Some((p1_controller, p2_controller)) = self.updates.pop_front() { + let update = self.battle.update(p1_controller, p2_controller); + self.ui.update( + res, update, + self.p1_info_updates.pop_front().flatten(), + self.p2_info_updates.pop_front().flatten() + ); + } else { + let replay; + loop { + match std::fs::File::open(&self.file) { + Ok(f) => { + match bincode::deserialize_from(libflate::deflate::Decoder::new(f)) { + Ok(r) => { + replay = r; + break } + Err(_) => {} } - Err(_) => {} } + Err(_) => {} } - let InfoReplay { replay, p1_info_updates, p2_info_updates } = replay; - let battle = Battle::new( - replay.p1_config, replay.p2_config, - replay.p1_seed, replay.p2_seed, - replay.garbage_seed - ); - self.gui = Gui::new(&battle, replay.p1_name, replay.p2_name); - self.battle = battle; - self.updates = replay.updates; - self.p1_info_updates = p1_info_updates; - self.p2_info_updates = p2_info_updates; - self.start_delay = 180; } - } else { - if self.start_delay == 180 { - while timer::check_update_time(ctx, 60) {} - } - self.start_delay -= 1; + let InfoReplay { replay, p1_info_updates, p2_info_updates } = replay; + let battle = Battle::new( + replay.p1_config, replay.p2_config, + replay.p1_seed, replay.p2_seed, + replay.garbage_seed + ); + self.ui = BattleUi::new(&battle, replay.p1_name, replay.p2_name); + self.battle = battle; + self.updates = replay.updates; + self.p1_info_updates = p1_info_updates; + self.p2_info_updates = p2_info_updates; + self.start_delay = 180; } + } else { + self.start_delay -= 1; } - Ok(()) + None } - fn draw(&mut self, ctx: &mut Context) -> GameResult { - let (scale, center) = setup_graphics(ctx)?; - + fn render(&mut self, res: &mut Resources) { if self.start_delay != 0 { - let txt = text(format!("{}", self.start_delay / 60 + 1), scale * 4.0, 10.0*scale); - graphics::queue_text(ctx, &txt, [center-14.5*scale, 9.0*scale], None); - graphics::queue_text(ctx, &txt, [center+4.5*scale, 9.0*scale], None); + res.text.draw_text( + &format!("{}", self.start_delay / 60 + 1), + 9.5, 12.25, + game_util::Alignment::Center, + [0xFF; 4], 3.0, 0 + ); + res.text.draw_text( + &format!("{}", self.start_delay / 60 + 1), + 29.5, 12.25, + game_util::Alignment::Center, + [0xFF; 4], 3.0, 0 + ); } - - self.gui.draw(ctx, self.resources, scale, center)?; - graphics::present(ctx) + self.ui.draw(res); } } diff --git a/gui/src/res.rs b/gui/src/res.rs new file mode 100644 index 0000000..403a513 --- /dev/null +++ b/gui/src/res.rs @@ -0,0 +1,37 @@ +use game_util::{ TextRenderer, SpriteBatch, sprite_shader, Sound }; +use game_util::rusttype::Font; +use game_util::rodio::{ self, Sink }; + +pub struct Resources { + pub text: TextRenderer, + pub sprites: sprites::Sprites, + pub sprite_batch: SpriteBatch, + pub line_clear: Sound, + pub move_sound: Sound, + pub hard_drop: Sound, + pub move_sound_sink: Sink +} + +impl Resources { + pub fn load() -> Self { + let mut text = TextRenderer::new(); + text.screen_size = (40.0, 23.0); + text.add_style(vec![ + Font::from_bytes(include_bytes!("font/NotoSerif-Regular.ttf") as &[_]).unwrap() + ]); + + let (sprites, sprite_sheet) = sprites::Sprites::load(); + let mut sprite_batch = SpriteBatch::new(sprite_shader(), sprite_sheet); + sprite_batch.pixels_per_unit = 83.0; + + Resources { + text, sprites, sprite_batch, + line_clear: Sound::new(include_bytes!("sounds/line-clear.ogg") as &[_]), + move_sound: Sound::new(include_bytes!("sounds/move.ogg") as &[_]), + hard_drop: Sound::new(include_bytes!("sounds/hard-drop.ogg") as &[_]), + move_sound_sink: Sink::new(&rodio::default_output_device().unwrap()) + } + } +} + +include!(concat!(env!("OUT_DIR"), "/sprites.rs")); \ No newline at end of file diff --git a/gui/resources/hard-drop.ogg b/gui/src/sounds/hard-drop.ogg similarity index 100% rename from gui/resources/hard-drop.ogg rename to gui/src/sounds/hard-drop.ogg diff --git a/gui/resources/line-clear.ogg b/gui/src/sounds/line-clear.ogg similarity index 100% rename from gui/resources/line-clear.ogg rename to gui/src/sounds/line-clear.ogg diff --git a/gui/resources/move.ogg b/gui/src/sounds/move.ogg similarity index 100% rename from gui/resources/move.ogg rename to gui/src/sounds/move.ogg