From c036dc372d864aad57de882e0ba97bd16e929824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 3 Nov 2025 21:41:26 +0000 Subject: [PATCH] feat: implement game board rendering and spaceship player control - Create 10x20 game board grid with retro styling - Render spaceship emoji at player position (bottom center) - Implement cursor movement with hjkl (left/right only) - Add moveToStart (0) and moveToEnd ($) anchor motions - Re-render board when cursor position changes - Board displays dark grid with highlighted player row --- example/src/App.tsx | 11 +++++--- src/types.ts | 14 ++++++---- src/useBoard/useBoard.ts | 55 ++++++++++++++++++++++++++++++++++++-- src/useCursor/useCursor.ts | 34 ++++++++++++----------- src/useGame/useGame.ts | 7 ++++- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index e489b2b..e096da5 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,19 +3,24 @@ import { useGame } from "@vimazing/typing-chud"; import "@vimazing/typing-chud/game.css"; import type { GameManager } from "@vimazing/typing-chud"; import { useKeyBindings } from "./useKeyBindings"; +import { useMounted } from "./useMounted"; function App() { const gameManager = useGame({}, useKeyBindings); - const { containerRef, gameStatus, scoreManager, renderBoard } = gameManager; + const { containerRef, gameStatus, scoreManager, renderBoard, cursor } = gameManager; const gameManagerRef = useRef(gameManager); + const [mounted] = useMounted(); useEffect(() => { gameManagerRef.current = gameManager; }, [gameManager]); + // Re-render board when cursor position changes useEffect(() => { - renderBoard(); - }, [renderBoard]); + if (mounted) { + renderBoard(); + } + }, [cursor.position(), renderBoard, mounted]); const formatTime = (ms: number) => { const seconds = Math.floor(ms / 1000); diff --git a/src/types.ts b/src/types.ts index 8f59b48..29e68a6 100755 --- a/src/types.ts +++ b/src/types.ts @@ -15,10 +15,16 @@ export const getGamePhase = (status: GameStatus): GamePhase => status === 'waiting' ? 'idle' : 'playing'; // ============================================================================ -// Game-Specific Types (to be defined) +// Game-Specific Types // ============================================================================ -// TODO: Define game-specific types here +export type PlayerPosition = { x: number }; // 0-9 for 10 columns +export type Word = { + id: string; + text: string; + x: number; // 0-9 column position + y: number; // vertical position (0 = top, increases downward) +}; // ============================================================================ // Input & Key Tracking @@ -45,10 +51,8 @@ export type BoardManager = { export type CursorMode = 'normal'; -export type Coord = { row: number; col: number }; - export type CursorManager = { - position: () => Coord; + position: () => PlayerPosition; mode: () => CursorMode; // VIM-style motions (required by Unified API) diff --git a/src/useBoard/useBoard.ts b/src/useBoard/useBoard.ts index 6184ebd..3e1b3ee 100644 --- a/src/useBoard/useBoard.ts +++ b/src/useBoard/useBoard.ts @@ -1,13 +1,64 @@ import { useRef } from "react"; +import type { PlayerPosition } from "../types"; + +const BOARD_WIDTH = 10; +const BOARD_HEIGHT = 20; +const CELL_SIZE = 40; // pixels export function useBoard() { const containerRef = useRef(null); - const renderBoard = () => { + const renderBoard = (playerPosition: PlayerPosition) => { const container = containerRef.current; if (!container) return; - // TODO: Implement board rendering + container.innerHTML = ""; + + // Create board grid + const board = document.createElement("div"); + board.className = "typing-board"; + board.style.display = "grid"; + board.style.gridTemplateColumns = `repeat(${BOARD_WIDTH}, ${CELL_SIZE}px)`; + board.style.gap = "1px"; + board.style.backgroundColor = "#000"; + board.style.padding = "8px"; + board.style.borderRadius = "4px"; + board.style.width = "fit-content"; + + // Create cells + for (let row = 0; row < BOARD_HEIGHT; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + const cell = document.createElement("div"); + cell.className = "typing-cell"; + cell.setAttribute("data-row", String(row)); + cell.setAttribute("data-col", String(col)); + cell.style.width = `${CELL_SIZE}px`; + cell.style.height = `${CELL_SIZE}px`; + cell.style.backgroundColor = "#1a1a2e"; + cell.style.border = "1px solid #0f3460"; + cell.style.display = "flex"; + cell.style.alignItems = "center"; + cell.style.justifyContent = "center"; + cell.style.fontSize = "12px"; + cell.style.fontFamily = "monospace"; + cell.style.color = "#00ff00"; + + // Highlight player row (bottom) + if (row === BOARD_HEIGHT - 1) { + cell.style.backgroundColor = "#0f3460"; + + // Render spaceship in player column + if (col === playerPosition.x) { + cell.innerHTML = "🚀"; + cell.style.fontSize = "24px"; + } + } + + board.appendChild(cell); + } + } + + container.appendChild(board); }; return { diff --git a/src/useCursor/useCursor.ts b/src/useCursor/useCursor.ts index a3a3968..e542333 100755 --- a/src/useCursor/useCursor.ts +++ b/src/useCursor/useCursor.ts @@ -1,43 +1,47 @@ import { useState } from "react"; -import type { Coord } from "../types"; +import type { PlayerPosition } from "../types"; + +const BOARD_WIDTH = 10; // 10 columns +const START_POSITION = 4; // Start at center (column 4-5) export function useCursor() { - const [position] = useState({ row: 0, col: 0 }); + const [position, setPosition] = useState({ x: START_POSITION }); - const moveLeft = (_count?: number) => { - // TODO: Implement movement + const moveLeft = (count: number = 1) => { + setPosition(p => ({ x: Math.max(0, p.x - count) })); }; - const moveRight = (_count?: number) => { - // TODO: Implement movement + const moveRight = (count: number = 1) => { + setPosition(p => ({ x: Math.min(BOARD_WIDTH - 1, p.x + count) })); }; - const moveUp = (_count?: number) => { - // TODO: Implement movement + // Typing game: hjkl only moves left/right, not up/down + const moveUp = () => { + // No vertical movement in typing game }; - const moveDown = (_count?: number) => { - // TODO: Implement movement + const moveDown = () => { + // No vertical movement in typing game }; const moveToStart = () => { - // TODO: Implement anchor motion + setPosition({ x: 0 }); }; const moveToEnd = () => { - // TODO: Implement anchor motion + setPosition({ x: BOARD_WIDTH - 1 }); }; const moveToTop = () => { - // TODO: Implement anchor motion + // No vertical movement in typing game }; const moveToBottom = () => { - // TODO: Implement anchor motion + // No vertical movement in typing game }; const repeatLastMotion = () => { - // TODO: Implement repeat + // TODO: Implement repeat for typing game }; return { diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index e3d8013..aed2ff8 100755 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -16,9 +16,14 @@ export function useGame( const gameStatusManager = useGameStatus(); const scoreManager = useScore(gameStatusManager); + // Wrap renderBoard to include player position + const renderBoardWrapped = () => { + boardManager.renderBoard(cursorManager.position()); + }; + const gameManager: GameManager = { containerRef: boardManager.containerRef, - renderBoard: boardManager.renderBoard, + renderBoard: renderBoardWrapped, gameStatus: gameStatusManager.gameStatus, setGameStatus: gameStatusManager.setGameStatus, startGame: gameStatusManager.startGame,