summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/const.ts0
-rw-r--r--src/ui/index.ts3
-rw-r--r--src/ui/inputHandler.ts53
-rw-r--r--src/ui/render.ts113
-rw-r--r--src/ui/utils.ts20
5 files changed, 189 insertions, 0 deletions
diff --git a/src/ui/const.ts b/src/ui/const.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ui/const.ts
diff --git a/src/ui/index.ts b/src/ui/index.ts
new file mode 100644
index 0000000..6a79a81
--- /dev/null
+++ b/src/ui/index.ts
@@ -0,0 +1,3 @@
+export * from "./render";
+export * from "./inputHandler";
+export * from "./utils";
diff --git a/src/ui/inputHandler.ts b/src/ui/inputHandler.ts
new file mode 100644
index 0000000..74bfdd8
--- /dev/null
+++ b/src/ui/inputHandler.ts
@@ -0,0 +1,53 @@
+export enum Key {
+ CTRL_C = "ctrl_c",
+ LEFT_ARROW = "left_arrow",
+ RIGHT_ARROW = "right_arrow",
+}
+
+const keyToBuffer: Record<Key, Buffer> = {
+ [Key.CTRL_C]: Buffer.from("\u0003"),
+ [Key.LEFT_ARROW]: Buffer.from("\u001b[D"),
+ [Key.RIGHT_ARROW]: Buffer.from("\u001b[C"),
+};
+
+export const createAndBindHandler = (
+ onInput: (key: Key) => any,
+ onExit: () => any
+) => {
+ process.stdin.setRawMode(true);
+
+ let seq = Buffer.alloc(0);
+
+ process.stdin.on("data", (inputBuffer) => {
+ seq = Buffer.concat([seq, inputBuffer]);
+
+ for (const [key, buffer] of Object.entries(keyToBuffer) as [
+ Key,
+ Buffer
+ ][]) {
+ if (
+ !(
+ seq.length >= buffer.length &&
+ seq.slice(-buffer.length).equals(buffer)
+ )
+ ) {
+ continue;
+ }
+ {
+ if (key === Key.CTRL_C) {
+ onExit();
+ } else {
+ onInput(key);
+ }
+ seq = Buffer.alloc(0);
+ return;
+ }
+ }
+
+ if (seq.length > 6) {
+ seq = Buffer.alloc(0);
+ }
+ });
+
+ process.stdin.resume();
+};
diff --git a/src/ui/render.ts b/src/ui/render.ts
new file mode 100644
index 0000000..95464dc
--- /dev/null
+++ b/src/ui/render.ts
@@ -0,0 +1,113 @@
+import { SessionState, GAME_SIZE, PADDLE_WIDTH, GameState } from "../state";
+import readline from "node:readline";
+import {
+ clearTerminal,
+ getCurrentTerminalSize,
+ TERM_SIZE as RENDER_SIZE,
+} from "./utils";
+
+let lastTermSize: ReturnType<typeof getCurrentTerminalSize> | undefined;
+
+export const renderGameState = (gameState: GameState): string[] => {
+ let rows: string[] = [];
+ for (let row = -1; row < GAME_SIZE.rows + 1; row++) {
+ // let rowOut: string = " ".repeat(marginCols);
+ 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_SIZE.cols || termSize.rows < RENDER_SIZE.rows) {
+ process.stdout.write("Please increase the screen size");
+ return;
+ }
+
+ const marginCols = (termSize.cols - RENDER_SIZE.cols) / 2;
+ const marginRows = (termSize.rows - RENDER_SIZE.rows) / 2;
+
+ let allOut: string = "\n".repeat(marginRows);
+
+ const localDisplay = renderGameState(sessionState.localPlayerGameState);
+ const remoteDisplay = renderGameState(sessionState.remotePlayerGameState);
+
+ localDisplay.forEach(
+ (row, i) =>
+ (allOut = allOut
+ .concat(" ".repeat(marginCols / 2))
+ .concat(row)
+ .concat(" ".repeat(marginCols / 2))
+ .concat(remoteDisplay[i])
+ .concat("\n"))
+ );
+
+ process.stdout.write(allOut);
+};
diff --git a/src/ui/utils.ts b/src/ui/utils.ts
new file mode 100644
index 0000000..32de02e
--- /dev/null
+++ b/src/ui/utils.ts
@@ -0,0 +1,20 @@
+import process from "node:process";
+import { GAME_SIZE } from "../state";
+
+export const TERM_SIZE = {
+ rows: GAME_SIZE.rows + 2,
+ cols: (GAME_SIZE.cols * 2 + 2) * 2,
+};
+
+export const getCurrentTerminalSize = (): { rows: number; cols: number } => {
+ const { rows, columns } = process.stdout;
+ return { rows, cols: columns };
+};
+
+export const clearTerminal = () => {
+ process.stdout.write("\x1Bc");
+};
+
+export const prepareTerminal = () => {
+ clearTerminal();
+};