Replace tiers with achievements

This commit is contained in:
2026-05-21 19:17:58 +03:00
parent 4765917be4
commit a6741da14c
4 changed files with 139 additions and 192 deletions

View File

@@ -15,37 +15,47 @@ pub mod l02_kv;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Game-wide difficulty. Chosen once on the TierSelect screen; persisted /// Awarded per level based on the grade score. Replaces the old
/// in `Progress.tier`. Maps to a fixed passing threshold per level. /// game-wide difficulty tier. Thresholds:
/// - Gold ≥ 95 %
/// - Silver ≥ 80 %
/// - Bronze ≥ 70 %
/// - Below 70 % → no nugget (retry).
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Difficulty { pub enum Nugget {
Easy, Bronze,
Medium, Silver,
Hard, Gold,
} }
impl Difficulty { impl Nugget {
pub fn threshold(self) -> f64 { /// Map a grade ratio (0.0..=1.0) to a nugget. `None` means the
match self { /// player didn't clear the level — retry without advancing.
Self::Easy => 0.70, pub fn from_score(score: f64) -> Option<Self> {
Self::Medium => 0.80, if score >= 0.95 {
Self::Hard => 0.95, Some(Nugget::Gold)
} else if score >= 0.80 {
Some(Nugget::Silver)
} else if score >= 0.70 {
Some(Nugget::Bronze)
} else {
None
} }
} }
pub fn label(self) -> &'static str { pub fn emoji(self) -> &'static str {
match self { match self {
Self::Easy => "Easy (70%)", Self::Bronze => "🥉",
Self::Medium => "Medium (80%)", Self::Silver => "🥈",
Self::Hard => "Hard (95%)", Self::Gold => "🥇",
} }
} }
pub fn name(self) -> &'static str { pub fn name(self) -> &'static str {
match self { match self {
Self::Easy => "Easy", Self::Bronze => "Bronze",
Self::Medium => "Medium", Self::Silver => "Silver",
Self::Hard => "Hard", Self::Gold => "Gold",
} }
} }
} }

View File

@@ -69,15 +69,14 @@ mod smoke {
// (4) Progress round-trips through the same YAML pipeline that disk // (4) Progress round-trips through the same YAML pipeline that disk
// save/load will use. // save/load will use.
let p = progress::Progress { let p = progress::Progress {
tier: Some(levels::Difficulty::Medium), nuggets: vec![(1, levels::Nugget::Silver), (2, levels::Nugget::Gold)],
completed: vec![1, 2],
current_level: 3, current_level: 3,
current_seed: 0xCAFE, current_seed: 0xCAFE,
attempts: 1, attempts: 1,
}; };
let s = serde_yaml::to_string(&p).unwrap(); let s = serde_yaml::to_string(&p).unwrap();
let loaded: progress::Progress = serde_yaml::from_str(&s).unwrap(); let loaded: progress::Progress = serde_yaml::from_str(&s).unwrap();
assert_eq!(loaded.tier, p.tier); assert_eq!(loaded.nuggets, p.nuggets);
assert_eq!(loaded.current_level, p.current_level); assert_eq!(loaded.current_level, p.current_level);
assert_eq!(loaded.current_seed, p.current_seed); assert_eq!(loaded.current_seed, p.current_seed);
} }

View File

@@ -4,13 +4,16 @@ use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use crate::levels::Difficulty; use crate::levels::Nugget;
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Progress { pub struct Progress {
pub tier: Option<Difficulty>, /// Nuggets awarded per level: `(level_id, nugget)` in order of
pub completed: Vec<u8>, /// completion. A level appears at most once (re-runs after reset
pub current_level: u8, /// rebuild the list from scratch).
pub nuggets: Vec<(u8, Nugget)>,
pub current_level: u8, // 1-indexed; 0 means a new game
pub current_seed: u64, pub current_seed: u64,
pub attempts: u32, pub attempts: u32,
} }

View File

