From aa94df6513c18ce5bad544a01ac6e9dfdcb4d2ad Mon Sep 17 00:00:00 2001 From: grdddj Date: Sun, 26 Mar 2023 17:21:16 +0200 Subject: [PATCH] WIP - trezor-dinosaur homescreen game --- core/assets/model_r/logo_10_15.png | Bin 0 -> 6646 bytes core/embed/rust/src/ui/geometry.rs | 5 + .../rust/src/ui/model_tr/component/game.rs | 442 ++++++++++++++++++ .../src/ui/model_tr/component/homescreen.rs | 37 +- .../rust/src/ui/model_tr/component/mod.rs | 1 + .../rust/src/ui/model_tr/res/logo_10_15.toif | Bin 0 -> 49 bytes core/embed/rust/src/ui/model_tr/theme.rs | 3 +- 7 files changed, 483 insertions(+), 5 deletions(-) create mode 100644 core/assets/model_r/logo_10_15.png create mode 100644 core/embed/rust/src/ui/model_tr/component/game.rs create mode 100644 core/embed/rust/src/ui/model_tr/res/logo_10_15.toif diff --git a/core/assets/model_r/logo_10_15.png b/core/assets/model_r/logo_10_15.png new file mode 100644 index 0000000000000000000000000000000000000000..21b992d990a4c1a0f9520aae1e7d8a7e3e981b63 GIT binary patch literal 6646 zcmeHKc{tQ-`{z)W?2$r@L8N9kW?>RD6S8CrEn>`OFlNk*z35PuQnHKa2$elcNm)9T z$Qn{1+GNers$_X*)H$cF-}}$GuHXA#b6qpv=leXL`*Yv-=eeKfnmf+U)?8XrMN&ve zNZQhZ;J|+h#`)5P;2L(HOo|056111yF+-G$Enjf%>zI zq*vIo<11XHdQpGTTD8;i^-(p8%R5^e0%i8@s{!V?G459qBGF>EgxlIzzRto|Uc9yn z+(Nitn9qAFsiWwEx&Jt%Z;4TU#l*dQV!mc-bpO2UX{($(6L;>7*@%NRUBaEYoDJ@4 zTbGt|>sRb<(cu;RnTnfd}aAq7*y?D>iL}VNTQXEwEOUf z=-o?eOPkMriFIbrgO;O`d*6S49YB5+qp`?UpT}}r=Ff&T&OAmY5H;$`BYaNPeahTN zl$LP06lR}L@CGsCW1Z_rk77(HECcZ^;mX6C)&^aMmW3mShc-+d_38JBDGlteO&AKa zmhw~4J%@gr{Y<$^+2!E;3HJvzZnn4BpQXx<+)=L!8}OX3emzO+N;@0k>KBmq=k|of zw}XolB0dZ~*RY|^m<>-}pTnvm6fsL9hH@2Cf!L6kTK^?dwZ49oW7`_-e0} z`=a3@)3q>;G0cnlROC7E;^~ce1JQNfkIb{)=yb+iMZ-nxns{STJ^&9gPejXYj*xi$ z4bNL${w6D++rOWkOw84z&y!;BNZb-Yhl%6@=oqwDr*;xUKx#lq9qWtuokI$KsI`ew9u zJmFPJAF=Zet{V4ZZXjMKbdf*np>J?Pea{c>0D7r8Wq$8_vyeTJFoE@)A|FHx<&3&Y6f>@NsR&o&EC; z%^x+>*r%c|?a4d}r}zb5b5ntID4b_zk}iqk6OU_4zeJZ-T(}eB$ zwJi=kL#}9Fhfk$C6?YC)PsD;0u$rR{t`1wHRSqh=FjfgM>OZZ=R`-u{R7E`6t5|2I zujemw;;x!x+KsArQ-|g5j^7`Eg2z=KHz`MY`@L%O$~6QWYrcU|=3%BtQqQ{Xg;uSniIhb*GkHYqydYi;7 z7jUyr(9=bV4{a?~Zc0ks1=^p%rBMn{(F*!72G1EFizhs3H;D4%6gji5m$OH|5QoedTGlQxf7#$w?h^- zq7vltn8l#|f&B_oJ=TS$6Lw;8ktnBof7EJK42*~)GGGTDOHPbE5fL6axFjmMc>*65 zaud0)RR2^sCbf6JPiJt1$vMocr6u-gUtQy-&clbM8&x|GkJX)*xug_+!AokB-*Udc z1Zv$GQIxFHi$Y|K7|s4mZ9;o!eu~0d?QkW>Lsxs1aZkqbYWL(M)idKhM$v0h-cKkN z#ugpP_+v+K#+C4zGhH*##&-z|tgv@trwv#+i{P9bKjXH~-b$n9Ifwo82ZCzU{a?)~DK zFTD7@s+BL2H^=n@Pch^rv;1cViVvneXAIog68gw2MV{n!{lJG25Kvef1spyl%`WF&TN=j7c@~nz&6VYA6M;S3y zkf6edNyhy3jz*ce;Nit-p@gvxcbU1A2cxRe^?mWFd9(+Gmp6S*?>sFUN;}!ttyMJM zvMFzvSgV1ndaeK9Ybibaep^+@l|Wh3d@oE9PyQ0#sH6H?v$?Cta;`>4??s0ON0W}} z0$wFd?jAl%DVf*XEVEFXIDU5G>x9GpT_44UC_0hlwwJuKRZgUjhc+ncj5bf|yp&1_ zO0O6{1jqHes?e_8wnoKk=mb*QIVB^LBk`kkeRg-W57WBVN^GGI=?%k;wMquf{5#FnpYl_+NsZ;f?H;$^_pC@C4cJvocXvjJ!a4B$%j3mb?^KNgd2Ry zC5c;xp6Bayf2dA6;V5GPUI>p15smn$c4I~cfKdFPS~4fD>`=Le6RoGE>du$hf7ndYteOx<`zY5@08 zc&$fBd+bPYNG5zq_Cnc8V&9QRv@`l)G=){DW&hu zbjTW08{CsHquzObX1H%?$Xuh~&gTG!!Mbdyj?XBC19NJaY)$;5@eI8r>eiwL@0#VS z`YpH)em9}Q;CBtKHr7}&iwP!CSe{gHFq184D};6Lv5YNp*~~`1!%HE(l{8)Cty-}BtS6J*N=-0#sOD(vHY=M z8Uh5YLU=wnpsS4?0MFu30R~_LFcd@#W(2~4J0tgzHtz!Tr!8j<}p})00Ae-lNG?j z0fGE=z+dq(*)}$R)BAD1tH9R-5=>%4U|=YO$%Oo9!Q~MH`6S;1`mYvTCw}tJtAIjx>h5h8x+|tJGZwrA0bOw{XYQ-1( zCnS$S`$w#wViT;art@PUeDl9~e?tG2`zn}^va!JuSmXdfc$NelP~aa+VUZaW?CQ{f zib8tQ;GQ5V5()zukYFf~CmKoyk!hZuo=7AHMn)R^KxOI2<&pfzQ~?#A9L(VJplNV4 z3`M4b&}cLnWB`L1fIP_*3J6U?ppXbp5*cX-`+;H)hrzE(lJAdE38*N1Dg#5BAr(bI zgJ7N%PXQGPgdtNKWcb381a->?d%_UGss1ZtW61x} z#CR3(!!^LS`!>hFUih~apH)c|SgqSQ$r=RnNrWtkwuCY>kI4T3%ADAl literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index 70c410fcf..4eede460b 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -493,6 +493,11 @@ impl Rect { self.bottom_left() - Offset::y(1), ] } + + /// Whether two rects collide with each other + pub fn collides(&self, other: Rect) -> bool { + self.x0 < other.x1 && self.x1 > other.x0 && self.y0 < other.y1 && self.y1 > other.y0 + } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/core/embed/rust/src/ui/model_tr/component/game.rs b/core/embed/rust/src/ui/model_tr/component/game.rs new file mode 100644 index 000000000..129a53daa --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/game.rs @@ -0,0 +1,442 @@ +use crate::{ + time::Duration, + trezorhal::random, + ui::{ + component::{Child, Component, Event, EventCtx, Pad, TimerToken}, + display::{self, Font, Icon}, + geometry::{self, Offset, Point, Rect}, + model_tr::constant, + }, +}; + +use super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos}; + +use heapless::{String, Vec}; + +const SCREEN: Rect = constant::screen(); +const BOTTOM_Y: i16 = SCREEN.bottom_left().y - theme::BUTTON_HEIGHT - BTN_OFFSET; +const RIGHT_X: i16 = SCREEN.top_right().x; + +const MAX_JUMP_HEIGHT: u32 = 20; +const BTN_OFFSET: i16 = 5; + +const MAX_OBSTACLES: usize = 1; +const OBSTACLE_FONT: Font = Font::BOLD; + +// Speed in pixels per frame +const OBSTACLE_SPEED: f32 = 2.7; +const TREZOR_SPEED: f32 = 1.0; + +const TREZOR_ICON: Icon = Icon::new(theme::ICON_LOGO); + +const FUD_LENGTH: usize = 20; +#[rustfmt::skip] +const FUD_LIST: [&str; FUD_LENGTH] = [ + "USA", + "USD", + "SEC", + "EU", + "EUR", + "ECB", + "IMF", + "KYC", + "AML", + "ICO", + "PoS", + "NFT", + "ETH", + "BCH", + "MtG", + "PRC", + "JPM", + "FTX", + "SBF", + "CSW", +]; + +pub enum GameMsg { + Dismissed, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum GameState { + Initial, + Started, + Finished, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum TrezorState { + Bottom, + Jumped(u32), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Obstacle { + spawned_frame: u32, + fud_name: &'static str, + width: i16, + height: i16, +} + +impl Obstacle { + pub fn new(spawned_frame: u32, fud_name: &'static str) -> Self { + Self { + spawned_frame, + fud_name, + width: OBSTACLE_FONT.text_width(fud_name) + 2, + height: 8, + } + } + + pub fn get_rect(&self, frame_count: u32) -> Rect { + let x_diff = (OBSTACLE_SPEED * (frame_count - self.spawned_frame) as f32) as i16; + let left_x = SCREEN.top_right().x - x_diff; + let bottom_left = Point::new(left_x, BOTTOM_Y); + let size = Offset::new(self.width, self.height); + Rect::from_bottom_left_and_size(bottom_left, size) + } + + pub fn is_out_of_screen(&self, frame_count: u32) -> bool { + self.get_rect(frame_count).x1 < 0 + } + + pub fn is_colliding(&self, trezor_rect: Rect, frame_count: u32) -> bool { + let obstacle_rect = self.get_rect(frame_count); + trezor_rect.collides(obstacle_rect) + } + + pub fn paint(&self, frame_count: u32) { + let rect = self.get_rect(frame_count); + display::rect_fill(rect, theme::FG); + display::text_center( + rect.bottom_center(), + self.fud_name, + OBSTACLE_FONT, + theme::BG, + theme::FG, + ); + } +} + +pub struct Game { + game_state: GameState, + trezor_state: TrezorState, + trezor_jump_height: i16, + obstacles: Vec, + remaining_fuds: Vec<&'static str, FUD_LENGTH>, + collided_fud: Option<&'static str>, + timer: Option, + timer_duration: Duration, + frame_count: u32, + highest_score: u32, + pad: Pad, + buttons: Child, + needs_left_release: bool, +} + +impl Game { + pub fn new() -> Self { + Self { + game_state: GameState::Initial, + trezor_state: TrezorState::Bottom, + trezor_jump_height: 0, + obstacles: Vec::new(), + remaining_fuds: create_heapless_vec_from_array(FUD_LIST), + collided_fud: None, + timer: None, + timer_duration: Duration::from_millis(30), + frame_count: 0, + highest_score: 0, + pad: Pad::with_background(theme::BG).with_clear(), + buttons: Child::new(ButtonController::new(Self::get_button_layout( + GameState::Initial, + ))), + needs_left_release: false, + } + } + + fn get_score(&self) -> u32 { + self.frame_count / 10 + } + + fn get_button_layout(state: GameState) -> ButtonLayout { + match state { + GameState::Initial | GameState::Finished => { + ButtonLayout::text_none_text("START".into(), "CANCEL".into()) + } + GameState::Started => ButtonLayout::text_none_text("JUMP".into(), "STOP".into()), + } + } + + /// Reflecting the current page in the buttons. + fn update_buttons(&mut self, ctx: &mut EventCtx) { + let btn_layout = Self::get_button_layout(self.game_state); + self.buttons.mutate(ctx, |ctx, buttons| { + buttons.set(btn_layout); + ctx.request_paint(); + }); + } + + fn start(&mut self, ctx: &mut EventCtx) { + if self.game_state == GameState::Initial || self.game_state == GameState::Finished { + self.game_state = GameState::Started; + self.trezor_state = TrezorState::Bottom; + self.trezor_jump_height = 0; + self.obstacles.clear(); + self.collided_fud = None; + self.frame_count = 0; + self.remaining_fuds = create_heapless_vec_from_array(FUD_LIST); + self.timer = Some(ctx.request_timer(self.timer_duration)); + self.update_buttons(ctx); + ctx.request_paint(); + } + } + + fn stop(&mut self, ctx: &mut EventCtx) { + if self.game_state == GameState::Started { + self.game_state = GameState::Finished; + self.timer = None; + self.update_buttons(ctx); + ctx.request_paint(); + } + } + + fn jump(&mut self, ctx: &mut EventCtx) { + if self.trezor_state == TrezorState::Bottom && self.game_state == GameState::Started { + self.trezor_state = TrezorState::Jumped(self.frame_count); + ctx.request_paint(); + }; + } + + fn update_trezor_jump_height(&mut self) { + self.trezor_jump_height = match self.trezor_state { + TrezorState::Bottom => 0, + TrezorState::Jumped(frame_count) => { + let diff = (TREZOR_SPEED * (self.frame_count - frame_count) as f32) as u32; + if diff >= 2 * MAX_JUMP_HEIGHT { + self.trezor_state = TrezorState::Bottom; + } + if diff < MAX_JUMP_HEIGHT { + diff as i16 + } else { + (2 * MAX_JUMP_HEIGHT - diff) as i16 + } + } + }; + } + + fn paint_floor(&self) { + for x in SCREEN.x0..SCREEN.x1 { + let point = Point::new(x, BOTTOM_Y); + display::paint_point(&point, theme::FG); + } + } + + fn paint_header(&self) { + display::text_right( + SCREEN.top_right() + Offset::y(10), + "Jump over FUD!", + Font::BOLD, + theme::FG, + theme::BG, + ); + } + + fn paint_score(&self) { + let score_line = if self.highest_score > 0 { + build_string!( + 20, + inttostr!(self.get_score()), + " HI ", + inttostr!(self.highest_score) + ) + } else { + build_string!(20, inttostr!(self.get_score())) + }; + display::text_right( + SCREEN.top_right() + Offset::y(20), + &score_line, + Font::BOLD, + theme::FG, + theme::BG, + ); + } + + fn paint_game_over(&self) { + let text = build_string!(20, "Defeated by ", self.collided_fud.unwrap_or("FUD")); + display::text_right( + SCREEN.top_right() + Offset::y(30), + &text, + Font::BOLD, + theme::FG, + theme::BG, + ); + } + + fn paint_trezor(&mut self) { + let current_y = BOTTOM_Y - self.trezor_jump_height as i16; + TREZOR_ICON.draw( + Point::new(SCREEN.x0, current_y), + geometry::BOTTOM_LEFT, + theme::FG, + theme::BG, + ); + } + + fn paint_obstacles(&self) { + for obstacle in self.obstacles.iter() { + obstacle.paint(self.frame_count); + } + } + + fn update_obstacles(&mut self) { + if self.frame_count % 30 == 0 && self.obstacles.len() < MAX_OBSTACLES { + let fud_index = random::uniform(self.remaining_fuds.len() as u32); + let fud_name = self.remaining_fuds[fud_index as usize]; + let obstacle = Obstacle::new(self.frame_count, fud_name); + unwrap!(self.obstacles.push(obstacle)); + // So that we do not show duplicated fuds - always circle through the whole list + self.remaining_fuds = filter_heapless_vec(&self.remaining_fuds, |fud| fud != &fud_name); + if self.remaining_fuds.is_empty() { + self.remaining_fuds = create_heapless_vec_from_array(FUD_LIST); + } + } + self.obstacles = filter_heapless_vec(&self.obstacles, |obstacle| { + !obstacle.is_out_of_screen(self.frame_count) + }); + } + + fn check_for_collision(&self) -> Option { + let trezor_bottom_y = BOTTOM_Y - self.trezor_jump_height; + let trezor_rect = Rect::new( + Point::new(SCREEN.x0, trezor_bottom_y - TREZOR_ICON.toif.height()), + Point::new(SCREEN.x0 + TREZOR_ICON.toif.width(), trezor_bottom_y), + ); + for obstacle in self.obstacles.iter() { + if obstacle.is_colliding(trezor_rect, self.frame_count) { + return Some(*obstacle); + } + } + None + } +} + +fn filter_heapless_vec( + input: &Vec, + mut predicate: F, +) -> Vec +where + T: core::clone::Clone, + F: FnMut(&T) -> bool, +{ + let mut filtered = Vec::::new(); + for item in input.iter() { + if predicate(item) { + unwrap!(filtered.push(item.clone())); + } + } + filtered +} + +fn create_heapless_vec_from_array( + array: [&'static str; N], +) -> Vec<&'static str, N> { + let mut vec = Vec::<&'static str, N>::new(); + + for &item in array.iter() { + vec.push(item) + .unwrap_or_else(|_| panic!("Vector capacity exceeded")); + } + + vec +} + +impl Component for Game { + type Msg = GameMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (content_area, button_area) = SCREEN.split_bottom(theme::BUTTON_HEIGHT); + self.pad.place(content_area); + self.buttons.place(button_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let button_event = self.buttons.event(ctx, event); + + // For the JUMP release not to trigger the START button (after collision) + if self.needs_left_release { + if let Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) = button_event { + self.needs_left_release = false; + return None; + } + } + + match self.game_state { + GameState::Initial | GameState::Finished => { + if let Some(ButtonControllerMsg::Triggered(triggered_btn)) = button_event { + match triggered_btn { + ButtonPos::Left => self.start(ctx), + ButtonPos::Right => return Some(GameMsg::Dismissed), + _ => {} + } + } + } + GameState::Started => { + if let Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) = button_event { + self.jump(ctx); + self.needs_left_release = true; + } + if let Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) = button_event { + self.stop(ctx); + } + } + } + if let Event::Timer(token) = event { + if self.timer == Some(token) { + self.update_trezor_jump_height(); + self.update_obstacles(); + if let Some(collision) = self.check_for_collision() { + self.highest_score = self.highest_score.max(self.get_score()); + self.collided_fud = Some(collision.fud_name); + self.stop(ctx); + } else { + self.frame_count += 1; + self.timer = Some(ctx.request_timer(self.timer_duration)); + }; + } + } + self.pad.clear(); + ctx.request_paint(); + None + } + + fn paint(&mut self) { + self.pad.paint(); + self.buttons.paint(); + self.paint_floor(); + self.paint_trezor(); + self.paint_obstacles(); + self.paint_header(); + self.paint_score(); + if let GameState::Finished = self.game_state { + self.paint_game_over(); + } + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.pad.area); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Game { + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("Game"); + d.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/homescreen.rs b/core/embed/rust/src/ui/model_tr/component/homescreen.rs index a402276d4..ca34c0a22 100644 --- a/core/embed/rust/src/ui/model_tr/component/homescreen.rs +++ b/core/embed/rust/src/ui/model_tr/component/homescreen.rs @@ -10,7 +10,11 @@ use crate::{ }, }; -use super::{common::display_center, theme}; +use super::{ + common::display_center, + game::{Game, GameMsg}, + theme, +}; const AREA: Rect = constant::screen(); const TOP_CENTER: Point = AREA.top_center(); @@ -24,6 +28,8 @@ pub struct Homescreen { notification: Option<(StrBuffer, u8)>, usb_connected: bool, pad: Pad, + show_game: bool, + game: Game, } pub enum HomescreenMsg { @@ -37,6 +43,8 @@ impl Homescreen { notification, usb_connected: true, pad: Pad::with_background(theme::BG), + show_game: false, + game: Game::new(), } } @@ -64,16 +72,34 @@ impl Component for Homescreen { fn place(&mut self, bounds: Rect) -> Rect { self.pad.place(AREA); + self.game.place(AREA); bounds } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - Self::event_usb(self, ctx, event); + if self.show_game { + if let Some(GameMsg::Dismissed) = self.game.event(ctx, event) { + self.show_game = false; + self.pad.clear(); + ctx.request_paint(); + } + } else { + Self::event_usb(self, ctx, event); + if let Event::Button(ButtonEvent::ButtonReleased(_)) = event { + self.show_game = true; + self.pad.clear(); + ctx.request_paint(); + } + } None } fn paint(&mut self) { self.pad.paint(); + if self.show_game { + self.game.paint(); + return; + } self.paint_notification(); Icon::new(theme::ICON_LOGO).draw( TOP_CENTER + Offset::y(ICON_TOP_MARGIN), @@ -84,8 +110,11 @@ impl Component for Homescreen { let label = self.label.as_ref(); // Special case for the initial screen label if label == "Go to trezor.io/start" { - display_center(TOP_CENTER + Offset::y(54), &"Go to", Font::BOLD); - display_center(TOP_CENTER + Offset::y(64), &"trezor.io/start", Font::BOLD); + display_center( + TOP_CENTER + Offset::y(LABEL_Y), + &"Press to play", + Font::BOLD, + ); } else { display_center(TOP_CENTER + Offset::y(LABEL_Y), &label, Font::NORMAL); }; diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index 1b2c15aa6..ce056a7f1 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -7,6 +7,7 @@ mod flow; mod flow_pages; mod flow_pages_helpers; mod frame; +mod game; mod hold_to_confirm; mod homescreen; mod input_methods; diff --git a/core/embed/rust/src/ui/model_tr/res/logo_10_15.toif b/core/embed/rust/src/ui/model_tr/res/logo_10_15.toif new file mode 100644 index 0000000000000000000000000000000000000000..af9979309459939bb85706b4718f5966df1d96c6 GIT binary patch literal 49 zcmWIX_jKoC;Ac=}U|@*t+Q`dbz;oyi