Skip to main content

teeclub/
main.rs

1//! Teeclub patience card game.  Uses tile-based output
2
3#![allow(clippy::octal_escapes)] // Allow \0HFB sequences
4
5use 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
19// Main loop dumps setup I/O errors to console.  You could catch them
20// instead if you want.
21fn 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
46/// App main actor
47pub struct App {
48    _terminal: ActorOwn<Terminal>,
49    tsh: Option<TermShare>,
50    logo_hfb: Option<Vec<u16>>, // Logo HFB if 256-colour is available
51    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    /// Redraw, with intentional visual flash (for ^L)
122    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                // Update 100ms after next change
319                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/// Game state
382#[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
413/// Game state and gameplay handling
414struct Game {
415    tile: Tile,
416    history: Vec<Vec<u8>>,
417    state: State,
418    select: Option<(usize, usize)>, // (index, count)
419    dur: Duration,
420    activity: Instant,
421    ret: Option<Ret<Duration>>, // Report score for game completion
422}
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    /// Mark this state by saving it in history, available to undo
500    /// later
501    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    /// Restore most recent marked state and remove it from history
512    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    /// Move selected cards to the given stack or to top if the stack
521    /// is the selected stack
522    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            // Move to top
536            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            // Move to another pile
558            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    /// Move card down from stock pile
578    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    /// Set the tile to use for drawing, and redraw
597    fn layout(&mut self, core: &mut Core, tile: Tile) {
598        self.tile = tile;
599        self.draw(core);
600    }
601
602    /// Draw the playing area
603    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                // Pile
611                if let Some(card) = self.state.pile[col].peek_last() {
612                    if col == 0 {
613                        // Facedown pile
614                        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                // Index number
623                r.hfb(7)
624                    .at(CARD_SY, x)
625                    .hfb(70)
626                    .char((49 + col as u8) as char);
627
628                // Stack
629                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    /// Handle input
663    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
696// Adapted from `oorandom` crate, MIT license
697struct 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}