Replace tiers with achievements
This commit is contained in:
@@ -15,37 +15,47 @@ pub mod l02_kv;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Game-wide difficulty. Chosen once on the TierSelect screen; persisted
|
||||
/// in `Progress.tier`. Maps to a fixed passing threshold per level.
|
||||
/// Awarded per level based on the grade score. Replaces the old
|
||||
/// game-wide difficulty tier. Thresholds:
|
||||
/// - Gold ≥ 95 %
|
||||
/// - Silver ≥ 80 %
|
||||
/// - Bronze ≥ 70 %
|
||||
/// - Below 70 % → no nugget (retry).
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum Difficulty {
|
||||
Easy,
|
||||
Medium,
|
||||
Hard,
|
||||
pub enum Nugget {
|
||||
Bronze,
|
||||
Silver,
|
||||
Gold,
|
||||
}
|
||||
|
||||
impl Difficulty {
|
||||
pub fn threshold(self) -> f64 {
|
||||
match self {
|
||||
Self::Easy => 0.70,
|
||||
Self::Medium => 0.80,
|
||||
Self::Hard => 0.95,
|
||||
impl Nugget {
|
||||
/// Map a grade ratio (0.0..=1.0) to a nugget. `None` means the
|
||||
/// player didn't clear the level — retry without advancing.
|
||||
pub fn from_score(score: f64) -> Option<Self> {
|
||||
if score >= 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 {
|
||||
Self::Easy => "Easy (70%)",
|
||||
Self::Medium => "Medium (80%)",
|
||||
Self::Hard => "Hard (95%)",
|
||||
Self::Bronze => "🥉",
|
||||
Self::Silver => "🥈",
|
||||
Self::Gold => "🥇",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Easy => "Easy",
|
||||
Self::Medium => "Medium",
|
||||
Self::Hard => "Hard",
|
||||
Self::Bronze => "Bronze",
|
||||
Self::Silver => "Silver",
|
||||
Self::Gold => "Gold",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +69,14 @@ mod smoke {
|
||||
// (4) Progress round-trips through the same YAML pipeline that disk
|
||||
// save/load will use.
|
||||
let p = progress::Progress {
|
||||
tier: Some(levels::Difficulty::Medium),
|
||||
completed: vec![1, 2],
|
||||
nuggets: vec![(1, levels::Nugget::Silver), (2, levels::Nugget::Gold)],
|
||||
current_level: 3,
|
||||
current_seed: 0xCAFE,
|
||||
attempts: 1,
|
||||
};
|
||||
let s = serde_yaml::to_string(&p).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_seed, p.current_seed);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::levels::Difficulty;
|
||||
use crate::levels::Nugget;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Progress {
|
||||
pub tier: Option<Difficulty>,
|
||||
pub completed: Vec<u8>,
|
||||
pub current_level: u8,
|
||||
/// Nuggets awarded per level: `(level_id, nugget)` in order of
|
||||
/// completion. A level appears at most once (re-runs after reset
|
||||
/// 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 attempts: u32,
|
||||
}
|
||||
|
||||
241
src/tui.rs
241
src/tui.rs
@@ -13,7 +13,7 @@ use std::io::{stdout, Stdout};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::config;
|
||||
use crate::levels::{self, Difficulty, Level};
|
||||
use crate::levels::{self, Level, Nugget};
|
||||
use crate::progress::{self, Progress};
|
||||
use crate::similarity;
|
||||
|
||||
@@ -21,7 +21,6 @@ use crate::similarity;
|
||||
|
||||
enum Screen {
|
||||
Welcome,
|
||||
TierSelect { cursor: u8 },
|
||||
/// Active play: left column = HistoryLog, right column = Editor.
|
||||
Game,
|
||||
ResetConfirm,
|
||||
@@ -35,34 +34,31 @@ enum Focus {
|
||||
}
|
||||
|
||||
enum LogEntry {
|
||||
/// Game rules + the full controls listing. Appended once on
|
||||
/// Welcome → TierSelect so the player can scroll back to it.
|
||||
/// Game rules + the full controls listing. Seeded at startup.
|
||||
Intro,
|
||||
/// The full tier table with a marker on the chosen one.
|
||||
TierChoice {
|
||||
chosen: u8, // 0=Easy, 1=Medium, 2=Hard
|
||||
},
|
||||
LevelPrompt {
|
||||
level_id: u8,
|
||||
level_name: String,
|
||||
tier_name: &'static str,
|
||||
flavor: String,
|
||||
description: String,
|
||||
},
|
||||
ResultPass {
|
||||
level_name: String,
|
||||
score: f64,
|
||||
threshold: f64,
|
||||
nugget: Nugget,
|
||||
},
|
||||
ResultFail {
|
||||
level_name: String,
|
||||
score: f64,
|
||||
threshold: f64,
|
||||
},
|
||||
InvalidYaml {
|
||||
error: String,
|
||||
},
|
||||
Completed,
|
||||
Completed {
|
||||
gold: u32,
|
||||
silver: u32,
|
||||
bronze: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Append-only chronological log of game events. Rendered bottom-anchored
|
||||
@@ -259,54 +255,57 @@ fn leave() -> Result<()> {
|
||||
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) {
|
||||
let mut log = HistoryLog::new();
|
||||
// The Intro entry is always the bottom of the log on startup so that
|
||||
// Welcome and TierSelect can render it inline (the player can scroll
|
||||
// back to it any time).
|
||||
// The Intro entry is always at the bottom of the log on startup so
|
||||
// Welcome can show it inline; the player can scroll back to it any
|
||||
// time during play.
|
||||
log.push(LogEntry::Intro);
|
||||
|
||||
match (prog.tier, prog.current_level) {
|
||||
(None, 0) => (Screen::Welcome, log),
|
||||
(None, _) => (Screen::TierSelect { cursor: 0 }, log),
|
||||
(Some(tier), 0) => (Screen::TierSelect { cursor: tier_index(tier) }, log),
|
||||
(Some(_), n) if (n as usize) > registry.len() => {
|
||||
log.push(LogEntry::Completed);
|
||||
match prog.current_level {
|
||||
0 => (Screen::Welcome, log),
|
||||
n if (n as usize) > registry.len() => {
|
||||
let (gold, silver, bronze) = count_nuggets(&prog.nuggets);
|
||||
log.push(LogEntry::Completed {
|
||||
gold,
|
||||
silver,
|
||||
bronze,
|
||||
});
|
||||
(Screen::Completed, log)
|
||||
}
|
||||
(Some(tier), _) => {
|
||||
log.push(LogEntry::TierChoice { chosen: tier_index(tier) });
|
||||
log.push(level_prompt_entry(prog, registry, tier));
|
||||
_ => {
|
||||
log.push(level_prompt_entry(prog, registry));
|
||||
(Screen::Game, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn level_prompt_entry(
|
||||
prog: &Progress,
|
||||
registry: &[Box<dyn Level>],
|
||||
tier: Difficulty,
|
||||
) -> LogEntry {
|
||||
fn level_prompt_entry(prog: &Progress, registry: &[Box<dyn Level>]) -> LogEntry {
|
||||
let idx = (prog.current_level - 1) as usize;
|
||||
let level = ®istry[idx];
|
||||
let g = level.generate(prog.current_seed);
|
||||
LogEntry::LevelPrompt {
|
||||
level_id: level.id(),
|
||||
level_name: level.name().to_string(),
|
||||
tier_name: tier.name(),
|
||||
flavor: g.flavor,
|
||||
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 {
|
||||
match screen {
|
||||
Screen::Game => Focus::Editor,
|
||||
@@ -326,7 +325,8 @@ fn main_loop(
|
||||
let mut focus = default_focus(&screen);
|
||||
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 {
|
||||
start_typewriter(&log, &cfg)
|
||||
} else {
|
||||
@@ -409,7 +409,7 @@ fn step(
|
||||
) -> Result<bool> {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
// Global: Ctrl-Q quits.
|
||||
// Global: Ctrl-Q quits from anywhere.
|
||||
if ctrl && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -417,7 +417,7 @@ fn step(
|
||||
// Global: Ctrl-X grades while in Game.
|
||||
if ctrl && key.code == KeyCode::Char('x') && matches!(screen, Screen::Game) {
|
||||
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;
|
||||
*focus = default_focus(screen);
|
||||
}
|
||||
@@ -468,10 +468,17 @@ fn step(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Other screens (Welcome, TierSelect, ResetConfirm, Completed).
|
||||
// Other screens (Welcome, ResetConfirm, Completed).
|
||||
match (&*screen, key.code) {
|
||||
(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);
|
||||
}
|
||||
(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::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')) => {
|
||||
let _ = std::fs::remove_file(progress::save_path());
|
||||
*prog = Progress::default();
|
||||
@@ -529,11 +505,11 @@ fn step(
|
||||
(Screen::Completed, KeyCode::Down) => log.scroll_down(1),
|
||||
(Screen::Completed, KeyCode::PageUp) => log.scroll_up(10),
|
||||
(Screen::Completed, KeyCode::PageDown) => log.scroll_down(10),
|
||||
(Screen::Completed, KeyCode::Char('q')) => return Ok(true),
|
||||
(Screen::Completed, KeyCode::Char('r')) => {
|
||||
*screen = Screen::ResetConfirm;
|
||||
*focus = default_focus(screen);
|
||||
}
|
||||
(Screen::Completed, KeyCode::Char('q')) => return Ok(true),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
@@ -559,38 +535,39 @@ fn grade(
|
||||
let level = ®istry[idx];
|
||||
let g = level.generate(prog.current_seed);
|
||||
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_id = level.id();
|
||||
|
||||
if passed {
|
||||
prog.completed.push(level_id);
|
||||
match Nugget::from_score(score) {
|
||||
Some(nugget) => {
|
||||
prog.nuggets.push((level_id, nugget));
|
||||
prog.current_level += 1;
|
||||
prog.current_seed = rand::random();
|
||||
progress::save(prog)?;
|
||||
log.push(LogEntry::ResultPass {
|
||||
level_name,
|
||||
score,
|
||||
threshold,
|
||||
nugget,
|
||||
});
|
||||
|
||||
// Append the next LevelPrompt, or Completed if we ran out.
|
||||
let next_idx = (prog.current_level - 1) as usize;
|
||||
if next_idx >= registry.len() {
|
||||
log.push(LogEntry::Completed);
|
||||
let (gold, silver, bronze) = count_nuggets(&prog.nuggets);
|
||||
log.push(LogEntry::Completed {
|
||||
gold,
|
||||
silver,
|
||||
bronze,
|
||||
});
|
||||
} else {
|
||||
log.push(level_prompt_entry(prog, registry, tier));
|
||||
log.push(level_prompt_entry(prog, registry));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
None => {
|
||||
prog.attempts += 1;
|
||||
progress::save(prog)?;
|
||||
log.push(LogEntry::ResultFail {
|
||||
level_name,
|
||||
score,
|
||||
threshold,
|
||||
});
|
||||
log.push(LogEntry::ResultFail { level_name, score });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -621,16 +598,7 @@ fn render(
|
||||
let (left, right) = (chunks[0], chunks[1]);
|
||||
|
||||
match screen {
|
||||
Screen::Welcome => {
|
||||
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::Welcome => 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),
|
||||
}
|
||||
@@ -655,9 +623,6 @@ fn status_text(screen: &Screen, focus: Focus) -> String {
|
||||
(Screen::Welcome, _) => {
|
||||
" [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [q] quit".into()
|
||||
}
|
||||
(Screen::TierSelect { .. }, _) => {
|
||||
" [↑/↓] choose · [Enter] confirm · [PgUp/PgDn] scroll · [q] quit".into()
|
||||
}
|
||||
(Screen::Game, Focus::Editor) => {
|
||||
" [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 })
|
||||
}
|
||||
|
||||
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) {
|
||||
let body = "\n💀 Wipe progress and start over?\n\n[y] yes, wipe · any other key cancels"
|
||||
.to_string();
|
||||
@@ -726,8 +669,8 @@ fn render_log(
|
||||
};
|
||||
all_lines.extend(entry_view);
|
||||
}
|
||||
// Append the transient prompt (Welcome / TierSelect) as the bottom block.
|
||||
// It's part of the bottom-anchored view but not part of the log itself.
|
||||
// Append the transient prompt (rare — none currently) as the bottom
|
||||
// block. Kept for future use.
|
||||
if let Some(prompt) = trailing_prompt {
|
||||
if !all_lines.is_empty() {
|
||||
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> {
|
||||
match entry {
|
||||
LogEntry::Intro => vec![
|
||||
"🏰 You stand at the entrance, brave explorer.".to_string(),
|
||||
String::new(),
|
||||
"🌀 YAMLabyrinth — learn YAML by writing your way through.".to_string(),
|
||||
String::new(),
|
||||
" 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(),
|
||||
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(),
|
||||
" [Tab] swap focus between log and editor".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(),
|
||||
" [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 {
|
||||
level_id,
|
||||
level_name,
|
||||
tier_name,
|
||||
flavor,
|
||||
description,
|
||||
} => {
|
||||
let mut v = vec![
|
||||
format!("🗺 Level {} — {} ({})", level_id, level_name, tier_name),
|
||||
format!("🗺 Level {} — {}", level_id, level_name),
|
||||
flavor.clone(),
|
||||
String::new(),
|
||||
];
|
||||
@@ -816,23 +750,19 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
|
||||
LogEntry::ResultPass {
|
||||
level_name,
|
||||
score,
|
||||
threshold,
|
||||
nugget,
|
||||
} => vec![format!(
|
||||
" 🗝 {} cleared at {:.0} % (threshold {:.0} %) ✨",
|
||||
" {} {} cleared with a {} nugget at {:.0} % ✨",
|
||||
nugget.emoji(),
|
||||
level_name,
|
||||
nugget.name(),
|
||||
score * 100.0,
|
||||
threshold * 100.0
|
||||
)],
|
||||
LogEntry::ResultFail {
|
||||
level_name,
|
||||
score,
|
||||
threshold,
|
||||
} => vec![
|
||||
LogEntry::ResultFail { level_name, score } => vec![
|
||||
format!(
|
||||
" 🕸 {} not yet — {:.0} % (threshold {:.0} %)",
|
||||
" 🕸 {} not yet — {:.0} % (need 70 % for a 🥉 Bronze)",
|
||||
level_name,
|
||||
score * 100.0,
|
||||
threshold * 100.0
|
||||
),
|
||||
" Refine your YAML and press Ctrl-X to retry.".to_string(),
|
||||
],
|
||||
@@ -841,8 +771,13 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
|
||||
format!(" {error}"),
|
||||
" 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(),
|
||||
format!(" 🥇 ×{} 🥈 ×{} 🥉 ×{}", gold, silver, bronze),
|
||||
" [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) {
|
||||
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(
|
||||
screen_widget(" 📝 Editor (inactive) ".to_string(), body),
|
||||
area,
|
||||
|
||||
Reference in New Issue
Block a user