#!/usr/bin/env lyng

/*
 * Lyng Console Tetris (interactive sample)
 *
 * Controls:
 *  - Left/Right arrows or A/D: move
 *  - Up arrow or W: rotate
 *  - Down arrow or S: soft drop
 *  - Space: hard drop
 *  - P or Escape: pause
 *  - Q: quit

    Tsted to score:
sergeych@sergeych-XPS-17-9720:~$ ~/dev/lyng/examples/tetris_console.lyng
Bye.
Score: 435480
Lines: 271
Level: 28
Ssergeych@sergeych-XPS-17-9720:~$
 */

import lyng.io.console
import lyng.io.fs

val MIN_COLS = 56
val MIN_ROWS = 24
val PANEL_WIDTH = 24
val BOARD_MARGIN_ROWS = 5
val BOARD_MIN_W = 10
val BOARD_MAX_W = 16
val BOARD_MIN_H = 16
val BOARD_MAX_H = 28
val LEVEL_LINES_STEP = 10
val DROP_FRAMES_BASE = 15
val DROP_FRAMES_MIN = 3
val FRAME_DELAY_MS = 35
val RESIZE_WAIT_MS = 250
val MAX_PENDING_INPUTS = 64
val ROTATION_KICKS = [0, -1, 1, -2, 2]
val ANSI_ESC = "\u001b["
val ANSI_RESET = ANSI_ESC + "0m"
val ERROR_LOG_PATH = "/tmp/lyng_tetris_errors.log"
val UNICODE_BLOCK = "██"
val UNICODE_TOP_LEFT = "┌"
val UNICODE_TOP_RIGHT = "┐"
val UNICODE_BOTTOM_LEFT = "└"
val UNICODE_BOTTOM_RIGHT = "┘"
val UNICODE_HORIZONTAL = "──"
val UNICODE_VERTICAL = "│"
val UNICODE_DOT = "· "
val PIECES: List<Piece> = []

type Cell = List<Int>
type Rotation = List<Cell>
type Rotations = List<Rotation>
type Row = List<Int>
type Board = List<Row>

class Piece(val name: String, val rotations: Rotations) {}
class RotateResult(val ok: Bool, val rot: Int, val px: Int) {}
class GameState(
    pieceId0: Int,
    nextId0: Int,
    next2Id0: Int,
    px0: Int,
    py0: Int,
) {
    var pieceId = pieceId0
    var nextId = nextId0
    var next2Id = next2Id0
    var rot = 0
    var px = px0
    var py = py0
    var score = 0
    var totalLines = 0
    var level = 1
    var running = true
    var gameOver = false
    var paused = false
}
class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {}
class InputBuffer {
    private val mutex: Mutex = Mutex()
    private val items: List<String> = []

    fun push(value: String): Void {
        mutex.withLock {
            if (items.size >= MAX_PENDING_INPUTS) {
                items.removeAt(0)
            }
            items.add(value)
        }
    }

    fun drain(): List<String> {
        val out: List<String> = []
        mutex.withLock {
            while (items.size > 0) {
                out.add(items[0])
                items.removeAt(0)
            }
        }
        out
    }
}

fun clearAndHome() {
    Console.clear()
    Console.home()
}

fun logError(message: String, err: Object?): Void {
    try {
        var details = ""
        if (err != null) {
            try {
                details = ": " + err
            } catch (_: Object) {
                details = ": <error-format-failed>"
            }
        }
        Path(ERROR_LOG_PATH).appendUtf8(message + details + "\n")
    } catch (_: Object) {
        // Never let logging errors affect gameplay.
    }
}

fun repeatText(s: String, n: Int): String {
    var out: String = ""
    for (i in 0..<n) {
        out += s
    }
    out
}

fun max<T>(a: T, b: T): T = if (a > b) a else b

fun emptyRow(width: Int): Row {
    val r: Row = []
    for (x in 0..<width) {
        r.add(0)
    }
    r
}

fun createBoard(width: Int, height: Int): Board {
    val b: Board = []
    for (y in 0..<height) {
        b.add(emptyRow(width))
    }
    b
}

