1#![allow(clippy::octal_escapes)] use stakker::{ActorOwn, CX, Core, Ret, Stakker, StopCause};
6use stakker::{actor, after, fwd_to, ret, ret_shutdown, ret_some_to, stop};
7use stakker_mio::MioPoll;
8use stakker_mio::mio::{Events, Poll};
9use stakker_tui::{Key, Region, TermShare, Terminal, Tile, sizer::SimpleSizer};
10use std::io::Write;
11use std::path::PathBuf;
12use std::time::{Duration, Instant};
13
14mod cardgame;
15mod draw;
16pub use cardgame::{Card, Hand, Rand};
17use draw::CARD_SY;
18
19fn main() -> std::io::Result<()> {
22 let mut stakker = Stakker::new(Instant::now());
23 let s = &mut stakker;
24 let miopoll = MioPoll::new(s, Poll::new()?, Events::with_capacity(1024), 0)?;
25
26 let _app = actor!(s, App::init(), ret_shutdown!(s));
27
28 const MAXDELAY: Duration = Duration::from_secs(60);
29 let mut idle_pending = s.run(Instant::now(), false);
30 let mut io_pending = false;
31 let mut activity;
32 while s.not_shutdown() {
33 let maxdur = s.next_wait_max(Instant::now(), MAXDELAY, idle_pending || io_pending);
34 (activity, io_pending) = miopoll.poll(maxdur)?;
35 idle_pending = s.run(Instant::now(), !activity);
36 }
37 if let Some(reason) = s.shutdown_reason()
38 && reason.has_error()
39 {
40 println!("{reason}");
41 }
42
43 Ok(())
44}
45
46pub struct App {
48 _terminal: ActorOwn<Terminal>,
49 tsh: Option<TermShare>,
50 logo_hfb: Option<Vec<u16>>, game: Option<Game>,
52 scores_tile: Tile,
53 scores: Vec<i32>,
54 score: Option<i32>,
55 last_score: Option<i32>,
56 adjust: i32,
57}
58
59impl App {
60 fn init(cx: CX![]) -> Option<Self> {
61 let _terminal = actor!(
62 cx,
63 Terminal::init(
64 SimpleSizer::new(),
65 fwd_to!([cx], resize() as (Option<TermShare>)),
66 fwd_to!([cx], input() as (Key))
67 ),
68 ret_some_to!([cx], |_, cx, cause: StopCause| {
69 println!("Terminal actor failed: {cause}");
70 stop!(cx);
71 })
72 );
73
74 let mut this = Self {
75 _terminal,
76 tsh: None,
77 logo_hfb: None,
78 game: None,
79 scores_tile: Default::default(),
80 scores: Vec::new(),
81 score: None,
82 last_score: None,
83 adjust: 0,
84 };
85 let _ = this.load_scores();
86 Some(this)
87 }
88
89 fn resize(&mut self, cx: CX![], tsh: Option<TermShare>) {
90 self.tsh = tsh;
91
92 if let Some(ref tsh) = self.tsh {
93 let out = tsh.output(cx);
94 out.attr_99().cursor_show().scroll_up().save_cleanup();
95 out.cursor_hide().clear_all_99();
96
97 if self.logo_hfb.is_none() && out.features().colour_256 {
98 const LOGO_RGB: [(f64, u32); 7] = [
99 (0.0, 0xFFFF00),
100 (1.0, 0x00FF00),
101 (2.0, 0x22FFFF),
102 (3.0, 0x4444FF),
103 (4.0, 0xFF22FF),
104 (5.0, 0xFF0000),
105 (6.0, 0xFFFF00),
106 ];
107 self.logo_hfb = Some(
108 (0..30)
109 .map(|i| {
110 let rgb = out.rgb_interpolate(&LOGO_RGB, i as f64 * 0.2);
111 out.hfb_alloc(false, false, rgb, 0x000000)
112 })
113 .collect(),
114 );
115 }
116
117 self.layout(cx);
118 }
119 }
120
121 fn redraw(&mut self, cx: CX![]) {
123 let Some(ref tsh) = self.tsh else {
124 return;
125 };
126 let mut tile = tsh.tile(cx);
127 if let Some(mut r) = tile.full(cx) {
128 r.clear_all_99();
129 }
130 after!(Duration::from_millis(10), [cx], layout());
131 }
132
133 fn layout(&mut self, cx: CX![]) {
134 let Some(ref tsh) = self.tsh else {
135 return;
136 };
137
138 let mut tile = tsh.tile(cx);
139 if let Some(mut r) = tile.full(cx) {
140 r.clear_all_99();
141 }
142 let left_gap = 0.max(tile.sx() - 80) / 2;
143 let right_off = (left_gap + 80).min(tile.sx());
144 let right_gap = tile.sx() - right_off;
145 self.adjust = self.adjust.max(-left_gap).min(right_gap);
146 let left_gap = left_gap + self.adjust;
147 let right_off = (left_gap + 80).min(tile.sx());
148
149 let (_, mid, _) = tile.split_xx(left_gap, right_off);
150 let (main, scores) = mid.split_x(74);
151 self.scores_tile = scores;
152 let sy = main.sy();
153 if let Some(game) = &mut self.game {
154 game.layout(cx, main);
155 } else {
156 let logo_sy = 9;
157 let instr_sy = 16;
158 let gap = (sy - logo_sy - instr_sy) / 3;
159 let (_, mut logo, rest) = main.split_yy(gap, gap + logo_sy);
160 let (_, mut text, _) = rest.split_yy(gap, gap + instr_sy);
161 if let Some(r) = logo.full(cx) {
162 self.draw_logo(r);
163 }
164 if let Some(r) = text.full(cx) {
165 Self::draw_instructions(r);
166 }
167 }
168
169 self.draw_scores(cx);
170 }
171
172 fn draw_scores(&mut self, cx: CX![]) {
173 let mut score = self.score;
174 let mut last_score = self.last_score;
175 if let Some(mut r) = self.scores_tile.full(cx) {
176 r.hfb(176).clear_all();
177 r.hfb(7).text("SCORES");
178 let mut y = 1;
179 let last_y = r.sy() - 1;
180 let mut draw = move |hfb, sc| {
181 if y > 0 {
182 r.at(y, 0).hfb(hfb);
183 write!(r, "{:3}:{:02}", sc / 60, sc % 60).unwrap();
184 y += 1;
185 }
186 };
187
188 for &s0 in &self.scores {
189 if let Some(curr) = score
190 && (curr < s0 || y == last_y)
191 {
192 draw(7, curr);
193 score = None;
194 }
195 if Some(s0) == last_score {
196 draw(173, s0);
197 last_score = None;
198 } else {
199 draw(176, s0);
200 }
201 }
202 if let Some(s1) = score {
203 draw(173, s1);
204 }
205 }
206 }
207
208 fn draw_logo(&self, mut r: Region<'_>) {
209 const LOGO_HFB: [u16; 30] = [
210 160, 160, 160, 140, 140, 140, 140, 140, 150, 150, 150, 150, 150, 110, 110, 110, 110,
211 110, 130, 130, 130, 130, 130, 120, 120, 120, 120, 120, 160, 160,
212 ];
213 const LOGO: [&str; 9] = [
214 r" ___________________________________ ",
215 r" / ____ ____________________________\ ",
216 r"/ / / / __ __ __ ",
217 r"\ \ / / ___ ___ _____/ /_ __/ /_ \ \ ",
218 r" \/ / / / _ \/ _ \/ ___/ / / / / __ \ \ \ ",
219 r" / / / __/ __/ /__/ / /_/ / /_/ / \ \ ",
220 r" \_\ \___/\___/\___/_/\__,_/_.___/ /_ \ \",
221 r" ___________________________________/ /",
222 r" \___________________________________/ ",
223 ];
224 let logo_hfb = match self.logo_hfb.as_ref() {
225 Some(v) => v.as_slice(),
226 None => LOGO_HFB.as_slice(),
227 };
228 let ox = (r.sx() - 43) / 2;
229 let oy = (r.sy() - 9) / 2;
230 for (y, s) in LOGO.iter().enumerate() {
231 r.at(oy + y as i32, ox);
232 for (x, c) in s.chars().enumerate() {
233 r.hfb(logo_hfb[(10 + x + y) % 30]);
234 r.char(c);
235 }
236 }
237 }
238
239 fn draw_instructions(mut r: Region<'_>) {
240 const TEXT: &str = r"
241
242 Keys: [N] New game; [Q] Quit; [1] to [9] Select cards in
243 corresponding column; [+]/[=] or [-] Increase or decrease
244 number of selected cards; [1] to [9] If the same digit as the
245 selecting keypress, move selected cards to top, if another
246 digit, move as many of selected cards as possible to that
247 column; [Space] Bring a new card down from the stock pile;
248 [BackSp] Go back one move; [<]/[>] Adjust display left/right.
249
250 Rules: The aim is to move all the cards to the top area, the
251 spaces the the right of the stock pile. Each top pile must
252 consist of one suit only, stacked in order from Ace up to
253 King. The cards in the main area can be moved around:
254 sequences of one or more cards of the same suit may be moved
255 on top of a card with the next-higher number, of any suit.
256 The bulk of the cards are in the stock pile in the top-left.
257 Cards may be brought down from there using Space.
258
259 ";
260 let lines: Vec<_> = TEXT.trim().split('\n').map(|s| s.to_string()).collect();
261 let n_lines = lines.len();
262 let oy = (r.sy() - n_lines as i32) / 2;
263 for (i, line) in lines.iter().enumerate() {
264 r.at(i as i32 + oy, 7);
265 let line = line.trim().replace("[", "\0171 ").replace("]", " \0099");
266 r.text(&line);
267 }
268 }
269
270 fn input(&mut self, cx: CX![], key: Key) {
271 match key {
272 Key::Ctrl('L') => self.redraw(cx),
273 Key::Ctrl('C') => stop!(cx),
274 Key::Pr('q' | 'Q') => {
275 if self.game.is_some() {
276 self.game = None;
277 self.score = None;
278 self.layout(cx);
279 } else {
280 stop!(cx);
281 }
282 }
283 Key::Pr('n' | 'N') => {
284 self.game = Some(Game::new(
285 cx.now(),
286 ret_some_to!([cx], game_finished() as (Duration)),
287 ));
288 self.last_score = None;
289 self.update_score(cx);
290 self.layout(cx);
291 }
292 Key::Pr('<') => {
293 self.adjust -= 1;
294 self.save_scores().expect("Failed to save scores");
295 self.layout(cx);
296 }
297 Key::Pr('>') => {
298 self.adjust += 1;
299 self.save_scores().expect("Failed to save scores");
300 self.layout(cx);
301 }
302 _ => {
303 if let Some(game) = &mut self.game {
304 game.input(cx, key);
305 }
306 }
307 }
308 }
309
310 fn update_score(&mut self, cx: CX![]) {
311 if let Some(game) = &mut self.game {
312 let dur = game.score(cx);
313 let score = dur.as_secs() as i32;
314 let to_wait = if Some(score) != self.score {
315 self.score = Some(score);
316 self.draw_scores(cx);
317
318 1_000_000_000 - dur.subsec_nanos() + 100_000_000
320 } else {
321 1_000_000_000
322 };
323 after!(Duration::from_nanos(to_wait as u64), [cx], update_score());
324 }
325 }
326
327 fn game_finished(&mut self, cx: CX![], dur: Duration) {
328 let score = dur.as_secs() as i32;
329 self.scores.push(score);
330 self.scores.sort_unstable();
331 self.save_scores().expect("Failed to save scores");
332 self.last_score = Some(score);
333 self.score = None;
334 self.game = None;
335 self.layout(cx);
336 }
337
338 fn score_path() -> PathBuf {
339 let mut path = std::env::home_dir().unwrap_or_default();
340 path.push(".teeclub-scores");
341 path
342 }
343
344 fn load_scores(&mut self) -> Option<()> {
345 let data = std::fs::read_to_string(Self::score_path()).ok()?;
346 let mut it = data.split(char::is_whitespace);
347 while let Some(tok) = it.next() {
348 match tok {
349 "adjust:" => {
350 self.adjust = it.next()?.parse::<i32>().ok()?;
351 }
352 "scores[" => {
353 self.scores = Vec::new();
354 loop {
355 match it.next()? {
356 "]" => break,
357 v => self.scores.push(v.parse::<i32>().ok()?),
358 }
359 }
360 self.scores.sort_unstable();
361 }
362 "" => (),
363 _ => return None,
364 }
365 }
366 Some(())
367 }
368
369 fn save_scores(&mut self) -> std::io::Result<()> {
370 let mut out = Vec::new();
371 let _ = writeln!(out, "adjust: {}", self.adjust);
372 let _ = writeln!(out, "scores[");
373 for sc in self.scores.iter().take(200) {
374 let _ = writeln!(out, "{sc}");
375 }
376 let _ = writeln!(out, "]");
377 std::fs::write(Self::score_path(), out)
378 }
379}
380
381#[derive(Eq, PartialEq)]
383struct State {
384 pile: [Hand; 9],
385 stack: [Hand; 9],
386}
387
388impl State {
389 fn pack(&self) -> Vec<u8> {
390 let mut out = Vec::new();
391 for p in &self.pile {
392 p.pack(&mut out);
393 }
394 for s in &self.stack {
395 s.pack(&mut out);
396 }
397 out
398 }
399
400 fn unpack(mut data: &[u8]) -> Self {
401 let mut pile: [Hand; 9] = Default::default();
402 let mut stack: [Hand; 9] = Default::default();
403 for p in &mut pile {
404 *p = Hand::unpack(&mut data);
405 }
406 for s in &mut stack {
407 *s = Hand::unpack(&mut data);
408 }
409 Self { pile, stack }
410 }
411}
412
413struct Game {
415 tile: Tile,
416 history: Vec<Vec<u8>>,
417 state: State,
418 select: Option<(usize, usize)>, dur: Duration,
420 activity: Instant,
421 ret: Option<Ret<Duration>>, }
423
424impl Game {
425 fn new(start: Instant, ret: Ret<Duration>) -> Self {
426 let seed = std::time::SystemTime::UNIX_EPOCH
427 .elapsed()
428 .map(|d| d.as_nanos() as u64)
429 .unwrap_or(0);
430
431 let mut rand = Rand32::new(seed);
432 let mut pile: [Hand; 9] = Default::default();
433 let mut stack: [Hand; 9] = Default::default();
434
435 let p0 = &mut pile[0];
436 p0.add_deck();
437 p0.add_deck();
438 p0.shuffle(&mut rand);
439 p0.shuffle(&mut rand);
440
441 for _ in 0..5 {
442 for s in &mut stack {
443 if let Some(card) = p0.pick_last() {
444 s.add(card);
445 }
446 }
447 }
448
449 Self {
450 tile: Tile::default(),
451 history: Vec::new(),
452 state: State { pile, stack },
453 select: None,
454 dur: Duration::from_secs(0),
455 activity: start,
456 ret: Some(ret),
457 }
458 }
459
460 fn select(&mut self, core: &mut Core, i: usize) {
461 let s = &self.state.stack[i];
462 let len = s.len();
463 if len == 0 {
464 self.select = None;
465 } else {
466 let mut count = 1;
467 while count < len && s[len - count].inc == Some(s[len - count - 1]) {
468 count += 1;
469 }
470 self.select = Some((i, count));
471 }
472 self.draw(core);
473 }
474
475 fn sel_inc(&mut self, core: &mut Core) {
476 if let Some((i, count)) = self.select {
477 let s = &self.state.stack[i];
478 let len = s.len();
479 if count < len && s[len - count].inc == Some(s[len - count - 1]) {
480 self.select = Some((i, count + 1));
481 self.draw(core);
482 }
483 }
484 }
485
486 fn sel_dec(&mut self, core: &mut Core) {
487 if let Some(sel) = &mut self.select {
488 let i = sel.0;
489 let count = sel.1;
490 if count <= 1 {
491 self.select = None;
492 } else {
493 self.select = Some((i, count - 1));
494 }
495 self.draw(core);
496 }
497 }
498
499 fn mark(&mut self) {
502 let v = self.state.pack();
503 if let Some(last) = self.history.last()
504 && *last == v
505 {
506 return;
507 }
508 self.history.push(v);
509 }
510
511 fn undo(&mut self, core: &mut Core) {
513 if let Some(data) = self.history.pop() {
514 self.state = State::unpack(&data);
515 self.select = None;
516 self.draw(core);
517 }
518 }
519
520 fn move_to(&mut self, core: &mut Core, to: usize) {
523 let Some((fr, cnt)) = self.select else {
524 return;
525 };
526 self.select = None;
527 self.mark();
528
529 let s = &mut self.state;
530 let Some(fr_last) = s.stack[fr].peek_last() else {
531 return;
532 };
533
534 if fr == to {
535 for pi in (1..9).rev() {
537 let do_move = if let Some(to_last) = s.pile[pi].peek_last() {
538 to_last.inc == Some(fr_last)
539 } else {
540 fr_last.num == 1
541 };
542 if do_move {
543 for _ in 0..cnt {
544 if let Some(card) = s.stack[fr].pick_last() {
545 s.pile[pi].add(card);
546 }
547 }
548 break;
549 }
550 }
551 if self.is_finished()
552 && let Some(ret) = self.ret.take()
553 {
554 ret!([ret], self.score(core));
555 }
556 } else {
557 let mut copy_cnt = cnt;
559 if let Some(to_last) = s.stack[to].peek_last() {
560 copy_cnt = copy_cnt.min(to_last.num.saturating_sub(fr_last.num) as usize);
561 if to_last.num != fr_last.num + copy_cnt as u8 {
562 copy_cnt = 0;
563 }
564 }
565
566 let mut tmp = Vec::new();
567 for _ in 0..copy_cnt {
568 tmp.push(s.stack[fr].pick_last().unwrap());
569 }
570 while let Some(card) = tmp.pop() {
571 s.stack[to].add(card);
572 }
573 }
574 self.draw(core);
575 }
576
577 fn move_down(&mut self, core: &mut Core) {
579 self.mark();
580 if let Some(card) = self.state.pile[0].pick_last() {
581 self.state.stack[0].add(card);
582 self.select = None;
583 self.draw(core);
584 }
585 }
586
587 fn is_finished(&mut self) -> bool {
588 for i in 1..9 {
589 if self.state.pile[i].len() != 13 {
590 return false;
591 }
592 }
593 true
594 }
595
596 fn layout(&mut self, core: &mut Core, tile: Tile) {
598 self.tile = tile;
599 self.draw(core);
600 }
601
602 fn draw(&mut self, core: &mut Core) {
604 if let Some(mut r) = self.tile.full(core) {
605 r.clear_all_99();
606
607 for col in 0..9 {
608 let x = 1 + col as i32 * 8;
609
610 if let Some(card) = self.state.pile[col].peek_last() {
612 if col == 0 {
613 draw::card_back(&mut r, 0, x);
615 } else {
616 draw::card(&mut r, 0, x, card);
617 }
618 } else {
619 draw::card_space(&mut r, 0, x);
620 }
621
622 r.hfb(7)
624 .at(CARD_SY, x)
625 .hfb(70)
626 .char((49 + col as u8) as char);
627
628 let y0 = CARD_SY + 1;
630 let y1 = r.sy();
631 let hand = &self.state.stack[col];
632 draw::card_space(&mut r, y0, x);
633
634 let selected = self
635 .select
636 .map(|(index, count)| if col == index { count } else { 0 })
637 .unwrap_or(0);
638 let len = hand.len();
639 let sel_i = len.saturating_sub(selected);
640 let space = y1 - y0 - 1;
641 let mut y = y0;
642 let mut i = 0;
643 if len as i32 + CARD_SY > space {
644 draw::card_dots(&mut r, y, x);
645 y += 1;
646 i = len - (space - CARD_SY) as usize;
647 }
648 while i < hand.len() {
649 let card = hand[i];
650 if i >= sel_i {
651 draw::card(&mut r, y + 1, x + 1, card);
652 } else {
653 draw::card(&mut r, y, x, card);
654 }
655 i += 1;
656 y += 1;
657 }
658 }
659 }
660 }
661
662 fn input(&mut self, core: &mut Core, key: Key) {
664 self.activity(core);
665 match key {
666 Key::Pr(dig @ '1'..='9') => {
667 let i = (dig as u8 - b'1') as usize;
668 if self.select.is_some() {
669 self.move_to(core, i);
670 } else {
671 self.select(core, i);
672 }
673 }
674 Key::Pr('+' | '=') => self.sel_inc(core),
675 Key::Pr('-') => self.sel_dec(core),
676 Key::BackSp => self.undo(core),
677 Key::Pr(' ') => self.move_down(core),
678 _ => (),
679 }
680 }
681
682 fn activity(&mut self, core: &mut Core) {
683 self.dur = self.score(core);
684 self.activity = core.now();
685 }
686
687 fn score(&mut self, core: &mut Core) -> Duration {
688 let now = core.now();
689 let dur = now
690 .saturating_duration_since(self.activity)
691 .min(Duration::from_secs(5));
692 self.dur + dur
693 }
694}
695
696struct Rand32(u64);
698
699impl Rand32 {
700 const INC: u64 = 1442695040888963407;
701 const MUL: u64 = 6364136223846793005;
702
703 fn new(seed: u64) -> Self {
704 let mut this = Self(0);
705 let _ = this.get(0..1);
706 this.0 = this.0.wrapping_add(seed);
707 let _ = this.get(0..1);
708 this
709 }
710}
711
712impl Rand for Rand32 {
713 fn get(&mut self, range: std::ops::Range<u32>) -> u32 {
714 let state = self.0;
715 self.0 = state.wrapping_mul(Self::MUL).wrapping_add(Self::INC);
716 let xorshifted = (((state >> 18) ^ state) >> 27) as u32;
717 let rot = (state >> 59) as u32;
718 let v = xorshifted.rotate_right(rot);
719 v % (range.end - range.start) + range.start
720 }
721}