diff --git a/Cargo.lock b/Cargo.lock index 1eabbd5..bb89b2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "arc-swap" version = "0.4.5" @@ -53,6 +61,35 @@ dependencies = [ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "clap" +version = "3.0.0-beta.1" +source = "git+https://github.com/clap-rs/clap/#37889c661134e8286102f7d2ab3267965d010403" +dependencies = [ + "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap_derive 3.0.0-beta.1 (git+https://github.com/clap-rs/clap/)", + "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.1" +source = "git+https://github.com/clap-rs/clap/#37889c661134e8286102f7d2ab3267965d010403" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "colored" version = "1.9.3" @@ -97,6 +134,14 @@ dependencies = [ "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hermit-abi" version = "0.1.8" @@ -105,6 +150,14 @@ dependencies = [ "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.4" @@ -250,6 +303,30 @@ name = "ppv-lite86" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "1.0.9" @@ -365,6 +442,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "1.0.16" @@ -375,10 +457,21 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tetris" version = "0.1.0" dependencies = [ + "clap 3.0.0-beta.1 (git+https://github.com/clap-rs/clap/)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "sdl2 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -386,6 +479,14 @@ dependencies = [ "tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.42" @@ -429,11 +530,31 @@ dependencies = [ "syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "wasi" version = "0.7.0" @@ -478,6 +599,7 @@ dependencies = [ ] [metadata] +"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" "checksum arc-swap 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" @@ -486,13 +608,17 @@ dependencies = [ "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +"checksum clap 3.0.0-beta.1 (git+https://github.com/clap-rs/clap/)" = "" +"checksum clap_derive 3.0.0-beta.1 (git+https://github.com/clap-rs/clap/)" = "" "checksum colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" "checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -510,6 +636,8 @@ dependencies = [ "checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" "checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +"checksum proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +"checksum proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" "checksum proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" @@ -523,11 +651,18 @@ dependencies = [ "checksum simple_logger 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fea0c4611f32f4c2bac73754f22dca1f57e6c1945e0590dae4e5f2a077b92367" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" +"checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" "checksum syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" +"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tokio 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "0fa5e81d6bc4e67fe889d5783bd2a128ab2e0cfa487e0be16b6a8d177b101616" "checksum tokio-macros 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" diff --git a/Cargo.toml b/Cargo.toml index d499e36..e18140c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ rand = "0.7" tokio = { version = "0.2", features = ["full"] } log = "0.4" simple_logger = "1.6" -sdl2 = { version = "0.33.0", features = ["ttf"] } \ No newline at end of file +sdl2 = { version = "0.33.0", features = ["ttf"] } +clap = { git = "https://github.com/clap-rs/clap/" } \ No newline at end of file diff --git a/src/actors/mod.rs b/src/actors/mod.rs new file mode 100644 index 0000000..81ff3a2 --- /dev/null +++ b/src/actors/mod.rs @@ -0,0 +1,44 @@ +use crate::game::{Action, Game}; +use crate::playfield::{Matrix, PlayField}; +use crate::tetromino::{Tetromino, TetrominoType}; +use rand::RngCore; + +pub mod qlearning; + +#[derive(Hash, PartialEq, Eq, Clone)] +pub struct State { + matrix: Matrix, + active_piece: Option, + held_piece: Option, +} + +impl From for State { + fn from(game: Game) -> Self { + (&game).into() + } +} + +impl From<&Game> for State { + fn from(game: &Game) -> Self { + game.playfield().clone().into() + } +} + +impl From for State { + fn from(playfield: PlayField) -> Self { + Self { + matrix: playfield.field().clone(), + active_piece: playfield.active_piece, + held_piece: playfield.hold_piece().map(|t| t.clone()), + } + } +} + +pub trait Actor { + fn get_action( + &self, + rng: &mut T, + state: &State, + legal_actions: &[Action], + ) -> Action; +} diff --git a/src/actors/qlearning.rs b/src/actors/qlearning.rs new file mode 100644 index 0000000..bdcfebb --- /dev/null +++ b/src/actors/qlearning.rs @@ -0,0 +1,86 @@ +use crate::actors::{Actor, State}; +use crate::game::Action; +use rand::seq::SliceRandom; +use rand::Rng; +use rand::RngCore; +use std::collections::HashMap; +pub struct QLearningAgent { + pub learning_rate: f64, + pub exploration_prob: f64, + discount_rate: f64, + q_values: HashMap>, +} + +impl Default for QLearningAgent { + fn default() -> Self { + QLearningAgent { + learning_rate: 0.1, + exploration_prob: 0.5, + discount_rate: 1.0, + q_values: HashMap::default(), + } + } +} + +impl QLearningAgent { + fn get_q_value(&self, state: &State, action: Action) -> f64 { + match self.q_values.get(&state) { + Some(action_qval) => *action_qval.get(&action).unwrap_or_else(|| &0.0), + None => 0.0, + } + } + + fn get_action_from_q_values(&self, state: &State, legal_actions: &[Action]) -> Action { + *legal_actions + .iter() + .map(|action| (action, self.get_q_value(&state, *action))) + .max_by_key(|(_, q1)| ((q1 * 1_000_000.0) as isize)) + .expect("Failed to select an action") + .0 + } + + fn get_value_from_q_values(&self, state: &State) -> f64 { + *self + .q_values + .get(state) + .and_then(|hashmap| { + hashmap + .values() + .max_by_key(|q_val| (**q_val * 1_000_000.0) as isize) + .or_else(|| Some(&0.0)) + }) + .unwrap_or_else(|| &0.0) + } + + pub fn update(&mut self, state: State, action: Action, next_state: State, reward: f64) { + let cur_q_val = self.get_q_value(&state, action); + let new_q_val = cur_q_val + + self.learning_rate + * (reward + self.discount_rate * self.get_value_from_q_values(&next_state) + - cur_q_val); + if !self.q_values.contains_key(&state) { + self.q_values.insert(state.clone(), HashMap::default()); + } + self.q_values + .get_mut(&state) + .unwrap() + .insert(action, new_q_val); + } +} + +impl Actor for QLearningAgent { + // Because doing (Nothing) is in the set of legal actions, this will never + // be empty + fn get_action( + &self, + rng: &mut T, + state: &State, + legal_actions: &[Action], + ) -> Action { + if rng.gen::() < self.exploration_prob { + *legal_actions.choose(rng).unwrap() + } else { + self.get_action_from_q_values(state, legal_actions) + } + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..fff787e --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,4 @@ +use clap::Clap; + +#[derive(Clap)] +pub struct Ops {} diff --git a/src/game.rs b/src/game.rs index a1a25bc..f3d1970 100644 --- a/src/game.rs +++ b/src/game.rs @@ -5,10 +5,8 @@ use crate::playfield::PlayField; use crate::srs::RotationSystem; use crate::srs::SRS; use crate::tetromino::Position; -use crate::Renderable; use crate::TICKS_PER_SECOND; use log::{error, info, trace}; -use sdl2::{render::Canvas, video::Window}; use std::fmt; // I think this was correct, can't find source @@ -34,6 +32,7 @@ pub struct Game { /// The last clear action performed, used for determining if a back-to-back /// bonus is needed. last_clear_action: ClearAction, + line_clears: u32, } impl fmt::Debug for Game { @@ -57,6 +56,7 @@ impl Default for Game { next_spawn_tick: 0, is_game_over: None, last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to + line_clears: 0, } } } @@ -113,6 +113,9 @@ enum ClearAction { } impl Game { + pub fn score(&self) -> u32 { + self.score + } pub fn is_game_over(&self) -> Option { self.is_game_over.or_else(|| { self.playfield @@ -164,15 +167,18 @@ impl Game { let positions = self.playfield.lock_active_piece(); self.is_game_over = self.is_game_over.or_else(|| { if positions.iter().map(|p| p.y).all(|y| y < 20) { - println!("{:?}", positions); - Some(LossReason::LockOut) + trace!("Loss due to topout! {:?}", positions); + Some(LossReason::TopOut) } else { None } }); - if self.clear_lines() > 0 { - println!("Lines were cleared."); + let cleared_lines = self.clear_lines(); + if cleared_lines > 0 { + trace!("Lines were cleared."); + self.line_clears += cleared_lines as u32; + self.level = (self.line_clears / 10) as u8; self.playfield.active_piece = None; self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY; } else { @@ -184,11 +190,9 @@ impl Game { false } } -} -impl Renderable for Game { - fn render(&self, canvas: &mut Canvas) -> Result<(), String> { - self.playfield.render(canvas) + pub fn playfield(&self) -> &PlayField { + &self.playfield } } @@ -200,6 +204,19 @@ pub trait Controllable { fn rotate_right(&mut self); fn hard_drop(&mut self); fn hold(&mut self); + fn get_legal_actions(&self) -> Vec; +} + +#[derive(Hash, Eq, PartialEq, Debug, Copy, Clone)] +pub enum Action { + Nothing, // Default value + MoveLeft, + MoveRight, + SoftDrop, + HardDrop, + Hold, + RotateLeft, + RotateRight, } impl Controllable for Game { @@ -303,4 +320,21 @@ impl Controllable for Game { let _ = self.playfield.try_swap_hold(); } + + fn get_legal_actions(&self) -> Vec { + let mut legal_actions = vec![ + Action::MoveLeft, + Action::MoveRight, + Action::SoftDrop, + Action::HardDrop, + Action::RotateLeft, + Action::RotateRight, + ]; + + if self.playfield.can_swap() { + legal_actions.push(Action::Hold); + } + + legal_actions + } } diff --git a/src/graphics.rs b/src/graphics.rs deleted file mode 100644 index 61fa19e..0000000 --- a/src/graphics.rs +++ /dev/null @@ -1,14 +0,0 @@ -use sdl2::pixels::Color; - -pub const CELL_SIZE: u32 = 32; -pub const BORDER_RADIUS: u32 = 1; - -pub static COLOR_BACKGROUND: Color = Color::RGB(60, 60, 60); -pub static COLOR_CYAN: Color = Color::RGB(0, 255, 255); -pub static COLOR_YELLOW: Color = Color::RGB(255, 255, 0); -pub static COLOR_PURPLE: Color = Color::RGB(255, 0, 255); -pub static COLOR_GREEN: Color = Color::RGB(0, 255, 0); -pub static COLOR_RED: Color = Color::RGB(255, 0, 0); -pub static COLOR_BLUE: Color = Color::RGB(0, 0, 255); -pub static COLOR_ORANGE: Color = Color::RGB(255, 127, 0); -pub static COLOR_GRAY: Color = Color::RGB(100, 100, 100); diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs new file mode 100644 index 0000000..d510530 --- /dev/null +++ b/src/graphics/mod.rs @@ -0,0 +1,86 @@ +use crate::tetromino::TetrominoType; +use sdl2::{ + pixels::Color, + render::{Canvas, RenderTarget}, +}; +use std::fmt; + +pub mod standard_renderer; + +pub const CELL_SIZE: u32 = 32; +pub const BORDER_RADIUS: u32 = 1; +pub const UI_PADDING: u32 = 8; + +pub static COLOR_BACKGROUND: Color = Color::RGB(60, 60, 60); +pub static COLOR_CYAN: Color = Color::RGB(0, 255, 255); +pub static COLOR_YELLOW: Color = Color::RGB(255, 255, 0); +pub static COLOR_PURPLE: Color = Color::RGB(255, 0, 255); +pub static COLOR_GREEN: Color = Color::RGB(0, 255, 0); +pub static COLOR_RED: Color = Color::RGB(255, 0, 0); +pub static COLOR_BLUE: Color = Color::RGB(0, 0, 255); +pub static COLOR_ORANGE: Color = Color::RGB(255, 127, 0); +pub static COLOR_GRAY: Color = Color::RGB(100, 100, 100); + +pub trait Renderable { + fn width(&self) -> usize; + fn height(&self) -> usize; + fn render(&self, canvas: &mut Canvas) -> Result<(), String>; +} + +#[derive(Copy, Clone)] +pub enum MinoColor { + Cyan, + Yellow, + Purple, + Green, + Red, + Blue, + Orange, + Gray, +} + +impl fmt::Debug for MinoColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Cyan => "c", + Self::Yellow => "y", + Self::Purple => "p", + Self::Green => "g", + Self::Red => "r", + Self::Blue => "b", + Self::Orange => "o", + Self::Gray => "x", + } + ) + } +} + +impl Into for MinoColor { + fn into(self) -> Color { + match self { + Self::Cyan => COLOR_CYAN, + Self::Yellow => COLOR_YELLOW, + Self::Purple => COLOR_PURPLE, + Self::Green => COLOR_GREEN, + Self::Red => COLOR_RED, + Self::Blue => COLOR_BLUE, + Self::Orange => COLOR_ORANGE, + Self::Gray => COLOR_GRAY, + } + } +} + +impl Into for TetrominoType { + fn into(self) -> Color { + Into::::into(self).into() + } +} + +impl Into for &TetrominoType { + fn into(self) -> Color { + Into::::into(*self).into() + } +} diff --git a/src/graphics/standard_renderer.rs b/src/graphics/standard_renderer.rs new file mode 100644 index 0000000..fa8122d --- /dev/null +++ b/src/graphics/standard_renderer.rs @@ -0,0 +1,135 @@ +use crate::{ + game::Game, + graphics::{MinoColor, Renderable, BORDER_RADIUS, CELL_SIZE, COLOR_BACKGROUND, UI_PADDING}, + playfield::{PlayField, PLAYFIELD_HEIGHT, PLAYFIELD_WIDTH}, + tetromino::{Position, RotationState, Tetromino, TetrominoType}, +}; +use sdl2::{ + pixels::Color, + rect::Rect, + render::{Canvas, RenderTarget}, + video::Window, +}; + +pub fn render(canvas: &mut Canvas, game: &Game) { + let (x, y) = canvas.window().size(); + let game_width = game.width() as i32; + let game_height = game.height() as i32; + canvas.set_viewport(Some(Rect::new( + x as i32 / 2 - game_width / 2 as i32, + y as i32 / 2 - game_height / 2 as i32, + game_width as u32, + game_height as u32, + ))); + game.render(canvas); +} + +impl Renderable for Game { + fn width(&self) -> usize { + self.playfield().width() + } + + fn height(&self) -> usize { + self.playfield().height() + } + + fn render(&self, canvas: &mut Canvas) -> Result<(), String> { + self.playfield().render(canvas) + } +} + +impl Renderable for PlayField { + fn width(&self) -> usize { + (CELL_SIZE as usize) * PLAYFIELD_WIDTH + } + + fn height(&self) -> usize { + (CELL_SIZE as usize) * PLAYFIELD_HEIGHT + } + + fn render(&self, canvas: &mut Canvas) -> Result<(), String> { + for y in 0..PLAYFIELD_HEIGHT { + for x in 0..PLAYFIELD_WIDTH { + canvas.set_draw_color(Color::RGB(0, 0, 0)); + canvas.fill_rect(Rect::new( + CELL_SIZE as i32 * x as i32, + CELL_SIZE as i32 * y as i32, + CELL_SIZE, + CELL_SIZE, + ))?; + + match self.field()[y + PLAYFIELD_HEIGHT][x] { + Some(mino) => canvas.set_draw_color(mino), + None => canvas.set_draw_color(COLOR_BACKGROUND), + } + canvas.fill_rect(Rect::new( + CELL_SIZE as i32 * x as i32 + BORDER_RADIUS as i32, + CELL_SIZE as i32 * y as i32 + BORDER_RADIUS as i32, + CELL_SIZE - 2 * BORDER_RADIUS, + CELL_SIZE - 2 * BORDER_RADIUS, + ))?; + } + } + + match self.hold_piece() { + Some(p) => { + canvas.set_draw_color(p); + canvas.fill_rect(Rect::new(-32 - UI_PADDING as i32, 0, CELL_SIZE, CELL_SIZE)); + } + None => (), + } + + match self.active_piece { + Some(piece) => piece.render(canvas)?, + None => (), + } + + Ok(()) + } +} + +impl Renderable for Tetromino { + fn width(&self) -> usize { + CELL_SIZE as usize + * match self.rotation_state { + RotationState::O | RotationState::U => match self.piece_type { + TetrominoType::I => 4, + TetrominoType::O => 2, + _ => 3, + }, + RotationState::L | RotationState::R => match self.piece_type { + TetrominoType::I => 1, + _ => 2, + }, + } + } + + fn height(&self) -> usize { + CELL_SIZE as usize + * match self.rotation_state { + RotationState::O | RotationState::U => match self.piece_type { + TetrominoType::I => 1, + _ => 2, + }, + RotationState::L | RotationState::R => match self.piece_type { + TetrominoType::I => 4, + TetrominoType::O => 2, + _ => 3, + }, + } + } + + fn render(&self, canvas: &mut Canvas) -> Result<(), String> { + for Position { x, y } in self.get_cur_occupied_spaces() { + canvas.set_draw_color::(self.piece_type.into()); + let height = y as isize - PLAYFIELD_HEIGHT as isize; + canvas.fill_rect(Rect::new( + CELL_SIZE as i32 * x as i32 + BORDER_RADIUS as i32, + CELL_SIZE as i32 * height as i32 + BORDER_RADIUS as i32, + CELL_SIZE - 2 * BORDER_RADIUS, + CELL_SIZE - 2 * BORDER_RADIUS, + ))?; + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 44e199b..51cbb1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ -use game::{Controllable, Game, Tickable}; +use actors::*; +use game::{Action, Controllable, Game, Tickable}; +use graphics::standard_renderer; use graphics::COLOR_BACKGROUND; use log::{debug, info, trace}; +use rand::SeedableRng; use sdl2::event::Event; use sdl2::keyboard::Keycode; -use sdl2::{render::Canvas, video::Window}; use simple_logger; use std::time::Duration; use tokio::time::interval; +mod actors; mod game; mod graphics; mod playfield; @@ -17,13 +20,51 @@ mod tetromino; const TICKS_PER_SECOND: usize = 60; -pub trait Renderable { - fn render(&self, canvas: &mut Canvas) -> Result<(), String>; -} - #[tokio::main] async fn main() -> Result<(), Box> { - simple_logger::init()?; + // simple_logger::init()?; + simple_logger::init_with_level(log::Level::Info)?; + + let mut actor = qlearning::QLearningAgent::default(); + let mut rng = rand::rngs::StdRng::seed_from_u64(1337); + + let mut avg = 0.0; + + for i in 0..100000 { + if i % 100 == 0 { + info!("Last 100 scores avg: {}", avg); + avg = 0.0; + } + let mut game = Game::default(); + while (&game).is_game_over().is_none() { + let cur_state = (&game).into(); + let cur_score = game.score(); + let action = actor.get_action(&mut rng, &cur_state, &((&game).get_legal_actions())); + + match action { + Action::Nothing => (), + Action::MoveLeft => game.move_left(), + Action::MoveRight => game.move_right(), + Action::SoftDrop => game.move_down(), + Action::HardDrop => game.hard_drop(), + Action::Hold => game.hold(), + Action::RotateLeft => game.rotate_left(), + Action::RotateRight => game.rotate_right(), + } + + let new_state = (&game).into(); + let reward = game.score() - cur_score; + actor.update(cur_state, action, new_state, reward as f64); + + game.tick(); + } + + avg += game.score() as f64 / 100.0; + // info!("Game over with score of {}", game.score()); + } + + actor.exploration_prob = 0.0; + let sdl_context = sdl2::init()?; let video_subsystem = sdl_context.video()?; let window = video_subsystem @@ -44,6 +85,19 @@ async fn main() -> Result<(), Box> { None => (), } + let cur_state = (&game).into(); + let action = actor.get_action(&mut rng, &cur_state, &((&game).get_legal_actions())); + match action { + Action::Nothing => (), + Action::MoveLeft => game.move_left(), + Action::MoveRight => game.move_right(), + Action::SoftDrop => game.move_down(), + Action::HardDrop => game.hard_drop(), + Action::Hold => game.hold(), + Action::RotateLeft => game.rotate_left(), + Action::RotateRight => game.rotate_right(), + } + for event in event_pump.poll_iter() { match event { Event::Quit { .. } @@ -123,7 +177,7 @@ async fn main() -> Result<(), Box> { game.tick(); canvas.set_draw_color(COLOR_BACKGROUND); canvas.clear(); - game.render(&mut canvas)?; + standard_renderer::render(&mut canvas, &game); canvas.present(); interval.tick().await; } diff --git a/src/playfield.rs b/src/playfield.rs index 88af181..cd5c9b7 100644 --- a/src/playfield.rs +++ b/src/playfield.rs @@ -1,22 +1,21 @@ -use crate::graphics::{BORDER_RADIUS, CELL_SIZE, COLOR_BACKGROUND}; use crate::random::RandomSystem; -use crate::tetromino::{MinoColor, Position, Tetromino, TetrominoType}; -use crate::Renderable; -use sdl2::{pixels::Color, rect::Rect, render::Canvas, video::Window}; +use crate::tetromino::{Position, Tetromino, TetrominoType}; use std::collections::VecDeque; use std::fmt; pub const PLAYFIELD_HEIGHT: usize = 20; pub const PLAYFIELD_WIDTH: usize = 10; -pub type Matrix = [[Option; PLAYFIELD_WIDTH]; 2 * PLAYFIELD_HEIGHT]; +pub type Matrix = Vec>>; +#[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Movement { Rotation, Gravity, Translation, } +#[derive(Clone)] pub struct PlayField { can_swap_hold: bool, hold_piece: Option, @@ -85,10 +84,15 @@ impl PlayField { next_pieces.push_back(bag.get_tetromino()); } + let row = [None; PLAYFIELD_WIDTH].to_vec(); + let mut field = Vec::with_capacity(2 * PLAYFIELD_HEIGHT); + for _ in 0..2 * PLAYFIELD_HEIGHT { + field.push(row.clone()); + } PlayField { can_swap_hold: true, hold_piece: None, - field: [[None; PLAYFIELD_WIDTH]; 2 * PLAYFIELD_HEIGHT], + field, active_piece: Some(active_piece), bag, next_pieces, @@ -161,10 +165,9 @@ impl PlayField { pub fn lock_active_piece(&mut self) -> Vec { match &self.active_piece { Some(active_piece) => { - let active_color = active_piece.get_color(); let new_pieces = active_piece.get_cur_occupied_spaces(); for Position { x, y } in &new_pieces { - self.field[*y as usize][*x as usize] = Some(active_color); + self.field[*y as usize][*x as usize] = Some(active_piece.piece_type); } new_pieces @@ -209,49 +212,27 @@ impl PlayField { Err(()) } + pub fn can_swap(&self) -> bool { + self.can_swap_hold + } + pub fn try_clear_row(&mut self, row: usize) -> Result<(), ()> { if self.field[row].iter().all(|cell| cell.is_some()) { - self.field[row] = [None; PLAYFIELD_WIDTH]; + self.field[row] = [None; PLAYFIELD_WIDTH].to_vec(); for y in (1..=row).rev() { - self.field[y] = self.field[y - 1]; + self.field[y] = self.field[y - 1].clone(); } Ok(()) } else { Err(()) } } -} -impl Renderable for PlayField { - fn render(&self, canvas: &mut Canvas) -> Result<(), String> { - for y in 0..PLAYFIELD_HEIGHT { - for x in 0..PLAYFIELD_WIDTH { - canvas.set_draw_color(Color::RGB(0, 0, 0)); - canvas.fill_rect(Rect::new( - CELL_SIZE as i32 * x as i32, - CELL_SIZE as i32 * y as i32, - CELL_SIZE, - CELL_SIZE, - ))?; + pub fn field(&self) -> &Matrix { + &self.field + } - match self.field[y + PLAYFIELD_HEIGHT][x] { - Some(mino) => canvas.set_draw_color(mino), - None => canvas.set_draw_color(COLOR_BACKGROUND), - } - canvas.fill_rect(Rect::new( - CELL_SIZE as i32 * x as i32 + BORDER_RADIUS as i32, - CELL_SIZE as i32 * y as i32 + BORDER_RADIUS as i32, - CELL_SIZE - 2 * BORDER_RADIUS, - CELL_SIZE - 2 * BORDER_RADIUS, - ))?; - } - } - - match self.active_piece { - Some(piece) => piece.render(canvas)?, - None => (), - } - - Ok(()) + pub fn hold_piece(&self) -> Option<&TetrominoType> { + self.hold_piece.as_ref() } } diff --git a/src/random.rs b/src/random.rs index abbe3a9..8150fc5 100644 --- a/src/random.rs +++ b/src/random.rs @@ -1,6 +1,7 @@ use crate::tetromino::TetrominoType; use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng}; +#[derive(Clone, Copy, Debug)] pub struct RandomSystem { rng: ThreadRng, bag: [TetrominoType; 7], diff --git a/src/tetromino.rs b/src/tetromino.rs index d2484ab..d7866fe 100644 --- a/src/tetromino.rs +++ b/src/tetromino.rs @@ -1,58 +1,9 @@ use crate::{ graphics::*, playfield::{PLAYFIELD_HEIGHT, PLAYFIELD_WIDTH}, - Renderable, }; -use sdl2::{pixels::Color, rect::Rect, render::Canvas, video::Window}; -use std::fmt; -#[derive(Copy, Clone)] -pub enum MinoColor { - Cyan, - Yellow, - Purple, - Green, - Red, - Blue, - Orange, - Gray, -} - -impl fmt::Debug for MinoColor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Cyan => "c", - Self::Yellow => "y", - Self::Purple => "p", - Self::Green => "g", - Self::Red => "r", - Self::Blue => "b", - Self::Orange => "o", - Self::Gray => "x", - } - ) - } -} - -impl Into for MinoColor { - fn into(self) -> Color { - match self { - Self::Cyan => COLOR_CYAN, - Self::Yellow => COLOR_YELLOW, - Self::Purple => COLOR_PURPLE, - Self::Green => COLOR_GREEN, - Self::Red => COLOR_RED, - Self::Blue => COLOR_BLUE, - Self::Orange => COLOR_ORANGE, - Self::Gray => COLOR_GRAY, - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum TetrominoType { I, O, @@ -91,7 +42,7 @@ impl Default for RotationState { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct Position { pub x: isize, pub y: isize, @@ -133,7 +84,7 @@ impl std::ops::Sub for Position { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct Tetromino { pub position: Position, pub piece_type: TetrominoType, @@ -149,18 +100,6 @@ impl Tetromino { } } - pub fn get_color(&self) -> MinoColor { - match self.piece_type { - TetrominoType::I => MinoColor::Cyan, - TetrominoType::O => MinoColor::Yellow, - TetrominoType::T => MinoColor::Purple, - TetrominoType::S => MinoColor::Green, - TetrominoType::Z => MinoColor::Red, - TetrominoType::J => MinoColor::Blue, - TetrominoType::L => MinoColor::Orange, - } - } - fn get_start_position(tetromino_type: TetrominoType) -> Position { if tetromino_type == TetrominoType::I { Position::new( @@ -366,19 +305,3 @@ impl From for Tetromino { Self::new(tetromino_type) } } - -impl Renderable for Tetromino { - fn render(&self, canvas: &mut Canvas) -> Result<(), String> { - for Position { x, y } in self.get_cur_occupied_spaces() { - canvas.set_draw_color::(self.piece_type.into()); - let height = y as isize - PLAYFIELD_HEIGHT as isize; - canvas.fill_rect(Rect::new( - CELL_SIZE as i32 * x as i32 + BORDER_RADIUS as i32, - CELL_SIZE as i32 * height as i32 + BORDER_RADIUS as i32, - CELL_SIZE - 2 * BORDER_RADIUS, - CELL_SIZE - 2 * BORDER_RADIUS, - ))?; - } - Ok(()) - } -}