fun colorize(text: String, sgr: String, useColor: Bool): String {
    if (!useColor) return text
    ANSI_ESC + sgr + "m" + text + ANSI_RESET
}

fun blockText(pieceId: Int, useColor: Bool): String {
    if (pieceId <= 0) return "  "
    val sgr = when (pieceId) {
        1 -> "36" // I
        2 -> "33" // O
        3 -> "35" // T
        4 -> "32" // S
        5 -> "31" // Z
        6 -> "34" // J
        7 -> "93" // L
        else -> "37"
    }
    colorize(UNICODE_BLOCK, sgr, useColor)
}

fun emptyCellText(useColor: Bool): String {
    colorize(UNICODE_DOT, "90", useColor)
}

fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
    try {
        if (pieceId < 1 || pieceId > 7) return false
        val piece: Piece = PIECES[pieceId - 1]
        if (rot < 0 || rot >= piece.rotations.size) return false
        val cells = piece.rotations[rot]

        for (cell in cells) {
            val x = px + cell[0]
            val y = py + cell[1]

            if (x < 0 || x >= boardW) return false
            if (y >= boardH) return false

            if (y >= 0) {
                if (y >= board.size) return false
                val row = board[y]
                if (row == null) return false
                if (row[x] != 0) return false
            }
        }
        true
    } catch (_: Object) {
        false
    }
}

fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void {
    val piece: Piece = PIECES[pieceId - 1]
    val cells = piece.rotations[rot]

    for (cell in cells) {
        val x = px + cell[0]
        val y = py + cell[1]

        if (y >= 0 && y < board.size) {
            val row = board[y]
            if (row != null) {
                row[x] = pieceId
            }
        }
    }
}

fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
    val b = board
    var y = boardH - 1
    var cleared = 0

    while (y >= 0) {
        if (y >= b.size) {
            y--
            continue
        }
        val row = b[y]
        if (row == null) {
            b.removeAt(y)
            b.insertAt(0, emptyRow(boardW))
            cleared++
            continue
        }
        var full = true
        for (x in 0..<boardW) {
            if (row[x] == 0) {
                full = false
                break
            }
        }

        if (full) {
            b.removeAt(y)
            b.insertAt(0, emptyRow(boardW))
            cleared++
        } else {
            y--
        }
    }

    cleared
}

fun activeCellId(pieceId: Int, rot: Int, px: Int, py: Int, x: Int, y: Int): Int {
    val piece: Piece = PIECES[pieceId - 1]
    val cells = piece.rotations[rot]

    for (cell in cells) {
        val ax = px + cell[0]
        val ay = py + cell[1]
        if (ax == x && ay == y) return pieceId
    }

    0
}

fun tryRotateCw(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): RotateResult {
    val piece: Piece = PIECES[pieceId - 1]
    val rotations = piece.rotations.size
    val nr = (rot + 1) % rotations
    for (kx in ROTATION_KICKS) {
        val nx = px + kx
        if (canPlace(board, boardW, boardH, pieceId, nr, nx, py) == true) {
            return RotateResult(true, nr, nx)
        }
    }
    RotateResult(false, rot, px)
}

fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
    val out: List<String> = []
    if (pieceId <= 0) {
        for (i in 0..<4) {
            out.add("        ")
        }
        return out
    }

    val piece: Piece = PIECES[pieceId - 1]
    val cells = piece.rotations[0]

    for (y in 0..<4) {
        var line = ""
        for (x in 0..<4) {
            var filled = false
            for (cell in cells) {
                if (cell[0] == x && cell[1] == y) {
                    filled = true
                    break
                }
            }
            line += if (filled) blockText(pieceId, useColor) else "  "
        }
        out.add(line)
    }
    out
}

