tetro-tui 3.5.2

A terminal-based but modern tetromino-stacking game that is very customizable and cross-platform.
use crossterm::{
    QueueableCommand, cursor,
    style::{self, Stylize},
    terminal,
};

use crate::core_game_engine::{Board, PLAYABLE_BOARD_HEIGHT};

use super::*;

// "|⠁|⠂|⠄|⠈|⠐|⠠|⡀|⢀|"
const BRAILLE: &str = "⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿";

#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)]
pub struct BrailleRenderer {
    x: u16,
    y: u16,
    w: u16,
    h: u16,
    cached_board: Board,
}

impl GameRenderer for BrailleRenderer {
    fn update_feed(
        &mut self,
        _feed: impl IntoIterator<Item = (Notification, InGameTime)>,
        _settings: &Settings,
    ) {
        // We do not use/display feedback_msg-related things for this renderer at this time.
    }

    fn reset_veffects_state(&mut self) {
        // We do not store any effects state associated with the game at this time.
    }

    fn reset_viewport_state(
        &mut self,
        (x, y): (u16, u16),
        (w, h): (u16, u16),
        _ambience: TermCell,
    ) {
        self.x = x;
        self.y = y;
        self.w = w;
        self.h = h;
        self.cached_board = Board::default();
    }

    fn render<W: Write>(
        &mut self,
        term: &mut W,
        game: &Game,
        _meta_data: &GameMetaData,
        settings: &Settings,
        _temp_data: &TemporaryAppData,
        _keybinds_legend: &KeybindsLegend,
        _replay_extra: Option<(InGameTime, f64)>,
    ) -> io::Result<()> {
        let mut board = game.state().board.clone();
        board.resize(PLAYABLE_BOARD_HEIGHT, Default::default());
        if let Some(piece) = game.phase().piece() {
            for (x, y) in piece.coords() {
                if (y as usize) < PLAYABLE_BOARD_HEIGHT {
                    board[y as usize].0[x as usize] = Some(piece.tetromino.into());
                }
            }
        }

        // Simple optimization: Do not do anything if board (=our entire view state) has not changed!
        if board == self.cached_board {
            return Ok(());
        }
        self.cached_board = board;

        let braille = BRAILLE.chars().collect::<Vec<char>>();
        let (delim_l, delim_r) = ('▐', '▌'); //'░';

        let btxt_lines = [
            [19, 18, 17, 16],
            [15, 14, 13, 12],
            [11, 10, 9, 8],
            [7, 6, 5, 4],
            [3, 2, 1, 0],
        ]
        .iter()
        .map(|[i0, i1, i2, i3]| {
            let [l0, l1, l2, l3] = [
                self.cached_board.get(*i0).copied().unwrap_or_default(),
                self.cached_board.get(*i1).copied().unwrap_or_default(),
                self.cached_board.get(*i2).copied().unwrap_or_default(),
                self.cached_board.get(*i3).copied().unwrap_or_default(),
            ];
            [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
                .iter()
                .map(|[j0, j1]| {
                    let b0 = if l0.0[*j0].is_some() { 1 } else { 0 };
                    let b1 = if l1.0[*j0].is_some() { 2 } else { 0 };
                    let b2 = if l2.0[*j0].is_some() { 4 } else { 0 };
                    let b3 = if l3.0[*j0].is_some() { 64 } else { 0 };
                    let b4 = if l0.0[*j1].is_some() { 8 } else { 0 };
                    let b5 = if l1.0[*j1].is_some() { 16 } else { 0 };
                    let b6 = if l2.0[*j1].is_some() { 32 } else { 0 };
                    let b7 = if l3.0[*j1].is_some() { 128 } else { 0 };
                    braille[b0 + b1 + b2 + b3 + b4 + b5 + b6 + b7]
                })
                .collect::<String>()
        });

        term.queue(terminal::Clear(terminal::ClearType::All))?;

        let (w_render, h_render) = (1 + 5 + 1, 5);
        let (x_render, y_render) = (
            self.x + self.w.saturating_sub(w_render) / 2,
            self.y + self.h.saturating_sub(h_render) / 2,
        );

        for (dy, b_line) in btxt_lines.enumerate() {
            term.queue(cursor::MoveTo(
                x_render,
                y_render + u16::try_from(dy).unwrap(),
            ))?
            .queue(style::PrintStyledContent(
                format!("{delim_l}{b_line}{delim_r}")
                    .with(settings.tui_coloring().fg_tui)
                    .on(settings.tui_coloring().bg_tui),
            ))?;
        }

        term.flush()?;

        Ok(())
    }
}