1+ import React , { useState , useEffect , useCallback } from 'react' ;
2+ import { Link } from 'react-router-dom' ;
3+ import { ArrowLeftIcon , CubeIcon } from '@phosphor-icons/react' ; // Using CubeIcon as a placeholder icon for now
4+ import useSeo from '../../hooks/useSeo' ;
5+
6+ const MAP_WIDTH = 20 ;
7+ const MAP_HEIGHT = 15 ;
8+ const TILE_FLOOR = '.' ;
9+ const TILE_WALL = '#' ;
10+ const ENTITY_PLAYER = '@' ;
11+ const ENTITY_ENEMY = 'E' ;
12+ const ENTITY_EXIT = 'X' ;
13+
14+ function RoguelikeGamePage ( ) {
15+ useSeo ( {
16+ title : 'Roguelike Game | Fezcodex' ,
17+ description : 'A simple roguelike game.' ,
18+ keywords : [ 'Fezcodex' , 'game' , 'roguelike' ] ,
19+ ogTitle : 'Roguelike Game | Fezcodex' ,
20+ ogDescription : 'A simple roguelike game.' ,
21+ ogImage : 'https://fezcode.github.io/logo512.png' ,
22+ twitterCard : 'summary_large_image' ,
23+ twitterTitle : 'Roguelike Game | Fezcodex' ,
24+ twitterDescription : 'A simple roguelike game.' ,
25+ twitterImage : 'https://fezcode.github.io/logo512.png' ,
26+ } ) ;
27+
28+ const [ gameMap , setGameMap ] = useState ( [ ] ) ;
29+ const [ playerPosition , setPlayerPosition ] = useState ( null ) ;
30+ const [ enemyPositions , setEnemyPositions ] = useState ( [ ] ) ;
31+ const [ exitPosition , setExitPosition ] = useState ( null ) ;
32+ const [ gameStatus , setGameStatus ] = useState ( 'playing' ) ; // playing, won, lost
33+
34+ // Utility to get a random empty position
35+ const getRandomEmptyPosition = useCallback ( ( map ) => {
36+ let x , y ;
37+ do {
38+ x = Math . floor ( Math . random ( ) * MAP_WIDTH ) ;
39+ y = Math . floor ( Math . random ( ) * MAP_HEIGHT ) ;
40+ } while ( map [ y ] [ x ] !== TILE_FLOOR ) ;
41+ return { x, y } ;
42+ } , [ ] ) ;
43+
44+ // Map Generation
45+ const generateMap = useCallback ( ( ) => {
46+ // Initialize map with walls
47+ let newMap = Array ( MAP_HEIGHT )
48+ . fill ( null )
49+ . map ( ( ) => Array ( MAP_WIDTH ) . fill ( TILE_WALL ) ) ;
50+
51+ // Simple room generation
52+ const roomCount = 5 + Math . floor ( Math . random ( ) * 5 ) ; // 5-9 rooms
53+ for ( let i = 0 ; i < roomCount ; i ++ ) {
54+ const roomW = 5 + Math . floor ( Math . random ( ) * 5 ) ; // 5-9 width
55+ const roomH = 3 + Math . floor ( Math . random ( ) * 5 ) ; // 3-7 height
56+ const roomX = 1 + Math . floor ( Math . random ( ) * ( MAP_WIDTH - roomW - 2 ) ) ;
57+ const roomY = 1 + Math . floor ( Math . random ( ) * ( MAP_HEIGHT - roomH - 2 ) ) ;
58+
59+ for ( let y = roomY ; y < roomY + roomH ; y ++ ) {
60+ for ( let x = roomX ; x < roomX + roomW ; x ++ ) {
61+ newMap [ y ] [ x ] = TILE_FLOOR ;
62+ }
63+ }
64+ }
65+
66+ // Place Player
67+ const playerPos = getRandomEmptyPosition ( newMap ) ;
68+ setPlayerPosition ( playerPos ) ;
69+
70+ // Place Exit
71+ let exitPos ;
72+ do {
73+ exitPos = getRandomEmptyPosition ( newMap ) ;
74+ } while ( exitPos . x === playerPos . x && exitPos . y === playerPos . y ) ;
75+ setExitPosition ( exitPos ) ;
76+
77+ // Place Enemies
78+ const numEnemies = 3 + Math . floor ( Math . random ( ) * 3 ) ; // 3-5 enemies
79+ const newEnemyPositions = [ ] ;
80+ for ( let i = 0 ; i < numEnemies ; i ++ ) {
81+ let enemyPos ;
82+ do {
83+ enemyPos = getRandomEmptyPosition ( newMap ) ;
84+ } while (
85+ ( enemyPos . x === playerPos . x && enemyPos . y === playerPos . y ) ||
86+ ( enemyPos . x === exitPos . x && enemyPos . y === exitPos . y ) ||
87+ newEnemyPositions . some (
88+ ( ep ) => ep . x === enemyPos . x && ep . y === enemyPos . y ,
89+ )
90+ ) ;
91+ newEnemyPositions . push ( enemyPos ) ;
92+ }
93+ setEnemyPositions ( newEnemyPositions ) ;
94+
95+ setGameMap ( newMap ) ;
96+ setGameStatus ( 'playing' ) ;
97+ } , [ getRandomEmptyPosition ] ) ;
98+
99+ useEffect ( ( ) => {
100+ generateMap ( ) ;
101+ } , [ generateMap ] ) ;
102+
103+ // Enemy AI - Simple random movement (will be handled by player move)
104+ const moveEnemies = useCallback (
105+ ( currentPlayerPos ) => {
106+ setEnemyPositions ( ( prevEnemyPositions ) => {
107+ const newEnemyPositions = prevEnemyPositions . map ( ( enemy ) => {
108+ const possibleMoves = [
109+ { dx : 0 , dy : - 1 } ,
110+ { dx : 0 , dy : 1 } ,
111+ { dx : - 1 , dy : 0 } ,
112+ { dx : 1 , dy : 0 } ,
113+ ] ;
114+ const randomMove =
115+ possibleMoves [ Math . floor ( Math . random ( ) * possibleMoves . length ) ] ;
116+
117+ const newX = enemy . x + randomMove . dx ;
118+ const newY = enemy . y + randomMove . dy ;
119+
120+ // Check boundaries and walls
121+ if (
122+ newX >= 0 &&
123+ newX < MAP_WIDTH &&
124+ newY >= 0 &&
125+ newY < MAP_HEIGHT &&
126+ gameMap [ newY ] [ newX ] !== TILE_WALL
127+ ) {
128+ return { x : newX , y : newY } ;
129+ }
130+ return enemy ; // Stay if cannot move
131+ } ) ;
132+
133+ // Check for collision with player after all enemies moved
134+ for ( const newEnemy of newEnemyPositions ) {
135+ if (
136+ newEnemy . x === currentPlayerPos . x &&
137+ newEnemy . y === currentPlayerPos . y
138+ ) {
139+ setGameStatus ( 'lost' ) ;
140+ break ;
141+ }
142+ }
143+ return newEnemyPositions ;
144+ } ) ;
145+ } ,
146+ [ gameMap , setGameStatus ] ,
147+ ) ;
148+
149+ // Player Movement
150+ const movePlayer = useCallback (
151+ ( dx , dy ) => {
152+ if ( gameStatus !== 'playing' ) return ;
153+
154+ const newX = playerPosition . x + dx ;
155+ const newY = playerPosition . y + dy ;
156+
157+ // Check boundaries
158+ if (
159+ newX < 0 ||
160+ newX >= MAP_WIDTH ||
161+ newY < 0 ||
162+ newY >= MAP_HEIGHT ||
163+ gameMap [ newY ] [ newX ] === TILE_WALL
164+ ) {
165+ return ; // Can't move through walls or out of bounds
166+ }
167+
168+ const nextPlayerPos = { x : newX , y : newY } ;
169+ setPlayerPosition ( nextPlayerPos ) ;
170+
171+ // Check for exit
172+ if ( newX === exitPosition . x && newY === exitPosition . y ) {
173+ setGameStatus ( 'won' ) ;
174+ return ;
175+ }
176+
177+ // Check for enemy collision immediately after player moves
178+ for ( const enemy of enemyPositions ) {
179+ if ( newX === enemy . x && newY === enemy . y ) {
180+ setGameStatus ( 'lost' ) ;
181+ return ;
182+ }
183+ }
184+
185+ // If no win/loss condition, then enemies move
186+ moveEnemies ( nextPlayerPos ) ;
187+ } ,
188+ [ playerPosition , gameMap , exitPosition , enemyPositions , gameStatus , moveEnemies ] ,
189+ ) ;
190+
191+ // Keyboard input handler
192+ useEffect ( ( ) => {
193+ const handleKeyDown = ( event ) => {
194+ switch ( event . key ) {
195+ case 'w' :
196+ case 'W' :
197+ movePlayer ( 0 , - 1 ) ;
198+ break ;
199+ case 's' :
200+ case 'S' :
201+ movePlayer ( 0 , 1 ) ;
202+ break ;
203+ case 'a' :
204+ case 'A' :
205+ movePlayer ( - 1 , 0 ) ;
206+ break ;
207+ case 'd' :
208+ case 'D' :
209+ movePlayer ( 1 , 0 ) ;
210+ break ;
211+ default :
212+ break ;
213+ }
214+ } ;
215+
216+ window . addEventListener ( 'keydown' , handleKeyDown ) ;
217+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
218+ } , [ movePlayer ] ) ;
219+
220+ // Render the game map
221+ const renderMap = ( ) => {
222+ if ( ! gameMap . length ) return null ;
223+
224+ return (
225+ < div
226+ className = "grid gap-px bg-gray-700 p-px"
227+ style = { {
228+ gridTemplateColumns : `repeat(${ MAP_WIDTH } , minmax(0, 1fr))` ,
229+ width : `${ MAP_WIDTH * 24 } px` , // Assuming 24px per tile
230+ } }
231+ >
232+ { gameMap . map ( ( row , y ) =>
233+ row . map ( ( tile , x ) => {
234+ let content = tile ;
235+ let className = 'flex items-center justify-center w-6 h-6 text-xs font-mono' ;
236+
237+ if ( playerPosition && playerPosition . x === x && playerPosition . y === y ) {
238+ content = ENTITY_PLAYER ;
239+ className += ' bg-blue-500 text-white' ;
240+ } else if ( exitPosition && exitPosition . x === x && exitPosition . y === y ) {
241+ content = ENTITY_EXIT ;
242+ className += ' bg-green-500 text-white' ;
243+ } else if ( enemyPositions . some ( ( ep ) => ep . x === x && ep . y === y ) ) {
244+ content = ENTITY_ENEMY ;
245+ className += ' bg-red-500 text-white' ;
246+ } else if ( tile === TILE_WALL ) {
247+ className += ' bg-gray-800 text-gray-600' ;
248+ } else {
249+ className += ' bg-gray-900 text-gray-500' ;
250+ }
251+
252+ return (
253+ < div key = { `${ x } -${ y } ` } className = { className } >
254+ { content }
255+ </ div >
256+ ) ;
257+ } ) ,
258+ ) }
259+ </ div >
260+ ) ;
261+ } ;
262+
263+ return (
264+ < div className = "flex flex-col items-center justify-center py-16 sm:py-24 text-gray-300" >
265+ < Link
266+ to = "/apps"
267+ className = "text-primary-400 hover:underline flex items-center justify-center gap-2 text-lg mb-4"
268+ >
269+ < ArrowLeftIcon size = { 24 } /> Back to Apps
270+ </ Link >
271+ < h1 className = "text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center" >
272+ < CubeIcon size = { 48 } className = "mr-2" /> Roguelike Adventure
273+ </ h1 >
274+ < p className = "text-lg text-gray-400 mb-6 text-center" >
275+ Navigate the maze, avoid enemies, and find the exit!
276+ </ p >
277+
278+ { gameStatus === 'won' && (
279+ < div className = "text-green-400 text-2xl font-bold mb-4" >
280+ You Won! 🎉
281+ < button
282+ onClick = { generateMap }
283+ className = "ml-4 px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-white text-lg"
284+ >
285+ Play Again
286+ </ button >
287+ </ div >
288+ ) }
289+ { gameStatus === 'lost' && (
290+ < div className = "text-red-400 text-2xl font-bold mb-4" >
291+ Game Over! 💀
292+ < button
293+ onClick = { generateMap }
294+ className = "ml-4 px-4 py-2 bg-red-600 hover:bg-red-700 rounded text-white text-lg"
295+ >
296+ Try Again
297+ </ button >
298+ </ div >
299+ ) }
300+
301+ < div className = "border-2 border-gray-600 p-2 relative" >
302+ { renderMap ( ) }
303+ </ div >
304+
305+ < div className = "mt-6 text-center" >
306+ < p > Use WASD Keys to Move</ p >
307+ < p className = "text-sm text-gray-500" >
308+ < span className = "text-blue-400" > @</ span > : Player,{ ' ' }
309+ < span className = "text-red-400" > E</ span > : Enemy,{ ' ' }
310+ < span className = "text-green-400" > X</ span > : Exit
311+ </ p >
312+ </ div >
313+ </ div >
314+ ) ;
315+ }
316+
317+ export default RoguelikeGamePage ;
0 commit comments