fun render(
    state: GameState,
    board: Board,
    boardW: Int,
    boardH: Int,
    prevFrameLines: List<String>,
    originRow: Int,
    originCol: Int,
    useColor: Bool,
): List<String> {
    val bottomBorder = UNICODE_BOTTOM_LEFT + repeatText(UNICODE_HORIZONTAL, boardW) + UNICODE_BOTTOM_RIGHT

    val panel: List<String> = []
    val nextPiece: Piece = PIECES[state.nextId - 1]
    val next2Piece: Piece = PIECES[state.next2Id - 1]
    val nextName = nextPiece.name
    val next2Name = next2Piece.name
    val preview = nextPreviewLines(state.nextId, useColor)

    panel.add("Lyng Tetris")
    panel.add("")
    panel.add("Score: " + state.score)
    panel.add("Lines: " + state.totalLines)
    panel.add("Level: " + state.level)
    panel.add("")
    panel.add("Next: " + nextName)
    panel.add("")
    for (pl in preview) panel.add(pl)
    panel.add("After: " + next2Name)
    panel.add("")
    panel.add("Keys:")
    panel.add("A/D or arrows")
    panel.add("W/Up: rotate")
    panel.add("S/Down: drop")
    panel.add("Space: hard drop")
    panel.add("P/Esc: pause")
    panel.add("Q: quit")

    val frameLines: List<String> = []

    for (y in 0..<boardH) {
        var line = UNICODE_VERTICAL

        for (x in 0..<boardW) {
            val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y)
            val row = if (y < board.size) board[y] else null
            val b = if (row == null) 0 else row[x]
            val id = if (a > 0) a else b
            line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor)
        }

        line += UNICODE_VERTICAL

        if (y < panel.size) {
            line += "   " + panel[y]
        }

        frameLines.add(line)
    }

    frameLines.add(bottomBorder)

    for (i in 0..<frameLines.size) {
        val line = frameLines[i]
        val old = if (i < prevFrameLines.size) prevFrameLines[i] else null
        if (old != line) {
            Console.moveTo(originRow + i, originCol)
            Console.clearLine()
            Console.write(line)
        }
    }
    frameLines
}

fun fitLine(line: String, width: Int): String {
    val maxLen = if (width > 0) width else 0
    if (maxLen <= 0) return ""
    if (line.size >= maxLen) return line[..<maxLen]
    line + repeatText(" ", maxLen - line.size)
}

fun renderPauseOverlay(
    originRow: Int,
    originCol: Int,
    boardW: Int,
    boardH: Int,
): Void {
    val contentWidth = boardW * 2 + 2 + 3 + PANEL_WIDTH
    val contentHeight = boardH + 1

    val lines: List<String> = []
    lines.add("PAUSED")
    lines.add("")
    lines.add("Any key: continue game")
    lines.add("Esc: exit game")
    lines.add("")
    lines.add("Move: A/D or arrows")
    lines.add("Rotate: W or Up")
    lines.add("Drop: S/Down, Space hard drop")

    var innerWidth = 0
    for (line in lines) {
        if (line.size > innerWidth) innerWidth = line.size
    }
    innerWidth += 4
    val maxInner = max(12, contentWidth - 2)
    if (innerWidth > maxInner) innerWidth = maxInner
    if (innerWidth % 2 != 0) innerWidth--

    val boxWidth = innerWidth + 2
    val boxHeight = lines.size + 2

    val left = originCol + max(0, (contentWidth - boxWidth) / 2)
    val top = originRow + max(0, (contentHeight - boxHeight) / 2)

    val topBorder = UNICODE_TOP_LEFT + repeatText(UNICODE_HORIZONTAL, innerWidth / 2) + UNICODE_TOP_RIGHT
    val bottomBorder = UNICODE_BOTTOM_LEFT + repeatText(UNICODE_HORIZONTAL, innerWidth / 2) + UNICODE_BOTTOM_RIGHT

    Console.moveTo(top, left)
    Console.write(topBorder)

    for (i in 0..<lines.size) {
        Console.moveTo(top + 1 + i, left)
        Console.write(UNICODE_VERTICAL + fitLine("  " + lines[i], innerWidth) + UNICODE_VERTICAL)
    }

    Console.moveTo(top + boxHeight - 1, left)
    Console.write(bottomBorder)
}

