cd home
BowenCactus.com
staticized for github pages
import React, { useState, useEffect, useCallback, useReducer } from ‘react’; // Use this for a consistent background and colors const tailwindConfig = { theme: { extend: { colors: { ‘dark-background’: ‘#1c1917’, ‘card-bg’: ‘rgba(31, 41, 55, 0.8)’, ‘card-border’: ‘#374151’, ‘gradient-start’: ‘#f97316’, ‘gradient-end’: ‘#facc15’, ‘wall’: ‘#2d3748’, ‘floor’: ‘#4a5568’, ‘corridor’: ‘#a0aec0’, ‘room’: ‘#cbd5e0’, ‘exit’: ‘#34d399’, ‘player-color’: ‘lightgray’, // Corrected to a valid color ‘enemy-color’: ‘red’, // Corrected to a valid color ‘key-color’: ‘gold’, }, }, }, }; const TILE_SIZE = 20; // Game state reducer to handle complex state changes cleanly const gameReducer = (state, action) => { switch (action.type) { case ‘START_GAME’: return { …state, …action.payload, gameStatus: ‘playing’, message: ”, messageTitle: ”, isMessageBoxVisible: false, }; case ‘UPDATE_GAME_STATE’: const { newPlayer, newEnemies, newCollectibles } = action.payload; const newState = { …state, player: newPlayer, enemies: newEnemies, collectibles: newCollectibles, }; // Check win/loss conditions and dispatch an end game action if needed if ( newPlayer.keysFound >= state.numKeysRequired && newPlayer.x === state.exit.x && newPlayer.y === state.exit.y ) { return { …newState, gameStatus: ‘levelComplete’, isMessageBoxVisible: true, messageTitle: ‘Level Complete!’, message: `You are now on level ${state.level + 1}.`, }; } let isCaught = false; newEnemies.forEach(enemy => { if (newPlayer.x === enemy.x && newPlayer.y === enemy.y) { isCaught = true; } }); if (isCaught) { return { …newState, gameStatus: ‘gameOver’, isMessageBoxVisible: true, messageTitle: ‘Game Over!’, message: `You were caught on level ${state.level}.`, }; } return newState; default: return state; } }; const App = () => { const [state, dispatch] = useReducer(gameReducer, { GRID_WIDTH: 0, GRID_HEIGHT: 0, grid: [], player: { x: 0, y: 0, keysFound: 0, size: TILE_SIZE }, enemies: [], collectibles: [], exit: { x: 0, y: 0 }, keys: {}, gamepadIndex: null, gameStatus: ‘loading’, level: 1, numKeysRequired: 1, message: ”, messageTitle: ”, isMessageBoxVisible: false, }); const { GRID_WIDTH, GRID_HEIGHT, grid, player, enemies, collectibles, exit, gameStatus, level, numKeysRequired, message, messageTitle, isMessageBoxVisible } = state; const canvasRef = React.useRef(null); const animationFrameId = React.useRef(null); const keysRef = React.useRef({}); const gamepadIndexRef = React.useRef(null); const lastMoveTimeRef = React.useRef(0); const moveInterval = 200; const isWalkable = useCallback((x, y) => { if (x < 0 || x >= GRID_WIDTH || y < 0 || y >= GRID_HEIGHT || !grid.length || !grid[y]) { return false; } const tileColor = grid[y][x]; return tileColor === tailwindConfig.theme.extend.colors.ROOM || tileColor === tailwindConfig.theme.extend.colors.CORRIDOR || tileColor === tailwindConfig.theme.extend.colors.EXIT; }, [GRID_WIDTH, GRID_HEIGHT, grid]); const getRandomWalkableTile = useCallback(() => { let tile; do { const x = Math.floor(Math.random() * GRID_WIDTH); const y = Math.floor(Math.random() * GRID_HEIGHT); tile = { x, y }; } while (!isWalkable(tile.x, tile.y)); return tile; }, [GRID_WIDTH, GRID_HEIGHT, isWalkable]); const generateDungeon = useCallback((currentLevel) => { const newGrid = Array(GRID_HEIGHT).fill(null).map(() => Array(GRID_WIDTH).fill(tailwindConfig.theme.extend.colors.WALL)); const rooms = []; const numRooms = Math.floor(Math.random() * (currentLevel + 4)) + 5; for (let i = 0; i < numRooms; i++) { const roomWidth = Math.floor(Math.random() * (currentLevel + 7)) + 3; const roomHeight = Math.floor(Math.random() * (currentLevel + 7)) + 3; const roomX = Math.floor(Math.random() * (GRID_WIDTH - roomWidth - 2)) + 1; const roomY = Math.floor(Math.random() * (GRID_HEIGHT - roomHeight - 2)) + 1; let hasOverlap = false; for (const existingRoom of rooms) { if ( roomX < existingRoom.x + existingRoom.width && roomX + roomWidth > existingRoom.x && roomY < existingRoom.y + existingRoom.height && roomY + roomHeight > existingRoom.y ) { hasOverlap = true; break; } } if (!hasOverlap) { for (let y = roomY; y < roomY + roomHeight; y++) { for (let x = roomX; x < roomX + roomWidth; x++) { if (y >= 0 && y < GRID_HEIGHT && x >= 0 && x < GRID_WIDTH) { newGrid[y][x] = tailwindConfig.theme.extend.colors.ROOM; } } } rooms.push({ x: roomX, y: roomY, width: roomWidth, height: roomHeight }); } } for (let i = 0; i < rooms.length - 1; i++) { const startRoom = rooms[i]; const endRoom = rooms[i + 1]; let startX = Math.floor(startRoom.x + startRoom.width / 2); let startY = Math.floor(startRoom.y + startRoom.height / 2); let endX = Math.floor(endRoom.x + endRoom.width / 2); let endY = Math.floor(endRoom.y + endRoom.height / 2); while (startX !== endX) { if (startY >= 0 && startY < GRID_HEIGHT && startX >= 0 && startX < GRID_WIDTH) { newGrid[startY][startX] = tailwindConfig.theme.extend.colors.CORRIDOR; } startX += (startX < endX) ? 1 : -1; } while (startY !== endY) { if (startY >= 0 && startY < GRID_HEIGHT && startX >= 0 && startX < GRID_WIDTH) { newGrid[startY][startX] = tailwindConfig.theme.extend.colors.CORRIDOR; } startY += (startY < endY) ? 1 : -1; } } return { newGrid, rooms }; }, [GRID_WIDTH, GRID_HEIGHT]); const startGame = useCallback((startLevel = 1) => { const { newGrid: generatedGrid, rooms } = generateDungeon(startLevel); const startTile = getRandomWalkableTile(); const initialPlayer = { x: startTile.x, y: startTile.y, size: TILE_SIZE, keysFound: 0 }; const numEnemies = Math.floor(Math.random() * (startLevel + 1)) + 2; const initialEnemies = []; for (let i = 0; i < numEnemies; i++) { const enemyTile = getRandomWalkableTile(); initialEnemies.push({ x: enemyTile.x, y: enemyTile.y, size: TILE_SIZE }); } const requiredKeys = Math.floor(startLevel / 2) + 1; const initialCollectibles = []; for (let i = 0; i < requiredKeys; i++) { initialCollectibles.push(getRandomWalkableTile()); } const exitTile = getRandomWalkableTile(); if (exitTile.y < generatedGrid.length && exitTile.x < generatedGrid[0].length) { generatedGrid[exitTile.y][exitTile.x] = tailwindConfig.theme.extend.colors.EXIT; } dispatch({ type: 'START_GAME', payload: { level: startLevel, numKeysRequired: requiredKeys, initialPlayer, initialEnemies, initialCollectibles, exit: exitTile, grid: generatedGrid, GRID_WIDTH, GRID_HEIGHT, }, }); }, [generateDungeon, getRandomWalkableTile, GRID_WIDTH, GRID_HEIGHT]); const handlePlayerMove = useCallback((dx, dy) => { if (gameStatus !== ‘playing’) return; const now = Date.now(); if (now – lastMoveTimeRef.current < moveInterval) return; const newPlayerX = player.x + dx; const newPlayerY = player.y + dy; if (isWalkable(newPlayerX, newPlayerY)) { const newPlayer = { ...player, x: newPlayerX, y: newPlayerY }; // Enemy movement logic const newEnemies = enemies.map(enemy => { const directions = [{ x: 0, y: 1 }, { x: 0, y: -1 }, { x: 1, y: 0 }, { x: -1, y: 0 }]; const newDirection = directions[Math.floor(Math.random() * directions.length)]; const newEnemyX = enemy.x + newDirection.x; const newEnemyY = enemy.y + newDirection.y; if (isWalkable(newEnemyX, newEnemyY)) { return { …enemy, x: newEnemyX, y: newEnemyY }; } return enemy; }); // Collectible logic const newCollectibles = collectibles.filter( (key) => key.x !== newPlayer.x || key.y !== newPlayer.y ); // Dispatch one comprehensive state update dispatch({ type: ‘UPDATE_GAME_STATE’, payload: { newPlayer: { …newPlayer, keysFound: newCollectibles.length < collectibles.length ? newPlayer.keysFound + 1 : newPlayer.keysFound }, newEnemies, newCollectibles, } }); } lastMoveTimeRef.current = now; }, [gameStatus, player, enemies, collectibles, isWalkable, moveInterval]); const drawGrid = useCallback(() => { const ctx = canvasRef.current.getContext(‘2d’); ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); for (let y = 0; y < grid.length; y++) { for (let x = 0; x < grid[y].length; x++) { ctx.fillStyle = grid[y][x]; ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } }, [grid]); // Main game loop for rendering const gameLoop = useCallback(() => { const ctx = canvasRef.current.getContext(‘2d’); // Draw Grid drawGrid(); // Draw Collectibles collectibles.forEach(item => { ctx.fillStyle = tailwindConfig.theme.extend.colors.keyColor; ctx.fillRect(item.x * TILE_SIZE, item.y * TILE_SIZE, TILE_SIZE, TILE_SIZE); }); // Draw Player ctx.fillStyle = tailwindConfig.theme.extend.colors.playerColor; ctx.fillRect(player.x * TILE_SIZE, player.y * TILE_SIZE, player.size, player.size); // Draw Enemies enemies.forEach(enemy => { ctx.fillStyle = tailwindConfig.theme.extend.colors.enemyColor; ctx.fillRect(enemy.x * TILE_SIZE, enemy.y * TILE_SIZE, enemy.size, enemy.size); }); if (gameStatus === ‘playing’ || gameStatus === ‘levelComplete’) { animationFrameId.current = requestAnimationFrame(gameLoop); } }, [drawGrid, collectibles, player, enemies, gameStatus]); // Handle level completion useEffect(() => { if (gameStatus === ‘levelComplete’) { setTimeout(() => { startGame(level + 1); }, 2000); } }, [gameStatus, level, startGame]); // Start the main loop useEffect(() => { if (gameStatus === ‘playing’) { gameLoop(); } return () => { if (animationFrameId.current) { cancelAnimationFrame(animationFrameId.current); } }; }, [gameStatus, gameLoop]); // Set up event listeners for keyboard and gamepad useEffect(() => { const handleKeyDown = (e) => { keysRef.current[e.key.toLowerCase()] = true; let dx = 0; let dy = 0; if (e.key.toLowerCase() === ‘w’ || e.key === ‘ArrowUp’) dy = -1; if (e.key.toLowerCase() === ‘s’ || e.key === ‘ArrowDown’) dy = 1; if (e.key.toLowerCase() === ‘a’ || e.key === ‘ArrowLeft’) dx = -1; if (e.key.toLowerCase() === ‘d’ || e.key === ‘ArrowRight’) dx = 1; if (dx !== 0 || dy !== 0) { handlePlayerMove(dx, dy); } }; const handleKeyUp = (e) => { keysRef.current[e.key.toLowerCase()] = false; }; const handleGamepadConnected = (e) => { gamepadIndexRef.current = e.gamepad.index; }; const handleGamepadDisconnected = (e) => { if (gamepadIndexRef.current === e.gamepad.index) { gamepadIndexRef.current = null; } }; document.addEventListener(‘keydown’, handleKeyDown); document.addEventListener(‘keyup’, handleKeyUp); window.addEventListener(‘gamepadconnected’, handleGamepadConnected); window.addEventListener(‘gamepaddisconnected’, handleGamepadDisconnected); // Initial canvas resize and game start const resizeObserver = new ResizeObserver(() => { if (canvasRef.current && canvasRef.current.parentElement) { canvasRef.current.width = canvasRef.current.parentElement.clientWidth; canvasRef.current.height = canvasRef.current.parentElement.clientHeight; const newGRID_WIDTH = Math.floor(canvasRef.current.width / TILE_SIZE); const newGRID_HEIGHT = Math.floor(canvasRef.current.height / TILE_SIZE); dispatch({ type: ‘START_GAME’, payload: { …state, GRID_WIDTH: newGRID_WIDTH, GRID_HEIGHT: newGRID_HEIGHT, level: 1 } }); } }); if (canvasRef.current) { resizeObserver.observe(canvasRef.current.parentElement); } return () => { document.removeEventListener(‘keydown’, handleKeyDown); document.removeEventListener(‘keyup’, handleKeyUp); window.removeEventListener(‘gamepadconnected’, handleGamepadConnected); window.removeEventListener(‘gamepaddisconnected’, handleGamepadDisconnected); if (canvasRef.current && canvasRef.current.parentElement) { resizeObserver.unobserve(canvasRef.current.parentElement); } if (animationFrameId.current) { cancelAnimationFrame(animationFrameId.current); } }; }, [handlePlayerMove, state]); return (

Dungeon Crawler

Navigate the dungeon and avoid the enemies.

Level: {level} Keys: = numKeysRequired ? ‘text-green-500’ : ‘text-red-500’}>{player.keysFound}/{numKeysRequired}
{isMessageBoxVisible && (

{messageTitle}

{message}

{gameStatus === ‘gameOver’ && ( )}
)}
← Back
); }; export default App;