@@ -13,7 +13,7 @@ use std::io::{stdout, Stdout};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::config; use crate::config;
use crate::levels::{self, Difficulty, Level}; use crate::levels::{self, Level, Nugget};
use crate::progress::{self, Progress}; use crate::progress::{self, Progress};
use crate::similarity; use crate::similarity;
@@ -21,7 +21,6 @@ use crate::similarity;
enum Screen { enum Screen {
Welcome, Welcome,
TierSelect { cursor: u8 },
/// Active play: left column = HistoryLog, right column = Editor. /// Active play: left column = HistoryLog, right column = Editor.
Game, Game,
ResetConfirm, ResetConfirm,
@@ -35,34 +34,31 @@ enum Focus {
} }
enum LogEntry { enum LogEntry {
/// Game rules + the full controls listing. Appended once on /// Game rules + the full controls listing. Seeded at startup.
/// Welcome → TierSelect so the player can scroll back to it.
Intro, Intro,
/// The full tier table with a marker on the chosen one.
TierChoice {
chosen: u8, // 0=Easy, 1=Medium, 2=Hard
},
LevelPrompt { LevelPrompt {
level_id: u8, level_id: u8,
level_name: String, level_name: String,
tier_name: &'static str,
flavor: String, flavor: String,
description: String, description: String,
}, },
ResultPass { ResultPass {
level_name: String, level_name: String,
score: f64, score: f64,
threshold: f64, nugget: Nugget,
}, },
ResultFail { ResultFail {
level_name: String, level_name: String,
score: f64, score: f64,
threshold: f64,
}, },
InvalidYaml { InvalidYaml {
error: String, error: String,
}, },
Completed, Completed {
gold: u32,
silver: u32,
bronze: u32,
},
} }
/// Append-only chronological log of game events. Rendered bottom-anchored /// Append-only chronological log of game events. Rendered bottom-anchored
@@ -259,54 +255,57 @@ fn leave() -> Result<()> {
Ok(()) Ok(())
} }
fn tier_index(t: Difficulty) -> u8 {
match t {
Difficulty::Easy => 0,
Difficulty::Medium => 1,
Difficulty::Hard => 2,
}
}
fn initial_state(prog: &Progress, registry: &[Box<dyn Level>]) -> (Screen, HistoryLog) { fn initial_state(prog: &Progress, registry: &[Box<dyn Level>]) -> (Screen, HistoryLog) {
let mut log = HistoryLog::new(); let mut log = HistoryLog::new();
// The Intro entry is always the bottom of the log on startup so that // The Intro entry is always at the bottom of the log on startup so
// Welcome and TierSelect can render it inline (the player can scroll // Welcome can show it inline; the player can scroll back to it any
// back to it any time). // time during play.
log.push(LogEntry::Intro); log.push(LogEntry::Intro);
match (prog.tier, prog.current_level) { match prog.current_level {
(None, 0) => (Screen::Welcome, log), 0 => (Screen::Welcome, log),
(None, _) => (Screen::TierSelect { cursor: 0 }, log), n if (n as usize) > registry.len() => {
(Some(tier), 0) => (Screen::TierSelect { cursor: tier_index(tier) }, log), let (gold, silver, bronze) = count_nuggets(&prog.nuggets);
(Some(_), n) if (n as usize) > registry.len() => { log.push(LogEntry::Completed {
log.push(LogEntry::Completed); gold,
silver,
bronze,
});
(Screen::Completed, log) (Screen::Completed, log)
} }
(Some(tier), _) => { _ => {
log.push(LogEntry::TierChoice { chosen: tier_index(tier) }); log.push(level_prompt_entry(prog, registry));
log.push(level_prompt_entry(prog, registry, tier));
(Screen::Game, log) (Screen::Game, log)
} }
} }
} }
fn level_prompt_entry( fn level_prompt_entry(prog: &Progress, registry: &[Box<dyn Level>]) -> LogEntry {
prog: &Progress,
registry: &[Box<dyn Level>],
tier: Difficulty,
) -> LogEntry {
let idx = (prog.current_level - 1) as usize; let idx = (prog.current_level - 1) as usize;
let level = &registry[idx]; let level = &registry[idx];
let g = level.generate(prog.current_seed); let g = level.generate(prog.current_seed);
LogEntry::LevelPrompt { LogEntry::LevelPrompt {
level_id: level.id(), level_id: level.id(),
level_name: level.name().to_string(), level_name: level.name().to_string(),
tier_name: tier.name(),
flavor: g.flavor, flavor: g.flavor,
description: g.description, description: g.description,
} }
} }
fn count_nuggets(nuggets: &[(u8, Nugget)]) -> (u32, u32, u32) {
let mut gold = 0;
let mut silver = 0;
let mut bronze = 0;
for (_, n) in nuggets {
match n {
Nugget::Gold => gold += 1,
Nugget::Silver => silver += 1,
Nugget::Bronze => bronze += 1,
}
}
(gold, silver, bronze)
}
fn default_focus(screen: &Screen) -> Focus { fn default_focus(screen: &Screen) -> Focus {
match screen { match screen {
Screen::Game => Focus::Editor, Screen::Game => Focus::Editor,
@@ -326,7 +325,8 @@ fn main_loop(
let mut focus = default_focus(&screen); let mut focus = default_focus(&screen);
let mut editor = Editor::new(); let mut editor = Editor::new();
// If resume put a LevelPrompt at the bottom, typewriter it from scratch. // Resume case: if the bottom of the log is a LevelPrompt, animate it
// from scratch so resumed sessions feel the same as fresh ones.
let mut typewriter = if cfg.typewriter_enabled { let mut typewriter = if cfg.typewriter_enabled {
start_typewriter(&log, &cfg) start_typewriter(&log, &cfg)
} else { } else {
@@ -409,7 +409,7 @@ fn step(
) -> Result<bool> { ) -> Result<bool> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
// Global: Ctrl-Q quits. // Global: Ctrl-Q quits from anywhere.
if ctrl && key.code == KeyCode::Char('q') { if ctrl && key.code == KeyCode::Char('q') {
return Ok(true); return Ok(true);
} }
@@ -417,7 +417,7 @@ fn step(
// Global: Ctrl-X grades while in Game. // Global: Ctrl-X grades while in Game.
if ctrl && key.code == KeyCode::Char('x') && matches!(screen, Screen::Game) { if ctrl && key.code == KeyCode::Char('x') && matches!(screen, Screen::Game) {
grade(editor.text(), prog, registry, log)?; grade(editor.text(), prog, registry, log)?;
if matches!(log.entries.last(), Some(LogEntry::Completed)) { if matches!(log.entries.last(), Some(LogEntry::Completed { .. })) {
*screen = Screen::Completed; *screen = Screen::Completed;
*focus = default_focus(screen); *focus = default_focus(screen);
} }
@@ -468,10 +468,17 @@ fn step(
return Ok(false); return Ok(false);
} }
// Other screens (Welcome, TierSelect, ResetConfirm, Completed). // Other screens (Welcome, ResetConfirm, Completed).
match (&*screen, key.code) { match (&*screen, key.code) {
(Screen::Welcome, KeyCode::Enter) => { (Screen::Welcome, KeyCode::Enter) => {
*screen = Screen::TierSelect { cursor: 0 }; if prog.current_level == 0 {
prog.current_level = 1;
prog.current_seed = rand::random();
}
progress::save(prog)?;
editor.clear();
log.push(level_prompt_entry(prog, registry));
*screen = Screen::Game;
*focus = default_focus(screen); *focus = default_focus(screen);
} }
(Screen::Welcome, KeyCode::Char('q')) => return Ok(true), (Screen::Welcome, KeyCode::Char('q')) => return Ok(true),
@@ -480,37 +487,6 @@ fn step(
(Screen::Welcome, KeyCode::PageUp) => log.scroll_up(10), (Screen::Welcome, KeyCode::PageUp) => log.scroll_up(10),
(Screen::Welcome, KeyCode::PageDown) => log.scroll_down(10), (Screen::Welcome, KeyCode::PageDown) => log.scroll_down(10),
(Screen::TierSelect { cursor }, KeyCode::Up) => {
let c = cursor.saturating_sub(1);
*screen = Screen::TierSelect { cursor: c };
}
(Screen::TierSelect { cursor }, KeyCode::Down) => {
let c = (*cursor + 1).min(2);
*screen = Screen::TierSelect { cursor: c };
}
(Screen::TierSelect { cursor }, KeyCode::Enter) => {
let chosen = *cursor;
let tier = match chosen {
0 => Difficulty::Easy,
1 => Difficulty::Medium,
_ => Difficulty::Hard,
};
prog.tier = Some(tier);
if prog.current_level == 0 {
prog.current_level = 1;
prog.current_seed = rand::random();
}
progress::save(prog)?;
editor.clear();
log.push(LogEntry::TierChoice { chosen });
log.push(level_prompt_entry(prog, registry, tier));
*screen = Screen::Game;
*focus = default_focus(screen);
}
(Screen::TierSelect { .. }, KeyCode::Char('q')) => return Ok(true),
(Screen::TierSelect { .. }, KeyCode::PageUp) => log.scroll_up(10),
(Screen::TierSelect { .. }, KeyCode::PageDown) => log.scroll_down(10),
(Screen::ResetConfirm, KeyCode::Char('y')) => { (Screen::ResetConfirm, KeyCode::Char('y')) => {
let _ = std::fs::remove_file(progress::save_path()); let _ = std::fs::remove_file(progress::save_path());
*prog = Progress::default(); *prog = Progress::default();
@@ -529,11 +505,11 @@ fn step(
(Screen::Completed, KeyCode::Down) => log.scroll_down(1), (Screen::Completed, KeyCode::Down) => log.scroll_down(1),
(Screen::Completed, KeyCode::PageUp) => log.scroll_up(10), (Screen::Completed, KeyCode::PageUp) => log.scroll_up(10),
(Screen::Completed, KeyCode::PageDown) => log.scroll_down(10), (Screen::Completed, KeyCode::PageDown) => log.scroll_down(10),
(Screen::Completed, KeyCode::Char('q')) => return Ok(true),
(Screen::Completed, KeyCode::Char('r')) => { (Screen::Completed, KeyCode::Char('r')) => {
*screen = Screen::ResetConfirm; *screen = Screen::ResetConfirm;
*focus = default_focus(screen); *focus = default_focus(screen);
} }
(Screen::Completed, KeyCode::Char('q')) => return Ok(true),
_ => {} _ => {}
} }
@@ -559,38 +535,39 @@ fn grade(
let level = &registry[idx]; let level = &registry[idx];
let g = level.generate(prog.current_seed); let g = level.generate(prog.current_seed);
let score = similarity::semantic_or_textual(&g.target_yaml, &candidate); let score = similarity::semantic_or_textual(&g.target_yaml, &candidate);
let tier = prog.tier.expect("tier set before grading");
let threshold = tier.threshold();
let passed = score >= threshold;
let level_name = level.name().to_string(); let level_name = level.name().to_string();
let level_id = level.id(); let level_id = level.id();
if passed { match Nugget::from_score(score) {
prog.completed.push(level_id); Some(nugget) => {
prog.current_level += 1; prog.nuggets.push((level_id, nugget));
prog.current_seed = rand::random(); prog.current_level += 1;
progress::save(prog)?; prog.current_seed = rand::random();
log.push(LogEntry::ResultPass { progress::save(prog)?;
level_name, log.push(LogEntry::ResultPass {
score, level_name,
threshold, score,
}); nugget,
});
// Append the next LevelPrompt, or Completed if we ran out. // Append the next LevelPrompt, or Completed if we ran out.
let next_idx = (prog.current_level - 1) as usize; let next_idx = (prog.current_level - 1) as usize;
if next_idx >= registry.len() { if next_idx >= registry.len() {
log.push(LogEntry::Completed); let (gold, silver, bronze) = count_nuggets(&prog.nuggets);
} else { log.push(LogEntry::Completed {
log.push(level_prompt_entry(prog, registry, tier)); gold,
silver,
bronze,
});
} else {
log.push(level_prompt_entry(prog, registry));
}
}
None => {
prog.attempts += 1;
progress::save(prog)?;
log.push(LogEntry::ResultFail { level_name, score });
} }
} else {
prog.attempts += 1;
progress::save(prog)?;
log.push(LogEntry::ResultFail {
level_name,
score,
threshold,
});
} }
Ok(()) Ok(())
@@ -621,16 +598,7 @@ fn render(
let (left, right) = (chunks[0], chunks[1]); let (left, right) = (chunks[0], chunks[1]);
match screen { match screen {
Screen::Welcome => { Screen::Welcome => render_log(frame, left, log, None, typewriter),
render_log(frame, left, log, Some(&welcome_prompt_lines()), typewriter)
}
Screen::TierSelect { cursor } => render_log(
frame,
left,
log,
Some(&tier_picker_lines(*cursor)),
typewriter,
),
Screen::Game | Screen::Completed => render_log(frame, left, log, None, typewriter), Screen::Game | Screen::Completed => render_log(frame, left, log, None, typewriter),
Screen::ResetConfirm => render_reset_confirm(frame, left), Screen::ResetConfirm => render_reset_confirm(frame, left),
} }
@@ -655,9 +623,6 @@ fn status_text(screen: &Screen, focus: Focus) -> String {
(Screen::Welcome, _) => { (Screen::Welcome, _) => {
" [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [q] quit".into() " [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [q] quit".into()
} }
(Screen::TierSelect { .. }, _) => {
" [↑/↓] choose · [Enter] confirm · [PgUp/PgDn] scroll · [q] quit".into()
}
(Screen::Game, Focus::Editor) => { (Screen::Game, Focus::Editor) => {
" [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-Q] quit".into() " [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-Q] quit".into()
} }
@@ -677,28 +642,6 @@ fn screen_widget<'a>(title: String, body: String) -> Paragraph<'a> {
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
} }
fn welcome_prompt_lines() -> Vec<String> {
vec![
"🏰 You stand at the entrance, brave explorer. Press [Enter] to enter the dungeon.".to_string(),
]
}
fn tier_picker_lines(cursor: u8) -> Vec<String> {
let rows = [
("🥉", "Easy", "70 %", "forgive small slips"),
("🥈", "Medium", "80 %", "most details must match"),
("🥇", "Hard", "95 %", "only near-perfect passes"),
];
let mut v = vec!["🎚 Choose your tier:".to_string()];
for (i, (emoji, name, pct, hint)) in rows.iter().enumerate() {
let marker = if i == cursor as usize { "" } else { " " };
v.push(format!(" {marker} {emoji} {name:<7} ({pct}) {hint}"));
}
v.push(String::new());
v.push(" ↑/↓ choose · [Enter] confirm · [PgUp/PgDn] scroll · [q] quit".to_string());
v
}
fn render_reset_confirm(frame: &mut Frame, area: Rect) { fn render_reset_confirm(frame: &mut Frame, area: Rect) {
let body = "\n💀 Wipe progress and start over?\n\n[y] yes, wipe · any other key cancels" let body = "\n💀 Wipe progress and start over?\n\n[y] yes, wipe · any other key cancels"
.to_string(); .to_string();
@@ -726,8 +669,8 @@ fn render_log(
}; };
all_lines.extend(entry_view); all_lines.extend(entry_view);
} }
// Append the transient prompt (Welcome / TierSelect) as the bottom block. // Append the transient prompt (rare — none currently) as the bottom
// It's part of the bottom-anchored view but not part of the log itself. // block. Kept for future use.
if let Some(prompt) = trailing_prompt { if let Some(prompt) = trailing_prompt {
if !all_lines.is_empty() { if !all_lines.is_empty() {
all_lines.push(String::new()); all_lines.push(String::new());
@@ -770,11 +713,16 @@ fn truncate_entry_to_chars(entry: &LogEntry, max_chars: usize) -> Vec<String> {
fn entry_lines(entry: &LogEntry) -> Vec<String> { fn entry_lines(entry: &LogEntry) -> Vec<String> {
match entry { match entry {
LogEntry::Intro => vec![ LogEntry::Intro => vec![
"🏰 You stand at the entrance, brave explorer.".to_string(),
String::new(),
"🌀 YAMLabyrinth — learn YAML by writing your way through.".to_string(), "🌀 YAMLabyrinth — learn YAML by writing your way through.".to_string(),
String::new(), String::new(),
" Each chamber reveals a target. Match it with YAML in the editor".to_string(), " Each chamber reveals a target. Match it with YAML in the editor".to_string(),
" on the right, then press Ctrl-X to grade your attempt.".to_string(), " on the right, then press Ctrl-X to grade your attempt.".to_string(),
String::new(), String::new(),
" Score ≥ 95 % → 🥇 Gold · ≥ 80 % → 🥈 Silver · ≥ 70 % → 🥉 Bronze.".to_string(),
" Below 70 % means retry — refine your YAML and press Ctrl-X again.".to_string(),
String::new(),
" ⌨ Controls".to_string(), " ⌨ Controls".to_string(),
" [Tab] swap focus between log and editor".to_string(), " [Tab] swap focus between log and editor".to_string(),
" [Ctrl-X] grade your YAML".to_string(), " [Ctrl-X] grade your YAML".to_string(),
@@ -783,28 +731,14 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
" [r] reset progress".to_string(), " [r] reset progress".to_string(),
" [q] / [Ctrl-Q] quit".to_string(), " [q] / [Ctrl-Q] quit".to_string(),
], ],
LogEntry::TierChoice { chosen } => {
let rows = [
("🥉", "Easy", "70 %", "forgive small slips"),
("🥈", "Medium", "80 %", "most details must match"),
("🥇", "Hard", "95 %", "only near-perfect passes"),
];
let mut v = vec!["🎚 Difficulty".to_string(), String::new()];
for (i, (emoji, name, pct, hint)) in rows.iter().enumerate() {
let mark = if i == *chosen as usize { " ← chosen" } else { "" };
v.push(format!(" {emoji} {name:<7} ({pct}) {hint}{mark}"));
}
v
}
LogEntry::LevelPrompt { LogEntry::LevelPrompt {
level_id, level_id,
level_name, level_name,
tier_name,
flavor, flavor,
description, description,
} => { } => {
let mut v = vec![ let mut v = vec![
format!("🗺 Level {}{} ({})", level_id, level_name, tier_name), format!("🗺 Level {}{}", level_id, level_name),
flavor.clone(), flavor.clone(),
String::new(), String::new(),
]; ];
@@ -816,23 +750,19 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
LogEntry::ResultPass { LogEntry::ResultPass {
level_name, level_name,
score, score,
threshold, nugget,
} => vec![format!( } => vec![format!(
" 🗝 {} cleared at {:.0} % (threshold {:.0} %)", " {} {} cleared with a {} nugget at {:.0} % ✨",
nugget.emoji(),
level_name, level_name,
nugget.name(),
score * 100.0, score * 100.0,
threshold * 100.0
)], )],
LogEntry::ResultFail { LogEntry::ResultFail { level_name, score } => vec![
level_name,
score,
threshold,
} => vec![
format!( format!(
" 🕸 {} not yet — {:.0} % (threshold {:.0} %)", " 🕸 {} not yet — {:.0} % (need 70 % for a 🥉 Bronze)",
level_name, level_name,
score * 100.0, score * 100.0,
threshold * 100.0
), ),
" Refine your YAML and press Ctrl-X to retry.".to_string(), " Refine your YAML and press Ctrl-X to retry.".to_string(),
], ],
@@ -841,8 +771,13 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
format!(" {error}"), format!(" {error}"),
" Fix it in the editor and press Ctrl-X to retry.".to_string(), " Fix it in the editor and press Ctrl-X to retry.".to_string(),
], ],
LogEntry::Completed => vec![ LogEntry::Completed {
gold,
silver,
bronze,
} => vec![
" 🏆 Labyrinth complete!".to_string(), " 🏆 Labyrinth complete!".to_string(),
format!(" 🥇 ×{} 🥈 ×{} 🥉 ×{}", gold, silver, bronze),
" [r] reset and replay · [q] quit".to_string(), " [r] reset and replay · [q] quit".to_string(),
], ],
} }
@@ -877,7 +812,7 @@ fn render_editor(frame: &mut Frame, area: Rect, editor: &Editor, focused: bool)
} }
fn render_editor_inactive(frame: &mut Frame, area: Rect) { fn render_editor_inactive(frame: &mut Frame, area: Rect) {
let body = "\n 🪶 The quill rests. Pick a tier first…".to_string(); let body = "\n 🪶 The quill rests. Press [Enter] on the welcome screen to begin.".to_string();
frame.render_widget( frame.render_widget(
screen_widget(" 📝 Editor (inactive) ".to_string(), body), screen_widget(" 📝 Editor (inactive) ".to_string(), body),
area, area,