chore: initialize typing-chud project with Unified API structure
- Copy vim-snake structure stripped of game-specific logic - Remove finalScore calculation (raw data only) - Setup example app with keybinding state management - Empty board renders and waits for game logic
This commit is contained in:
Executable
+149
@@ -0,0 +1,149 @@
|
||||
/* =========================================================
|
||||
🔧 Controls (tweak these)
|
||||
========================================================= */
|
||||
:root {
|
||||
--snake-cell-size: 32px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Container & Board Base
|
||||
========================================================= */
|
||||
#snake-board {
|
||||
position: relative;
|
||||
background-color: var(--card, #1e2030);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 5%, transparent);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Snake Board Layout
|
||||
========================================================= */
|
||||
.snake-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.snake-cell {
|
||||
width: var(--snake-cell-size);
|
||||
height: var(--snake-cell-size);
|
||||
background-color: var(--muted, #2f334d);
|
||||
border: 1px solid var(--border, #3b3f5c);
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Snake Rendering
|
||||
========================================================= */
|
||||
.snake-cell.snake-head {
|
||||
background-color: var(--accent, #ffc777);
|
||||
box-shadow:
|
||||
0 0 8px color-mix(in srgb, var(--accent, #ffc777) 70%, transparent),
|
||||
inset 0 0 4px color-mix(in srgb, var(--accent, #ffc777) 90%, transparent);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.snake-cell.snake-head.dir-up {
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-head.dir-down {
|
||||
border-radius: 2px 2px 6px 6px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-head.dir-left {
|
||||
border-radius: 6px 2px 2px 6px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-head.dir-right {
|
||||
border-radius: 2px 6px 6px 2px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60%;
|
||||
height: 30%;
|
||||
background: radial-gradient(circle, var(--accent-foreground, #1b1d2b) 30%, transparent 60%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.snake-cell.snake-body {
|
||||
background-color: var(--accent, #ffc777);
|
||||
opacity: 0.7;
|
||||
box-shadow: inset 0 0 3px color-mix(in srgb, var(--accent, #ffc777) 50%, transparent);
|
||||
}
|
||||
|
||||
.snake-cell.snake-tail {
|
||||
background-color: var(--accent, #ffc777);
|
||||
opacity: 0.5;
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent);
|
||||
}
|
||||
|
||||
.snake-cell.snake-tail.dir-up {
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-tail.dir-down {
|
||||
border-radius: 2px 2px 6px 6px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-tail.dir-left {
|
||||
border-radius: 6px 2px 2px 6px;
|
||||
}
|
||||
|
||||
.snake-cell.snake-tail.dir-right {
|
||||
border-radius: 2px 6px 6px 2px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Collision Effect
|
||||
========================================================= */
|
||||
.snake-cell.collision {
|
||||
background-color: var(--destructive, #c53b53) !important;
|
||||
box-shadow:
|
||||
0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent),
|
||||
0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 60%, transparent),
|
||||
inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent) !important;
|
||||
animation: collision-pulse 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes collision-pulse {
|
||||
0% {
|
||||
box-shadow:
|
||||
0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent),
|
||||
0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 60%, transparent),
|
||||
inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent);
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 20px color-mix(in srgb, var(--destructive, #c53b53) 100%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--destructive, #c53b53) 80%, transparent),
|
||||
inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Food Rendering
|
||||
========================================================= */
|
||||
.snake-cell.food {
|
||||
background-color: var(--chart-2, #c3e88d);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 8px color-mix(in srgb, var(--chart-2, #c3e88d) 60%, transparent),
|
||||
inset 0 0 4px color-mix(in srgb, var(--chart-2, #c3e88d) 80%, transparent);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.snake-cell.food::after {
|
||||
content: "🍎";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export const gameInfo = {
|
||||
name: 'Typing Chud',
|
||||
description: 'TODO: Game description',
|
||||
|
||||
controls: {
|
||||
navigation: [
|
||||
{ keys: 'hjkl', description: 'TODO: Navigation controls' },
|
||||
],
|
||||
game: [
|
||||
{ keys: 'Space', description: 'Start game' },
|
||||
{ keys: 'q', description: 'Quit game' },
|
||||
],
|
||||
},
|
||||
|
||||
rules: {
|
||||
// TODO: Define game rules
|
||||
},
|
||||
|
||||
scoring: {
|
||||
formula: 'TODO: Scoring formula',
|
||||
range: '0 - 1000 points',
|
||||
penalties: [],
|
||||
examples: [],
|
||||
},
|
||||
|
||||
gameOver: {
|
||||
conditions: [
|
||||
// TODO: Define game over conditions
|
||||
],
|
||||
},
|
||||
|
||||
objective: 'TODO: Game objective',
|
||||
winCondition: 'TODO: Win condition',
|
||||
} as const;
|
||||
|
||||
export type GameInfo = typeof gameInfo;
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
export * from './useGame';
|
||||
export * from './useCursor';
|
||||
export * from './useGameStatus';
|
||||
export * from './useBoard';
|
||||
export * from './useScore';
|
||||
export * from './types';
|
||||
export * from './gameInfo';
|
||||
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
// ============================================================================
|
||||
// Core Game Types
|
||||
// ============================================================================
|
||||
|
||||
export type GameStatus = 'waiting' | 'started' | 'game-won' | 'game-over';
|
||||
export type GamePhase = 'idle' | 'playing';
|
||||
export type PlayStatus = 'started' | 'game-over' | 'game-won';
|
||||
|
||||
export const isPlaying = (status: GameStatus): status is PlayStatus =>
|
||||
status === 'started' || status === 'game-over' || status === 'game-won';
|
||||
|
||||
export const getGamePhase = (status: GameStatus): GamePhase =>
|
||||
status === 'waiting' ? 'idle' : 'playing';
|
||||
|
||||
// ============================================================================
|
||||
// Game-Specific Types (to be defined)
|
||||
// ============================================================================
|
||||
|
||||
// TODO: Define game-specific types here
|
||||
|
||||
// ============================================================================
|
||||
// Input & Key Tracking
|
||||
// ============================================================================
|
||||
|
||||
export type KeyLogEntry = { key: string; timestamp: number };
|
||||
|
||||
export type GameOptions = {
|
||||
// TODO: Define game options
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Manager Interfaces - Board
|
||||
// ============================================================================
|
||||
|
||||
export type BoardManager = {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
renderBoard: (cols: number, rows: number) => void;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Manager Interfaces - Cursor/Snake
|
||||
// ============================================================================
|
||||
|
||||
export type CursorMode = 'normal';
|
||||
|
||||
export type Coord = { row: number; col: number };
|
||||
|
||||
export type CursorManager = {
|
||||
position: () => Coord;
|
||||
mode: () => CursorMode;
|
||||
|
||||
// VIM-style motions (required by Unified API)
|
||||
moveLeft: (count?: number) => void;
|
||||
moveRight: (count?: number) => void;
|
||||
moveUp: (count?: number) => void;
|
||||
moveDown: (count?: number) => void;
|
||||
moveToStart: () => void;
|
||||
moveToEnd: () => void;
|
||||
moveToTop: () => void;
|
||||
moveToBottom: () => void;
|
||||
repeatLastMotion: () => void;
|
||||
};
|
||||
|
||||
// TODO: Define game-specific CursorManager extension if needed
|
||||
|
||||
// ============================================================================
|
||||
// Manager Interfaces - Score
|
||||
// ============================================================================
|
||||
|
||||
export type ScoreManager = {
|
||||
timeValue: number;
|
||||
totalKeystrokes: number;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Manager Interfaces - Game Status
|
||||
// ============================================================================
|
||||
|
||||
export type GameStatusManager = {
|
||||
gameStatus: GameStatus;
|
||||
setGameStatus: (status: GameStatus) => void;
|
||||
startGame: () => void;
|
||||
quitGame: () => void;
|
||||
level: number;
|
||||
score: number;
|
||||
togglePause: () => void;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Manager Interfaces - Key Tracking
|
||||
// ============================================================================
|
||||
|
||||
export type GameKeyManager = {
|
||||
keyLog: KeyLogEntry[];
|
||||
clearKeyLog: () => void;
|
||||
getKeyLog: () => KeyLogEntry[];
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Unified Game Manager - Main Interface
|
||||
// ============================================================================
|
||||
|
||||
export type GameManager = {
|
||||
// Required rendering
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
renderBoard: () => void;
|
||||
|
||||
// Required managers
|
||||
cursor: CursorManager;
|
||||
scoreManager: ScoreManager;
|
||||
|
||||
// Required lifecycle
|
||||
gameStatus: GameStatus;
|
||||
setGameStatus: (status: GameStatus) => void;
|
||||
startGame: () => void;
|
||||
quitGame: () => void;
|
||||
|
||||
// Required key tracking
|
||||
keyLog: KeyLogEntry[];
|
||||
clearKeyLog: () => void;
|
||||
getKeyLog: () => KeyLogEntry[];
|
||||
|
||||
// TODO: Add game-specific additions
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook Return Types
|
||||
// ============================================================================
|
||||
|
||||
// Forward declarations for hook return types
|
||||
// These will be imported from their respective modules
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useBoard';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
export function useBoard() {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const renderBoard = () => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// TODO: Implement board rendering
|
||||
};
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
renderBoard,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseBoardType = ReturnType<typeof useBoard>;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useCursor';
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
import { useState } from "react";
|
||||
import type { Coord } from "../types";
|
||||
|
||||
export function useCursor() {
|
||||
const [position] = useState<Coord>({ row: 0, col: 0 });
|
||||
|
||||
const moveLeft = (_count?: number) => {
|
||||
// TODO: Implement movement
|
||||
};
|
||||
|
||||
const moveRight = (_count?: number) => {
|
||||
// TODO: Implement movement
|
||||
};
|
||||
|
||||
const moveUp = (_count?: number) => {
|
||||
// TODO: Implement movement
|
||||
};
|
||||
|
||||
const moveDown = (_count?: number) => {
|
||||
// TODO: Implement movement
|
||||
};
|
||||
|
||||
const moveToStart = () => {
|
||||
// TODO: Implement anchor motion
|
||||
};
|
||||
|
||||
const moveToEnd = () => {
|
||||
// TODO: Implement anchor motion
|
||||
};
|
||||
|
||||
const moveToTop = () => {
|
||||
// TODO: Implement anchor motion
|
||||
};
|
||||
|
||||
const moveToBottom = () => {
|
||||
// TODO: Implement anchor motion
|
||||
};
|
||||
|
||||
const repeatLastMotion = () => {
|
||||
// TODO: Implement repeat
|
||||
};
|
||||
|
||||
return {
|
||||
position: () => position,
|
||||
mode: () => 'normal' as const,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
moveDown,
|
||||
moveToStart,
|
||||
moveToEnd,
|
||||
moveToTop,
|
||||
moveToBottom,
|
||||
repeatLastMotion,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseCursorType = ReturnType<typeof useCursor>;
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
export * from './useGame';
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
export function useBoard() {
|
||||
return {};
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
import type { GameManager, GameOptions, KeyLogEntry } from "../types";
|
||||
import { useBoard } from "../useBoard";
|
||||
import { useCursor } from "../useCursor";
|
||||
import { useGameStatus } from "../useGameStatus";
|
||||
import { useScore } from "../useScore";
|
||||
|
||||
export function useGame(
|
||||
_options?: GameOptions,
|
||||
platformHook?: unknown
|
||||
): GameManager {
|
||||
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
|
||||
|
||||
const boardManager = useBoard();
|
||||
const cursorManager = useCursor();
|
||||
const gameStatusManager = useGameStatus();
|
||||
const scoreManager = useScore(gameStatusManager);
|
||||
|
||||
const gameManager: GameManager = {
|
||||
containerRef: boardManager.containerRef,
|
||||
renderBoard: boardManager.renderBoard,
|
||||
gameStatus: gameStatusManager.gameStatus,
|
||||
setGameStatus: gameStatusManager.setGameStatus,
|
||||
startGame: gameStatusManager.startGame,
|
||||
quitGame: gameStatusManager.quitGame,
|
||||
cursor: cursorManager,
|
||||
scoreManager,
|
||||
keyLog,
|
||||
clearKeyLog: () => setKeyLog([]),
|
||||
getKeyLog: () => [...keyLog],
|
||||
};
|
||||
|
||||
if (typeof platformHook === 'function') {
|
||||
platformHook(gameManager);
|
||||
}
|
||||
|
||||
return gameManager;
|
||||
}
|
||||
|
||||
export type UseGameType = ReturnType<typeof useGame>;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useGameStatus';
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import type { GameStatus } from "../types";
|
||||
|
||||
export function useGameStatus() {
|
||||
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||
|
||||
const startGame = () => {
|
||||
setGameStatus("started");
|
||||
};
|
||||
|
||||
const quitGame = () => {
|
||||
setGameStatus("waiting");
|
||||
};
|
||||
|
||||
return {
|
||||
gameStatus,
|
||||
setGameStatus,
|
||||
startGame,
|
||||
quitGame,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseGameStatusType = ReturnType<typeof useGameStatus>;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export type UseTimerReturn = {
|
||||
timeValue: number;
|
||||
startTimer: () => void;
|
||||
stopTimer: () => void;
|
||||
resetTimer: () => void;
|
||||
};
|
||||
|
||||
export type UseTimerOptions = {
|
||||
/**
|
||||
* Interval (in ms) between updates. Defaults to 100ms.
|
||||
*/
|
||||
interval?: number;
|
||||
};
|
||||
|
||||
export function useTimer(options: UseTimerOptions = {}): UseTimerReturn {
|
||||
const { interval = 100 } = options;
|
||||
const [timeValue, setTimeValue] = useState(0);
|
||||
|
||||
const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const offsetRef = useRef(0);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (startTimeRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
setTimeValue(offsetRef.current + (now - startTimeRef.current));
|
||||
}, []);
|
||||
|
||||
const startTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (startTimeRef.current === null) {
|
||||
startTimeRef.current = performance.now();
|
||||
}
|
||||
|
||||
update();
|
||||
intervalIdRef.current = setInterval(update, Math.max(16, interval));
|
||||
}, [interval, update]);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
|
||||
if (startTimeRef.current !== null) {
|
||||
update();
|
||||
offsetRef.current += performance.now() - startTimeRef.current;
|
||||
startTimeRef.current = null;
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
const resetTimer = useCallback(() => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
|
||||
startTimeRef.current = null;
|
||||
offsetRef.current = 0;
|
||||
setTimeValue(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
startTimer,
|
||||
stopTimer,
|
||||
resetTimer,
|
||||
};
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
export * from './useScore';
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { ScoreManager } from "../types";
|
||||
|
||||
export function useScore(gameStatusManager: any): ScoreManager {
|
||||
const [timeValue, setTimeValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (gameStatusManager.gameStatus !== 'started') return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimeValue(prev => prev + 100);
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [gameStatusManager.gameStatus]);
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
totalKeystrokes: 0, // TODO: Track keystrokes
|
||||
};
|
||||
}
|
||||
|
||||
export type UseScoreType = ReturnType<typeof useScore>;
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { UseTimerReturn } from "./hooks/useTimer";
|
||||
import type { GameStatus } from "../types";
|
||||
|
||||
type UseScoreTimeParams = {
|
||||
gameStatus: GameStatus;
|
||||
timer: UseTimerReturn;
|
||||
};
|
||||
|
||||
export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) {
|
||||
const prevStatusRef = useRef<GameStatus | null>(null);
|
||||
const { startTimer, stopTimer, resetTimer } = timer;
|
||||
|
||||
useEffect(() => {
|
||||
prevStatusRef.current = gameStatus;
|
||||
|
||||
switch (gameStatus) {
|
||||
case "started":
|
||||
resetTimer();
|
||||
startTimer();
|
||||
break;
|
||||
|
||||
case "game-over":
|
||||
case "game-won":
|
||||
case "waiting":
|
||||
stopTimer();
|
||||
break;
|
||||
}
|
||||
}, [gameStatus, startTimer, stopTimer, resetTimer]);
|
||||
}
|
||||
Reference in New Issue
Block a user