fun waitForMinimumSize(minCols: Int, minRows: Int): Object {
    while (true) {
        val g = Console.geometry()
        val cols = g?.columns ?: 0
        val rows = g?.rows ?: 0

        if (cols >= minCols && rows >= minRows) return g

        clearAndHome()
        val lines: List<String> = []
        lines.add("Lyng Tetris needs at least %sx%s terminal size."(minCols, minRows))
        lines.add("Current: %sx%s"(cols, rows))
        lines.add("Resize the console window to continue.")

        val visibleLines = if (rows < lines.size) rows else lines.size
        if (visibleLines > 0) {
            val startRow = max(1, ((rows - visibleLines) / 2) + 1)
            for (i in 0..<visibleLines) {
                val line = lines[i]
                val startCol = max(1, ((cols - line.size) / 2) + 1)
                Console.moveTo(startRow + i, startCol)
                Console.clearLine()
                Console.write(line)
            }
            Console.flush()
        }
        delay(RESIZE_WAIT_MS)
    }
}

fun scoreForLines(cleared: Int, level: Int): Int {
    when (cleared) {
        1 -> 100 * level
        2 -> 300 * level
        3 -> 500 * level
        4 -> 800 * level
        else -> 0
    }
}

// Classic 7 tetrominoes, minimal rotations per piece.
fun cell(x: Int, y: Int): Cell { [x, y] }
fun rot(a: Cell, b: Cell, c: Cell, d: Cell): Rotation {
    val r: Rotation = []
    r.add(a)
    r.add(b)
    r.add(c)
    r.add(d)
    r
}

val iRots: Rotations = []
iRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(3,1)))
iRots.add(rot(cell(2,0), cell(2,1), cell(2,2), cell(2,3)))
PIECES.add(Piece("I", iRots))

val oRots: Rotations = []
oRots.add(rot(cell(1,0), cell(2,0), cell(1,1), cell(2,1)))
PIECES.add(Piece("O", oRots))

val tRots: Rotations = []
tRots.add(rot(cell(1,0), cell(0,1), cell(1,1), cell(2,1)))
tRots.add(rot(cell(1,0), cell(1,1), cell(2,1), cell(1,2)))
tRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(1,2)))
tRots.add(rot(cell(1,0), cell(0,1), cell(1,1), cell(1,2)))
PIECES.add(Piece("T", tRots))

val sRots: Rotations = []
sRots.add(rot(cell(1,0), cell(2,0), cell(0,1), cell(1,1)))
sRots.add(rot(cell(1,0), cell(1,1), cell(2,1), cell(2,2)))
PIECES.add(Piece("S", sRots))

val zRots: Rotations = []
zRots.add(rot(cell(0,0), cell(1,0), cell(1,1), cell(2,1)))
zRots.add(rot(cell(2,0), cell(1,1), cell(2,1), cell(1,2)))
PIECES.add(Piece("Z", zRots))

val jRots: Rotations = []
jRots.add(rot(cell(0,0), cell(0,1), cell(1,1), cell(2,1)))
jRots.add(rot(cell(1,0), cell(2,0), cell(1,1), cell(1,2)))
jRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(2,2)))
jRots.add(rot(cell(1,0), cell(1,1), cell(0,2), cell(1,2)))
PIECES.add(Piece("J", jRots))

val lRots: Rotations = []
lRots.add(rot(cell(2,0), cell(0,1), cell(1,1), cell(2,1)))
lRots.add(rot(cell(1,0), cell(1,1), cell(1,2), cell(2,2)))
lRots.add(rot(cell(0,1), cell(1,1), cell(2,1), cell(0,2)))
lRots.add(rot(cell(0,0), cell(1,0), cell(1,1), cell(1,2)))
PIECES.add(Piece("L", lRots))

