import { SessionState, GAME_SIZE, PADDLE_WIDTH, GameState } from "../state"; import readline from "node:readline"; import { clearTerminal, getCurrentTerminalSize, RENDER_GAME_SIZE, RENDER_STATE_SIZE, } from "./utils"; let lastTermSize: ReturnType | undefined; let lastRender: { seqno: number; time: number } = { seqno: 0, time: Date.now(), }; export const renderGameState = (gameState: GameState): string[] => { let rows: string[] = []; for (let row = -1; row < GAME_SIZE.rows + 1; row++) { let rowOut: string = " "; if (row === -1) { rowOut = rowOut.concat("--".repeat(GAME_SIZE.cols + 2)); } else { for (let col = -1; col < GAME_SIZE.cols + 1; col++) { if (col === -1 || col === GAME_SIZE.cols) { rowOut = rowOut.concat("||"); } else { const [paddleX, paddleY] = gameState.paddle.position; const paddleXMin = paddleX; const paddleXMax = paddleX + PADDLE_WIDTH; const ballPositions = gameState.balls.map(({ position }) => position); const brickPositions = gameState.bricks.map( ({ position }) => position ); const hasPaddle = col >= paddleXMin && col <= paddleXMax && row === paddleY; const firstBall = ballPositions.find( ([ballX, ballY]) => col === Math.round(ballX) && row === Math.round(ballY) ); const hasBrick = brickPositions.some( ([brickX, brickY]) => col === brickX && row === brickY ); if (hasPaddle) { rowOut = rowOut.concat("##"); } else if (firstBall) { const fx = firstBall[0] - Math.round(firstBall[0]); let chars; if (fx < 0) { chars = "O "; } else { chars = " O"; } rowOut = rowOut.concat(chars); } else if (hasBrick) { rowOut = rowOut.concat("▒▒"); } else { rowOut = rowOut.concat(" "); } } } } rows.push(rowOut); } return rows; }; export const renderState = (sessionState: SessionState) => { const rl = new readline.promises.Readline(process.stdout, { autoCommit: true, }); rl.cursorTo(0, 0); const termSize = getCurrentTerminalSize(); if ( lastTermSize && (lastTermSize.cols !== termSize.cols || lastTermSize.rows !== termSize.rows) ) { clearTerminal(); } lastTermSize = termSize; if ( termSize.cols < RENDER_STATE_SIZE.cols || termSize.rows < RENDER_STATE_SIZE.rows ) { process.stdout.write("Please increase the screen size"); return; } const marginCols = termSize.cols - RENDER_STATE_SIZE.cols; const marginRows = (termSize.rows - RENDER_STATE_SIZE.rows) / 2; let allOut: string = "\n".repeat(marginRows); const localDisplay = renderGameState(sessionState.localPlayerGameState); const remoteDisplay = sessionState.remotePlayerGameState && renderGameState(sessionState.remotePlayerGameState); const timeNow = Date.now(); const infoHeader = `Frame: ${sessionState.seqno} Session: ${sessionState.sessionId} Fps: ${( ((sessionState.seqno - lastRender.seqno) / (timeNow - lastRender.time)) * 1000 ).toFixed(0)}\n`; if (remoteDisplay) { allOut = allOut .concat(infoHeader) .concat(" ".repeat(marginCols / 2)) .concat(`LOCAL:`) .concat(" ".repeat(RENDER_GAME_SIZE.cols)) .concat(" ".repeat(4)) .concat(`REMOTE:`) .concat("\n"); localDisplay.forEach( (row, i) => (allOut = allOut .concat(" ".repeat(marginCols / 2)) .concat(row) .concat(" ".repeat(8)) .concat(remoteDisplay[i]) .concat(" ".repeat(marginCols / 2)) .concat("\n")) ); } else { allOut = allOut.concat(infoHeader).concat("\n"); localDisplay.forEach( (row, i) => (allOut = allOut .concat(" ".repeat(marginCols / 2 + 4)) .concat(row) .concat(" ".repeat(marginCols / 2 + 4)) .concat("\n")) ); } process.stdout.write(allOut); lastRender = { seqno: sessionState.seqno, time: timeNow }; };