WIP - trezor-dinosaur homescreen game

grdddj/tr_homescreen_game
grdddj 1 year ago
parent 50aae5b200
commit aa94df6513

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -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)]

@ -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<Obstacle, MAX_OBSTACLES>,
remaining_fuds: Vec<&'static str, FUD_LENGTH>,
collided_fud: Option<&'static str>,
timer: Option<TimerToken>,
timer_duration: Duration,
frame_count: u32,
highest_score: u32,
pad: Pad,
buttons: Child<ButtonController>,
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<Obstacle> {
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<T, F, const N: usize, const M: usize>(
input: &Vec<T, N>,
mut predicate: F,
) -> Vec<T, M>
where
T: core::clone::Clone,
F: FnMut(&T) -> bool,
{
let mut filtered = Vec::<T, M>::new();
for item in input.iter() {
if predicate(item) {
unwrap!(filtered.push(item.clone()));
}
}
filtered
}
fn create_heapless_vec_from_array<const N: usize>(
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<Self::Msg> {
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();
}
}

@ -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::Msg> {
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);
};

@ -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;

@ -64,7 +64,8 @@ pub const ICON_CANCEL: &[u8] = include_res!("model_tr/res/cancel_no_outline.toif
pub const ICON_DELETE: &[u8] = include_res!("model_tr/res/delete.toif"); // 10*7
pub const ICON_EYE: &[u8] = include_res!("model_tr/res/eye_round.toif"); // 12*7
pub const ICON_LOCK: &[u8] = include_res!("model_tr/res/lock.toif"); // 10*10
pub const ICON_LOGO: &[u8] = include_res!("model_tr/res/logo_22_33.toif"); // 22*44
pub const ICON_LOGO: &[u8] = include_res!("model_tr/res/logo_22_33.toif"); // 22*33
pub const ICON_LOGO_SMALL: &[u8] = include_res!("model_tr/res/logo_10_15.toif"); // 10*15
pub const ICON_NEXT_PAGE: &[u8] = include_res!("model_tr/res/next_page.toif"); // 10*8
pub const ICON_PREV_PAGE: &[u8] = include_res!("model_tr/res/prev_page.toif"); // 8*10
pub const ICON_SPACE: &[u8] = include_res!("model_tr/res/space.toif"); // 12*3

Loading…
Cancel
Save