1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
|
//! The game loop and the scene stack.
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::io;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use ratatui::DefaultTerminal;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::dungeon::Pos;
use crate::dungeon::arms::Dmg;
use crate::dungeon::crawl::SavedCrawl;
use crate::items::{self, Gear, Item, Stack};
use crate::save::{self, Character, Record};
use crate::scenes::inventory::Sort;
use crate::scenes::{Scene, Transition, title::Title};
use crate::seed;
use crate::space::galaxy::{self, Galaxy};
use crate::space::system::{System, Wreck};
use crate::term::{self, KeyboardCaps};
use crate::trade::Rack;
/// Simulation rate. Real-time scenes (flight) move by this many steps a
/// second; turn-based scenes (dungeons) can ignore ticks and react to keys.
pub const TICKS_PER_SECOND: u32 = 30;
const TICK: Duration = Duration::from_nanos(1_000_000_000 / TICKS_PER_SECOND as u64);
/// After a stall (suspended with ^Z, a slow terminal), run at most this many
/// missed ticks, then carry on from now instead of fast-forwarding.
const MAX_CATCH_UP: u32 = 5;
/// Without key releases, a held key's auto-repeats look like presses. After
/// the scene changes, presses are dropped for as long as they keep coming
/// closer together than this, as auto-repeats do.
const REPEAT_GAP: Duration = Duration::from_millis(150);
/// State every scene can see.
pub struct Ctx {
/// The universe seed. Everything procedural derives from it (see `seed`).
pub seed: u64,
/// Who you're playing.
pub character: Character,
/// What they've done.
pub record: Record,
/// Ticks since the game started.
pub ticks: u64,
pub keyboard: KeyboardCaps,
/// How many derelicts you've boarded. Each boarding is a different ship,
/// `derive(seed, "derelict", n)`.
pub boarded: u64,
pub hold: Hold,
/// Wrecks you've boarded, by seed. There's nothing left aboard them.
pub stripped: HashSet<u64>,
/// Every star system, and the lanes between them.
pub galaxy: Galaxy,
/// Which of the galaxy's systems you're in: an index into `galaxy.sites`.
pub star: usize,
/// The systems you've been to. They, and the systems a lane joins to
/// them, are charted: you can plot a route through them.
pub visited: BTreeSet<usize>,
/// The star system you're in, generated whole.
pub system: System,
/// What to tell you when you next take the controls, after a journey
/// the map can describe better than the flight can.
pub arrival_note: Option<String>,
/// Days since the game began. It moves on only when you travel, and
/// everything in the system moves with it.
pub day: f64,
/// The day you set out for where you are now, while you're still
/// there. Wrecks that were calling then are still here, even if their
/// beacons have failed on the way: you came by their last fix. `None`
/// after a jump or a tow, which bring you somewhere you didn't choose.
pub departed: Option<f64>,
/// Where in the system you are: an index into `system.places`.
pub location: usize,
/// Whether your ship is set down at `location`, rather than flying.
pub landed: bool,
/// Better lenses fitted to the mining laser.
pub lenses: u32,
/// How each kind of monster takes each kind of damage, as far as you've
/// found out. Unlike what you carry, you keep this whatever happens.
pub bestiary: BTreeSet<(&'static str, Dmg)>,
/// The wreck you're aboard, as it stood after your last move, so a save
/// can bring you back to it. `None` when you're not in one.
pub crawl: Option<SavedCrawl>,
/// Armourers' racks you've traded at this fortnight, as you left them.
pub racks: Vec<Rack>,
/// How your inventory's lists are sorted.
pub inventory_sort: Sort,
/// The folder saves go in. Without one, nothing is saved.
pub saves: Option<PathBuf>,
/// This character's save file. There's none until you've made or
/// loaded a character, and nothing is saved until there is.
pub slot: Option<PathBuf>,
/// Why the last save failed, if it did.
pub save_error: Option<String>,
/// Keys held down right now. Only kept on terminals that report key
/// releases (`keyboard.key_release`); elsewhere there's no telling when
/// a key is let go, and this stays empty.
held: HashSet<KeyCode>,
/// Keys that were down when the scene last changed. They don't count as
/// held, and their auto-repeats are dropped, until they're let go: the
/// step that takes you out through an airlock shouldn't carry on
/// walking you about on the other side.
carried: HashSet<KeyCode>,
/// The same for terminals that don't report releases: while the scene
/// has only just changed, when the last key came.
settling: Option<Instant>,
}
impl Ctx {
pub fn new(seed: u64, keyboard: KeyboardCaps) -> Ctx {
let galaxy = galaxy::generate(seed);
let star = galaxy.home;
let system = galaxy.system(star);
// Someone to be until you make a character of your own.
let culture = system.culture;
let character = Character {
name: culture.person(&mut seed::rng(seed::derive(seed, "you", 0))),
origin: culture,
ship: culture.word(&mut seed::rng(seed::derive(seed, "your ship", 0))),
};
Ctx {
character,
record: Record::default(),
landed: false,
crawl: None,
racks: Vec::new(),
inventory_sort: Sort::default(),
saves: None,
slot: None,
save_error: None,
location: system.home_place(),
system,
galaxy,
star,
visited: BTreeSet::from([star]),
arrival_note: None,
day: 0.0,
departed: None,
lenses: 0,
bestiary: BTreeSet::new(),
seed,
ticks: 0,
keyboard,
boarded: 0,
hold: Hold::default(),
stripped: HashSet::new(),
held: HashSet::new(),
carried: HashSet::new(),
settling: None,
}
}
/// Starts a new game for `character` in universe `seed`, keeping only
/// what belongs to the terminal: the clock, the keyboard, where saves go.
/// The slot is cleared; whoever calls this decides where the new game
/// saves.
/// You start at the seat of your own people, on their homeworld.
pub fn begin(&mut self, seed: u64, character: Character) {
let mut fresh = Ctx::new(seed, self.keyboard);
let seat = fresh.galaxy.seat(character.origin);
if seat != fresh.star {
fresh.visited.clear();
fresh.enter_system(seat);
fresh.location = fresh.system.home_place();
}
fresh.character = character;
fresh.ticks = self.ticks;
fresh.saves = self.saves.take();
fresh.held = std::mem::take(&mut self.held);
fresh.carried = std::mem::take(&mut self.carried);
fresh.settling = self.settling;
*self = fresh;
}
/// Arrives in another system, at its edge, in flight.
pub fn enter_system(&mut self, star: usize) {
self.star = star;
self.system = self.galaxy.system(star);
self.visited.insert(star);
self.location = self.system.edge_place();
self.landed = false;
self.departed = None;
}
/// Flies to `place` in this system, `days` away.
pub fn travel(&mut self, place: usize, days: f64) {
self.departed = Some(self.day);
self.day += days;
self.location = place;
}
/// The wrecks you know of in this system that you haven't stripped:
/// those whose beacons are calling, and those here you came by a
/// failed beacon's last fix. Oldest first.
pub fn wrecks(&self) -> Vec<Wreck> {
let s = &self.system;
let mut wrecks = s.beacons(self.day, &self.stripped);
wrecks.retain(|w| w.place != self.location);
wrecks.extend(s.wrecks_at(self.location, self.day, self.departed, &self.stripped));
wrecks.sort_by(|a, b| a.arrived.total_cmp(&b.arrived));
wrecks
}
/// Whether you're out of fuel somewhere no one sells any.
pub fn stranded(&self) -> bool {
self.hold.fuel == 0 && !self.system.sells_fuel()
}
/// Saves the game, if there's a character to save.
pub fn autosave(&mut self) {
self.save_error = save::write(self).err().map(|e| format!("couldn't save: {e}"));
}
pub fn is_held(&self, code: KeyCode) -> bool {
let code = normalise(code);
self.held.contains(&code) && !self.carried.contains(&code)
}
/// Whether the top scene should get this key event. Keeps track of
/// which keys are held, too.
fn admit(&mut self, key: &KeyEvent, now: Instant) -> bool {
let code = normalise(key.code);
if self.keyboard.key_release {
// Scenes get presses and auto-repeats; releases only update
// which keys are held.
return match key.kind {
KeyEventKind::Release => {
self.held.remove(&code);
self.carried.remove(&code);
false
}
KeyEventKind::Repeat if self.carried.contains(&code) => false,
_ => {
self.held.insert(code);
self.carried.remove(&code);
true
}
};
}
match self.settling {
_ if key.kind == KeyEventKind::Release => false,
Some(last) if now.saturating_duration_since(last) < REPEAT_GAP => {
self.settling = Some(now);
false
}
_ => {
self.settling = None;
true
}
}
}
/// What's on screen changed under keys that are still down, without
/// the scene changing (a summary popped up over it): treat them as the
/// scene changing would, so a held key doesn't dismiss it unread.
pub fn settle(&mut self) {
self.scene_changed(Instant::now());
}
/// The top scene changed: keys still down were for the old one.
fn scene_changed(&mut self, now: Instant) {
self.carried.clone_from(&self.held);
if !self.keyboard.key_release {
self.settling = Some(now);
}
}
}
/// Letters count as the same key whatever the shift state, so releasing
/// shift mid-hold doesn't leave a key stuck down.
fn normalise(code: KeyCode) -> KeyCode {
match code {
KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()),
other => other,
}
}
/// Everything you own: money, ore and fuel, what you carry, and what's
/// stowed aboard your ship.
pub struct Hold {
/// Money, in ¤.
pub scrip: u32,
/// Tons of each kind of ore.
pub ore: BTreeMap<&'static str, u32>,
/// Tons of ore the hold can carry; cargo racks add more.
pub ore_capacity: u32,
/// What's on you: worn, ready and in your pack. It goes everywhere you
/// go, wrecks too, and if you don't get out of one, all but your own
/// torch and rivet gun stays there.
pub gear: Gear,
/// The ship's lockers, safe aboard whatever happens to you. Shops can
/// reach them, and send what you buy to them.
pub lockers: Vec<Stack>,
/// What you've put down on your ship's deck, and where.
pub deck: Vec<(Pos, Stack)>,
/// Jumps' worth of fuel in the tank. Each jump burns one, however long.
pub fuel: u32,
/// How many jumps' worth the tank holds; extra tanks add more.
pub fuel_capacity: u32,
/// What you owe, in ¤: a tow you couldn't pay for. What you earn pays it
/// off first.
pub debt: u32,
}
/// Where something you own is kept, for a shop.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kept {
Pack(usize),
Lockers(usize),
}
impl Kept {
/// "", or " (in your ship)".
pub fn note(self) -> &'static str {
match self {
Kept::Pack(_) => "",
Kept::Lockers(_) => " (in your ship)",
}
}
}
impl Default for Hold {
fn default() -> Hold {
Hold {
scrip: 0,
ore: BTreeMap::new(),
ore_capacity: Hold::BASE_ORE_CAPACITY,
gear: Gear::default(),
lockers: Vec::new(),
deck: Vec::new(),
fuel: Hold::BASE_FUEL,
fuel_capacity: Hold::BASE_FUEL,
debt: 0,
}
}
}
impl Hold {
/// Tons of ore the hold carries before any racks are fitted.
pub const BASE_ORE_CAPACITY: u32 = 40;
/// Jumps the tank holds before any extra tanks are fitted.
pub const BASE_FUEL: u32 = 4;
/// Takes in money, paying off any debt first. Returns how much went to
/// the debt.
pub fn earn(&mut self, amount: u32) -> u32 {
let paid = amount.min(self.debt);
self.debt -= paid;
self.scrip += amount - paid;
paid
}
/// Pays a fee from what you have, and owes the rest. Returns how much
/// is owed.
pub fn charge(&mut self, fee: u32) -> u32 {
let paid = fee.min(self.scrip);
self.scrip -= paid;
self.debt += fee - paid;
fee - paid
}
pub fn ore_tons(&self) -> u32 {
self.ore.values().sum()
}
/// Everything a shop can reach: what's in your pack, then what's in
/// your lockers.
pub fn kept(&self) -> impl Iterator<Item = (Kept, &Stack)> {
let pack = self.gear.pack.iter().enumerate().map(|(i, s)| (Kept::Pack(i), s));
pack.chain(self.lockers.iter().enumerate().map(|(i, s)| (Kept::Lockers(i), s)))
}
pub fn kept_mut(&mut self, at: Kept) -> Option<&mut Stack> {
match at {
Kept::Pack(i) => self.gear.pack.get_mut(i),
Kept::Lockers(i) => self.lockers.get_mut(i),
}
}
/// Takes up to `count` of what's kept `at`.
pub fn take_kept(&mut self, at: Kept, count: u32) -> Stack {
match at {
Kept::Pack(i) => items::take(&mut self.gear.pack, i, count),
Kept::Lockers(i) => items::take(&mut self.lockers, i, count),
}
}
/// You lose everything you were `carrying` but your own torch and rivet
/// gun. If you'd left one in the lockers, you take it out again.
pub fn lose_carried(&mut self, carrying: &Gear) {
let lockers = &mut self.lockers;
self.gear = carrying.stripped(|gun| {
let own = |s: &Stack| matches!(&s.item, Item::Weapon(w) if w.kit && w.is_gun() == gun);
let i = lockers.iter().position(own)?;
match items::take(lockers, i, 1).item {
Item::Weapon(w) => Some(w),
_ => None,
}
});
}
/// How many artifacts you have, on you and aboard.
pub fn artifacts(&self) -> u32 {
self.kept().filter(|(_, s)| matches!(s.item, Item::Artifact(_))).map(|(_, s)| s.count).sum()
}
/// How many medpatches you have, on you and aboard.
pub fn medpatches(&self) -> u32 {
self.kept().filter(|(_, s)| s.item == Item::Medpatch).map(|(_, s)| s.count).sum()
}
/// Puts something you've bought in your pack if there's room, and in
/// your lockers if not. Says whether it's on you.
pub fn deliver(&mut self, stack: Stack) -> bool {
if self.gear.fits(&stack.item) >= stack.count {
self.gear.add(stack);
true
} else {
items::stow(&mut self.lockers, stack);
false
}
}
pub fn locker_mass(&self) -> u32 {
items::pile_mass(&self.lockers)
}
pub fn ore_room(&self) -> u32 {
self.ore_capacity.saturating_sub(self.ore_tons())
}
}
/// Plays. Given a universe, starts a new character in it; otherwise picks up
/// the character you last played, if there is one.
pub fn run(universe: Option<u64>) -> io::Result<()> {
let (mut terminal, keyboard) = term::init()?;
let mut ctx = Ctx::new(universe.unwrap_or_else(seed::from_clock), keyboard);
ctx.saves = save::default_dir();
if universe.is_none()
&& let Some(dir) = &ctx.saves
{
let last = save::list(dir).into_iter().find(|s| s.summary.is_ok());
if let Some(slot) = last {
let _ = save::load(&slot.path, &mut ctx);
}
}
let result = run_loop(&mut terminal, &mut ctx);
ctx.autosave();
term::restore(keyboard);
result
}
/// After the stack changes: the new top scene catches up, and the game is
/// saved.
fn changed(stack: &mut [Box<dyn Scene>], ctx: &mut Ctx) {
ctx.scene_changed(Instant::now());
if let Some(top) = stack.last_mut() {
top.on_top(ctx);
}
ctx.autosave();
}
fn run_loop(terminal: &mut DefaultTerminal, ctx: &mut Ctx) -> io::Result<()> {
let mut stack: Vec<Box<dyn Scene>> = vec![Box::new(Title::new(ctx))];
let mut next_tick = Instant::now() + TICK;
while let Some(top) = stack.last() {
terminal.draw(|frame| top.render(frame, ctx))?;
// Wait for input until the next tick is due, then take whatever else
// is already queued without waiting again.
let mut timeout = next_tick.saturating_duration_since(Instant::now());
while event::poll(timeout)? {
timeout = Duration::ZERO;
let Event::Key(key) = event::read()? else {
continue;
};
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
return Ok(());
}
if !ctx.admit(&key, Instant::now()) {
continue;
}
let Some(top) = stack.last_mut() else { break };
let transition = top.key(key, ctx);
if apply(&mut stack, transition) {
changed(&mut stack, ctx);
}
}
let mut caught_up = 0;
while Instant::now() >= next_tick {
let Some(top) = stack.last_mut() else { break };
ctx.ticks += 1;
let transition = top.tick(ctx);
if apply(&mut stack, transition) {
changed(&mut stack, ctx);
}
next_tick += TICK;
caught_up += 1;
if caught_up == MAX_CATCH_UP {
next_tick = Instant::now() + TICK;
}
}
}
Ok(())
}
/// Changes the scene stack, and says whether it did.
fn apply(stack: &mut Vec<Box<dyn Scene>>, transition: Transition) -> bool {
match transition {
Transition::None => return false,
Transition::Push(scene) => stack.push(scene),
Transition::Open(scenes) => stack.extend(scenes),
Transition::Pop => {
stack.pop();
}
Transition::Replace(scenes) => {
stack.pop();
stack.extend(scenes);
}
Transition::Title => stack.truncate(1),
Transition::Quit => stack.clear(),
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn event(code: KeyCode, kind: KeyEventKind) -> KeyEvent {
KeyEvent::new_with_kind(code, KeyModifiers::NONE, kind)
}
#[test]
fn going_to_the_title_closes_everything_above_it() {
let ctx = Ctx::new(1, KeyboardCaps { key_release: false });
let mut stack: Vec<Box<dyn Scene>> = vec![Box::new(Title::new(&ctx))];
assert!(apply(&mut stack, Transition::Open(crate::scenes::flight::resume(&ctx))));
assert!(stack.len() > 1);
assert!(apply(&mut stack, Transition::Title));
assert_eq!(stack.len(), 1);
}
#[test]
fn keys_held_through_a_change_of_scene_stay_behind() {
let mut ctx = Ctx::new(1, KeyboardCaps { key_release: true });
let now = Instant::now();
assert!(ctx.admit(&event(KeyCode::Left, KeyEventKind::Press), now));
assert!(ctx.is_held(KeyCode::Left));
ctx.scene_changed(now);
assert!(!ctx.is_held(KeyCode::Left));
assert!(!ctx.admit(&event(KeyCode::Left, KeyEventKind::Repeat), now));
assert!(ctx.admit(&event(KeyCode::Right, KeyEventKind::Press), now), "others get through");
assert!(!ctx.admit(&event(KeyCode::Left, KeyEventKind::Release), now));
assert!(ctx.admit(&event(KeyCode::Left, KeyEventKind::Press), now));
assert!(ctx.is_held(KeyCode::Left));
}
#[test]
fn without_key_releases_auto_repeats_stay_behind() {
let mut ctx = Ctx::new(1, KeyboardCaps { key_release: false });
let start = Instant::now();
let at = |ms| start + Duration::from_millis(ms);
let left = event(KeyCode::Left, KeyEventKind::Press);
ctx.scene_changed(start);
assert!(!ctx.admit(&left, at(30)));
assert!(!ctx.admit(&left, at(60)));
assert!(!ctx.admit(&left, at(150)));
assert!(ctx.admit(&left, at(400)), "a press after a pause is a new one");
assert!(ctx.admit(&left, at(420)), "and from then on they all count");
}
}
|