if (!Console.isSupported()) {
    println("Console API is not supported in this runtime.")
    void
} else if (!Console.isTty()) {
    println("This sample needs an interactive terminal (TTY).")
    void
} else {
    waitForMinimumSize(MIN_COLS, MIN_ROWS)

    val g0 = Console.geometry()
    val cols = g0?.columns ?: MIN_COLS
    val rows = g0?.rows ?: MIN_ROWS

    val boardW = clamp((cols - PANEL_WIDTH) / 2, BOARD_MIN_W..BOARD_MAX_W)
    val boardH = clamp(rows - BOARD_MARGIN_ROWS, BOARD_MIN_H..BOARD_MAX_H)

    val board: Board = createBoard(boardW, boardH)

    fun nextPieceId() {
        Random.next(1..7)
    }

    val state: GameState = GameState(
        nextPieceId(),
        nextPieceId(),
        nextPieceId(),
        (boardW / 2) - 2,
        -1,
    )
    var prevFrameLines: List<String> = []

    var forceRedraw = false
    val inputBuffer: InputBuffer = InputBuffer()

    val rawModeEnabled = Console.setRawMode(true)
    if (!rawModeEnabled) {
        println("Raw keyboard mode is not available in this terminal/runtime.")
        println("Use jlyng in an interactive terminal with raw input support.")
        void
    } else {
        val useColor = Console.ansiLevel() != ConsoleAnsiLevel.NONE
        Console.enterAltScreen()
        Console.setCursorVisible(false)
        clearAndHome()

        fun resetActivePiece(s: GameState): Void {
            s.pieceId = nextPieceId()
            s.rot = 0
            s.px = (boardW / 2) - 2
            s.py = -1
        }

        fun applyKeyInput(s: GameState, key: String): Void {
            try {
                if (key == "__CTRL_C__") {
                    s.running = false
                }
                else if (s.paused) {
                    if (key == "Escape") {
                        s.running = false
                    } else {
                        s.paused = false
                        forceRedraw = true
                    }
                }
                else if (key == "p" || key == "P" || key == "Escape") {
                    s.paused = true
                    forceRedraw = true
                }
                else if (key == "q" || key == "Q") {
                    s.running = false
                }
                else if (key == "ArrowLeft" || key == "a" || key == "A") {
                    if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py) == true) s.px--
                }
                else if (key == "ArrowRight" || key == "d" || key == "D") {
                    if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px + 1, s.py) == true) s.px++
                }
                else if (key == "ArrowUp" || key == "w" || key == "W") {
                    val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
                    if (rr.ok) {
                        s.rot = rr.rot
                        s.px = rr.px
                    }
                }
                else if (key == "ArrowDown" || key == "s" || key == "S") {
                    if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
                        s.py++
                        s.score++
                    }
                }
                else if (key == " ") {
                    while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
                        s.py++
                        s.score += 2
                    }
                }
            } catch (inputErr: Object) {
                logError("applyKeyInput recovered after error", inputErr)
                resetActivePiece(s)
            }
        }

        var inputRunning = true
        launch {
            while (inputRunning) {
                try {
                    for (ev in Console.events()) {
                        if (!inputRunning) break

                        // Isolate per-event failures so one bad event does not unwind the stream.
                        try {
                            if (ev is ConsoleKeyEvent) {
                                val ke = ev as ConsoleKeyEvent
                                if (ke.type == ConsoleEventType.KEY_DOWN) {
                                    var key = ""
                                    var ctrl = false
                                    try {
                                        key = ke.key
                                    } catch (keyErr: Object) {
                                        logError("Input key read error", keyErr)
                                    }
                                    try {
                                        ctrl = ke.ctrl == true
                                    } catch (_: Object) {
                                        ctrl = false
                                    }
                                    if (key == "") {
                                        logError("Dropped key event with empty/null key", null)
                                        continue
                                    }
                                    val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
                                    inputBuffer.push(mapped)
                                }
                            }
                        } catch (eventErr: Object) {
                            // Keep the input stream alive; report for diagnostics.
                            logError("Input event error", eventErr)
                        }
                    }
                } catch (err: Object) {
                    // Recover stream-level failures by recreating event stream in next loop turn.
                    if (!inputRunning) break
                    logError("Input stream recovered after error", err)
                    Console.setRawMode(true)
                    delay(50)
                }
            }
        }

        fun pollLoopFrame(): LoopFrame? {
            val g = Console.geometry()
            val c = g?.columns ?: 0
            val r = g?.rows ?: 0
            if (c < MIN_COLS || r < MIN_ROWS) {
                waitForMinimumSize(MIN_COLS, MIN_ROWS)
                clearAndHome()
                prevFrameLines = []
                return null
            }

            val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH
            val contentRows = boardH + 1
            val requiredCols = max(MIN_COLS, contentCols)
            val requiredRows = max(MIN_ROWS, contentRows)
            if (c < requiredCols || r < requiredRows) {
                waitForMinimumSize(requiredCols, requiredRows)
                clearAndHome()
                prevFrameLines = []
                return null
            }

            val originCol = max(1, ((c - contentCols) / 2) + 1)
            val originRow = max(1, ((r - contentRows) / 2) + 1)
            LoopFrame(false, originRow, originCol)
        }

        fun advanceGravity(s: GameState, frame: Int): Int {
            s.level = 1 + (s.totalLines / LEVEL_LINES_STEP)
            val dropEvery = max(DROP_FRAMES_MIN, DROP_FRAMES_BASE - s.level)

            var nextFrame = frame + 1
            if (nextFrame < dropEvery) return nextFrame
            nextFrame = 0

            if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
                s.py++
                return nextFrame
            }

            lockPiece(board, s.pieceId, s.rot, s.px, s.py)

            val cleared = clearCompletedLines(board, boardW, boardH)
            if (cleared > 0) {
                s.totalLines += cleared
                s.score += scoreForLines(cleared, s.level)
            }

            s.pieceId = s.nextId
            s.nextId = s.next2Id
            s.next2Id = nextPieceId()
            s.rot = 0
            s.px = (boardW / 2) - 2
            s.py = -1

            if (!canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)) {
                s.gameOver = true
            }
            nextFrame
        }

        try {
            if (!canPlace(board, boardW, boardH, state.pieceId, state.rot, state.px, state.py)) {
                state.gameOver = true
            }

            var frame = 0
            var shouldStop = false
            var prevOriginRow = -1
            var prevOriginCol = -1
            var prevPaused = false
            while (!shouldStop) {
                val frameData = pollLoopFrame()
                if (frameData == null) {
                    frame = 0
                    prevPaused = false
                    continue
                }

                val toApply = inputBuffer.drain()
                if (toApply.size > 0) {
                    for (k in toApply) {
                        applyKeyInput(state, k)
                        if (!state.running || state.gameOver) break
                    }
                }
                if (!state.running || state.gameOver) {
                    shouldStop = true
                } else {
                    val localForceRedraw = forceRedraw
                    forceRedraw = false
                    val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol
                    if (frameData.resized || movedOrigin || localForceRedraw) {
                        clearAndHome()
                        prevFrameLines = []
                    }
                    prevOriginRow = frameData.originRow
                    prevOriginCol = frameData.originCol
                    if (!state.paused) {
                        frame = advanceGravity(state, frame)
                    }
                    prevFrameLines = render(
                        state,
                        board,
                        boardW,
                        boardH,
                        prevFrameLines,
                        frameData.originRow,
                        frameData.originCol,
                        useColor
                    )
                    if (state.paused && (!prevPaused || frameData.resized || movedOrigin)) {
                        renderPauseOverlay(frameData.originRow, frameData.originCol, boardW, boardH)
                    }
                    prevPaused = state.paused
                }
                Console.flush()
                delay(FRAME_DELAY_MS)
            }
        } finally {
            inputRunning = false
            Console.setRawMode(false)
            Console.setCursorVisible(true)
            Console.leaveAltScreen()
            Console.flush()
        }

        if (state.gameOver) {
            println("Game over.")
        } else {
            println("Bye.")
        }
        println("Score: %s"(state.score))
        println("Lines: %s"(state.totalLines))
        println("Level: %s"(state.level))
    }
}
