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
This commit is contained in:
2025-11-03 21:41:26 +00:00
parent 80f7aa3f8a
commit c036dc372d
5 changed files with 95 additions and 26 deletions
+8 -3
View File
@@ -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>(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);
+9 -5
View File
@@ -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)
+53 -2
View File
@@ -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<HTMLDivElement | null>(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 {
+19 -15
View File
@@ -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<Coord>({ row: 0, col: 0 });
const [position, setPosition] = useState<PlayerPosition>({ 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 {
+6 -1
View File
@@ -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,