@@ -15,7 +15,7 @@ if(!CanvasRenderingContext2D.prototype.roundRect){
1515var ctx = c . getContext ( '2d' ) , W = c . width , H = c . height ;
1616
1717/* ── Per-game leaderboards (top 10) ─────────────── */
18- var GNAMES = [ 'runner' , 'jetpack' , 'racer' , 'miner' , 'asteroids' ] ;
18+ var GNAMES = [ 'runner' , 'jetpack' , 'racer' , 'miner' , 'asteroids' , 'snake' , 'pacman' ] ;
1919var lbData = { } ;
2020GNAMES . forEach ( function ( g ) {
2121 var raw = localStorage . getItem ( 'cs404_lb_' + g ) ;
@@ -39,7 +39,7 @@ function renderLeaderboard(game){
3939 var panel = document . getElementById ( 'cs404-lb-body' ) ;
4040 var title = document . getElementById ( 'cs404-lb-title' ) ;
4141 if ( ! panel ) return ;
42- var gname = { runner :'Runner' , jetpack :'Jetpack' , racer :'Racer' , miner :'Miner' , asteroids :'Asteroids' } ;
42+ var gname = { runner :'Runner' , jetpack :'Jetpack' , racer :'Racer' , miner :'Miner' , asteroids :'Asteroids' , snake : 'Snake' , pacman : 'Pac-Man' } ;
4343 if ( title ) title . textContent = '\uD83C\uDFC6 ' + ( gname [ game ] || game ) + ' \u2014 Top 10' ;
4444 var lb = lbData [ game ] ;
4545 if ( ! lb || lb . length === 0 ) { panel . innerHTML = '<p class="cs404-lb-empty">No scores yet \u2014 be the first!</p>' ; return ; }
@@ -870,6 +870,302 @@ function asDraw(){
870870 el . addEventListener ( 'mouseup' , up ) ;
871871} ) ;
872872
873+ /* ═══════════════════════════════════════════════
874+ GAME 6 — SNAKE
875+ ═══════════════════════════════════════════════ */
876+ var SN_CELL = 20 , SN_COLS = 31 , SN_ROWS = 14 ;
877+ var snKeys = { up :false , dn :false , lt :false , rt :false } ;
878+ var SN = { run :false , over :false , score :0 , fr :0 , tick :8 , dir :{ x :1 , y :0 } , nxt :{ x :1 , y :0 } , seg :[ ] , apple :{ x :15 , y :7 } , newHi :false } ;
879+ function snPlace ( ) {
880+ var ok = false , ax , ay , i ;
881+ while ( ! ok ) {
882+ ax = 1 + Math . floor ( Math . random ( ) * ( SN_COLS - 2 ) ) ;
883+ ay = 1 + Math . floor ( Math . random ( ) * ( SN_ROWS - 2 ) ) ;
884+ ok = true ;
885+ for ( i = 0 ; i < SN . seg . length ; i ++ ) { if ( SN . seg [ i ] . x === ax && SN . seg [ i ] . y === ay ) { ok = false ; break ; } }
886+ }
887+ SN . apple = { x :ax , y :ay } ;
888+ }
889+ function snReset ( ) {
890+ SN . dir = { x :1 , y :0 } ; SN . nxt = { x :1 , y :0 } ;
891+ var mx = Math . floor ( SN_COLS / 2 ) , my = Math . floor ( SN_ROWS / 2 ) ;
892+ SN . seg = [ { x :mx , y :my } , { x :mx - 1 , y :my } , { x :mx - 2 , y :my } ] ;
893+ SN . score = 0 ; SN . fr = 0 ; SN . tick = 8 ; SN . newHi = false ;
894+ namePending = false ; particles = [ ] ;
895+ if ( nameOverlay ) nameOverlay . style . display = 'none' ;
896+ snPlace ( ) ; SN . run = true ; SN . over = false ;
897+ }
898+ function snUpdate ( ) {
899+ if ( ! SN . run || SN . over ) return ;
900+ SN . fr ++ ; if ( SN . fr < SN . tick ) return ; SN . fr = 0 ;
901+ // queue direction, block 180 reversal
902+ var nd = SN . nxt ;
903+ if ( ! ( nd . x === - SN . dir . x && nd . y === - SN . dir . y ) ) SN . dir = { x :nd . x , y :nd . y } ;
904+ var hd = SN . seg [ 0 ] , nx = hd . x + SN . dir . x , ny = hd . y + SN . dir . y , i ;
905+ if ( nx < 0 || nx >= SN_COLS || ny < 0 || ny >= SN_ROWS ) { snDie ( ) ; return ; }
906+ for ( i = 0 ; i < SN . seg . length ; i ++ ) { if ( SN . seg [ i ] . x === nx && SN . seg [ i ] . y === ny ) { snDie ( ) ; return ; } }
907+ SN . seg . unshift ( { x :nx , y :ny } ) ;
908+ if ( nx === SN . apple . x && ny === SN . apple . y ) {
909+ SN . score += 10 ;
910+ if ( SN . score % 50 === 0 && SN . tick > 3 ) SN . tick -- ;
911+ snPlace ( ) ;
912+ } else { SN . seg . pop ( ) ; }
913+ }
914+ function snDraw ( ) {
915+ ctx . fillStyle = 'rgba(8,20,40,0.96)' ; ctx . fillRect ( 0 , 0 , W , H ) ;
916+ // subtle grid
917+ ctx . fillStyle = 'rgba(42,96,144,0.15)' ;
918+ for ( var gx = 0 ; gx < SN_COLS ; gx ++ ) for ( var gy = 0 ; gy < SN_ROWS ; gy ++ ) { ctx . beginPath ( ) ; ctx . arc ( gx * SN_CELL + SN_CELL / 2 , gy * SN_CELL + SN_CELL / 2 , 1 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; }
919+ // apple
920+ ctx . fillStyle = '#ff3a3a' ; ctx . beginPath ( ) ; ctx . arc ( SN . apple . x * SN_CELL + SN_CELL / 2 , SN . apple . y * SN_CELL + SN_CELL / 2 , SN_CELL / 2 - 2 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
921+ ctx . fillStyle = '#55bb33' ; ctx . fillRect ( SN . apple . x * SN_CELL + SN_CELL / 2 , SN . apple . y * SN_CELL + 1 , 3 , 5 ) ;
922+ // snake body
923+ for ( var i = SN . seg . length - 1 ; i >= 0 ; i -- ) {
924+ var s = SN . seg [ i ] , t = i / Math . max ( SN . seg . length - 1 , 1 ) ;
925+ ctx . fillStyle = i === 0 ?'#44ff88' :( 'rgba(' + ( 54 + Math . round ( t * 60 ) ) + ',' + ( 160 - Math . round ( t * 60 ) ) + ',80,1)' ) ;
926+ ctx . beginPath ( ) ; ctx . roundRect ( s . x * SN_CELL + 2 , s . y * SN_CELL + 2 , SN_CELL - 4 , SN_CELL - 4 , 3 ) ; ctx . fill ( ) ;
927+ }
928+ // eyes on head
929+ if ( SN . seg . length > 0 ) {
930+ var h = SN . seg [ 0 ] ;
931+ var dx = SN . dir . x , dy = SN . dir . y ;
932+ var cx2 = h . x * SN_CELL + SN_CELL / 2 , cy2 = h . y * SN_CELL + SN_CELL / 2 ;
933+ var perp = { x :- dy , y :dx } ;
934+ ctx . fillStyle = '#111' ;
935+ ctx . beginPath ( ) ; ctx . arc ( cx2 + dx * 4 + perp . x * 3 , cy2 + dy * 4 + perp . y * 3 , 2 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
936+ ctx . beginPath ( ) ; ctx . arc ( cx2 + dx * 4 - perp . x * 3 , cy2 + dy * 4 - perp . y * 3 , 2 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
937+ }
938+ drawHiPanel ( 'snake' ) ; drawScore ( SN . score ) ; drawParticles ( ) ;
939+ if ( ! SN . run && ! SN . over ) drawWelcome ( 'Snake' , 'Arrow keys or d-pad \u2022 eat apples' ) ;
940+ if ( SN . over ) drawGameOver ( SN . score , SN . newHi ) ;
941+ }
942+ function snDie ( ) { SN . run = false ; SN . over = true ; SN . newHi = checkNewHi ( 'snake' , SN . score ) ; }
943+ // Shared 4-directional d-pad for Snake and Pac-Man
944+ ( function ( ) {
945+ var map = { '4up' :{ x :0 , y :- 1 } , '4dn' :{ x :0 , y :1 } , '4lt' :{ x :- 1 , y :0 } , '4rt' :{ x :1 , y :0 } } ;
946+ Object . keys ( map ) . forEach ( function ( id ) {
947+ var el = document . getElementById ( 'cs404-' + id ) ; if ( ! el ) return ;
948+ var dir = map [ id ] ;
949+ function press ( e ) {
950+ e . preventDefault ( ) ;
951+ if ( currentGame === 'snake' ) { if ( ! SN . run && ! SN . over ) snReset ( ) ; else if ( SN . over ) snReset ( ) ; else SN . nxt = { x :dir . x , y :dir . y } ; }
952+ else if ( currentGame === 'pacman' ) { if ( ! PM . run && ! PM . over && ! PM . win ) pmReset ( ) ; else if ( PM . over || PM . win ) pmReset ( ) ; else PM . pNxt = { x :dir . x , y :dir . y } ; }
953+ }
954+ el . addEventListener ( 'touchstart' , press , { passive :false } ) ; el . addEventListener ( 'mousedown' , press ) ;
955+ } ) ;
956+ } ) ( ) ;
957+
958+ /* ═══════════════════════════════════════════════
959+ GAME 7 — PAC-MAN
960+ ═══════════════════════════════════════════════ */
961+ var PM_CELL = 20 , PM_COLS = 31 , PM_ROWS = 14 ;
962+ // 31×14 maze: '#'=wall, '.'=dot, 'o'=power, ' '=open, 'G'=ghost-spawn
963+ var PM_MAP = [
964+ '#############################.#' ,
965+ '#....#.....#.....#.....#....#.#' ,
966+ '#.##.#.###.#.###.#.###.#.##.#.#' ,
967+ '#o#..#.###.#.###.#.###.#..#.#o#' ,
968+ '#.##.#.###.#.###.#.###.#.##.#.#' ,
969+ '#.............................#.' ,
970+ '##.##.##.### ###.##.##.####' ,
971+ '##.##.## GGGGGGGGGGG ##.##.####' ,
972+ '##.##.##.### ###.##.##.####' ,
973+ '#.............................#.' ,
974+ '#.##.#.###.#.###.#.###.#.##.#.#' ,
975+ '#o#..#.###.#.###.#.###.#..#.#o#' ,
976+ '#.##.#.###.#.###.#.###.#.##.#.#' ,
977+ '#....#.....#.....#.....#....#.#' ,
978+ ] ;
979+ // Validate & normalise to exactly PM_COLS chars per row
980+ ( function ( ) { for ( var i = 0 ; i < PM_MAP . length ; i ++ ) { while ( PM_MAP [ i ] . length < PM_COLS ) PM_MAP [ i ] += ' ' ; PM_MAP [ i ] = PM_MAP [ i ] . substring ( 0 , PM_COLS ) ; } } ) ( ) ;
981+ function pmCell ( r , c ) { if ( r < 0 || r >= PM_ROWS || c < 0 || c >= PM_COLS ) return '#' ; return PM_MAP [ r ] [ c ] ; }
982+ function pmIsWall ( r , c ) { return pmCell ( r , c ) === '#' ; }
983+ // Collect all dots and power pellets
984+ var PM = { run :false , over :false , win :false , score :0 , lives :3 , newHi :false ,
985+ px :0 , py :0 , pDir :{ x :1 , y :0 } , pNxt :{ x :1 , y :0 } , pMouth :0 , pMouthDir :1 ,
986+ dots :[ ] , total :0 , scared :0 ,
987+ ghosts :[ ] , ghostTimer :0 } ;
988+ var pmKeys = { up :false , dn :false , lt :false , rt :false } ;
989+ function pmInit ( ) {
990+ PM . dots = [ ] ; PM . total = 0 ;
991+ for ( var r = 0 ; r < PM_ROWS ; r ++ ) {
992+ for ( var c = 0 ; c < PM_COLS ; c ++ ) {
993+ var ch = PM_MAP [ r ] [ c ] ;
994+ if ( ch === '.' || ch === 'o' ) PM . dots . push ( { r :r , c :c , type :ch , eaten :false } ) ;
995+ }
996+ }
997+ PM . total = PM . dots . length ;
998+ }
999+ pmInit ( ) ;
1000+ function pmReset ( ) {
1001+ PM . score = 0 ; PM . lives = 3 ; PM . over = false ; PM . win = false ; PM . newHi = false ; PM . scared = 0 ;
1002+ namePending = false ; particles = [ ] ; if ( nameOverlay ) nameOverlay . style . display = 'none' ;
1003+ pmInit ( ) ;
1004+ PM . px = 15 * PM_CELL + PM_CELL / 2 ; PM . py = 13 * PM_CELL + PM_CELL / 2 ;
1005+ PM . pDir = { x :1 , y :0 } ; PM . pNxt = { x :1 , y :0 } ; PM . pMouth = 0.25 ; PM . pMouthDir = 1 ;
1006+ // 3 ghosts starting at row 7 col 10,15,20
1007+ PM . ghosts = [
1008+ { x :9 * PM_CELL + PM_CELL / 2 , y :7 * PM_CELL + PM_CELL / 2 , dir :{ x :1 , y :0 } , col :'#ff4444' , mode :'chase' , dead :false , ret :0 } ,
1009+ { x :14 * PM_CELL + PM_CELL / 2 , y :7 * PM_CELL + PM_CELL / 2 , dir :{ x :- 1 , y :0 } , col :'#ff88cc' , mode :'scatter' , dead :false , ret :0 } ,
1010+ { x :19 * PM_CELL + PM_CELL / 2 , y :7 * PM_CELL + PM_CELL / 2 , dir :{ x :1 , y :0 } , col :'#44ccff' , mode :'chase' , dead :false , ret :0 } ,
1011+ ] ;
1012+ PM . run = true ;
1013+ }
1014+ function pmTileRC ( px , py ) { return { r :Math . round ( ( py - PM_CELL / 2 ) / PM_CELL ) , c :Math . round ( ( px - PM_CELL / 2 ) / PM_CELL ) } ; }
1015+ function pmCanMove ( px , py , dx , dy , spd ) {
1016+ var nx = px + dx * spd , ny = py + dy * spd ;
1017+ // sample corners
1018+ var off = PM_CELL / 2 - 3 ;
1019+ function blocked ( x , y ) {
1020+ var r = Math . floor ( y / PM_CELL ) , c = Math . floor ( x / PM_CELL ) ;
1021+ return pmIsWall ( r , c ) ;
1022+ }
1023+ if ( dx !== 0 ) {
1024+ var cx = ( dx > 0 ?nx + off :nx - off ) ;
1025+ return ! blocked ( cx , ny - off ) && ! blocked ( cx , ny + off ) ;
1026+ } else {
1027+ var cy = ( dy > 0 ?ny + off :ny - off ) ;
1028+ return ! blocked ( nx - off , cy ) && ! blocked ( nx + off , cy ) ;
1029+ }
1030+ }
1031+ function pmMoveGhost ( g ) {
1032+ if ( g . dead ) return ;
1033+ var spd = PM . scared > 0 ?1 :1.5 ;
1034+ // every ~16px of movement, choose new dir at intersections
1035+ var tc = pmTileRC ( g . x , g . y ) ;
1036+ var cx = tc . c * PM_CELL + PM_CELL / 2 , cy = tc . r * PM_CELL + PM_CELL / 2 ;
1037+ var atCenter = ( Math . abs ( g . x - cx ) < spd + 1 && Math . abs ( g . y - cy ) < spd + 1 ) ;
1038+ if ( atCenter ) {
1039+ g . x = cx ; g . y = cy ;
1040+ // pick best dir toward/away from pac or random
1041+ var dirs = [ { x :1 , y :0 } , { x :- 1 , y :0 } , { x :0 , y :1 } , { x :0 , y :- 1 } ] ;
1042+ var best = null , bestScore = PM . scared > 0 ?- Infinity :Infinity ;
1043+ var pr = pmTileRC ( PM . px , PM . py ) ;
1044+ dirs . forEach ( function ( d ) {
1045+ if ( d . x === - g . dir . x && d . y === - g . dir . y ) return ; // no U-turn
1046+ if ( pmIsWall ( tc . r + d . y , tc . c + d . x ) ) return ;
1047+ var dr = tc . r + d . y - pr . r , dc = tc . c + d . x - pr . c ;
1048+ var dist = dr * dr + dc * dc ;
1049+ if ( PM . scared > 0 ) { if ( dist > bestScore ) { bestScore = dist ; best = d ; } }
1050+ else { if ( dist < bestScore ) { bestScore = dist ; best = d ; } }
1051+ } ) ;
1052+ if ( ! best ) { dirs . forEach ( function ( d ) { if ( ! ( d . x === - g . dir . x && d . y === - g . dir . y ) && ! pmIsWall ( tc . r + d . y , tc . c + d . x ) ) best = d ; } ) ; }
1053+ if ( best ) g . dir = best ;
1054+ }
1055+ g . x += g . dir . x * spd ; g . y += g . dir . y * spd ;
1056+ // clamp
1057+ if ( g . x < PM_CELL / 2 ) g . x = PM_CELL / 2 ; if ( g . x > PM_COLS * PM_CELL - PM_CELL / 2 ) g . x = PM_COLS * PM_CELL - PM_CELL / 2 ;
1058+ if ( g . y < PM_CELL / 2 ) g . y = PM_CELL / 2 ; if ( g . y > PM_ROWS * PM_CELL - PM_CELL / 2 ) g . y = PM_ROWS * PM_CELL - PM_CELL / 2 ;
1059+ }
1060+ function pmUpdate ( ) {
1061+ if ( ! PM . run || PM . over || PM . win ) return ;
1062+ // pac movement
1063+ var spd = 2 ;
1064+ var nd = PM . pNxt ;
1065+ if ( pmCanMove ( PM . px , PM . py , nd . x , nd . y , spd ) ) PM . pDir = { x :nd . x , y :nd . y } ;
1066+ if ( pmCanMove ( PM . px , PM . py , PM . pDir . x , PM . pDir . y , spd ) ) { PM . px += PM . pDir . x * spd ; PM . py += PM . pDir . y * spd ; }
1067+ PM . px = Math . max ( PM_CELL / 2 , Math . min ( PM_COLS * PM_CELL - PM_CELL / 2 , PM . px ) ) ;
1068+ PM . py = Math . max ( PM_CELL / 2 , Math . min ( PM_ROWS * PM_CELL - PM_CELL / 2 , PM . py ) ) ;
1069+ // mouth animation
1070+ PM . pMouth += 0.07 * PM . pMouthDir ; if ( PM . pMouth > 0.35 || PM . pMouth < 0.02 ) PM . pMouthDir *= - 1 ;
1071+ // eat dots
1072+ var pr = pmTileRC ( PM . px , PM . py ) ;
1073+ PM . dots . forEach ( function ( d ) {
1074+ if ( ! d . eaten && d . r === pr . r && d . c === pr . c ) {
1075+ d . eaten = true ;
1076+ if ( d . type === 'o' ) { PM . score += 50 ; PM . scared = 240 ; }
1077+ else PM . score += 10 ;
1078+ }
1079+ } ) ;
1080+ // check win
1081+ if ( PM . dots . every ( function ( d ) { return d . eaten ; } ) ) { PM . win = true ; PM . run = false ; PM . newHi = checkNewHi ( 'pacman' , PM . score ) ; return ; }
1082+ // ghosts
1083+ if ( PM . scared > 0 ) PM . scared -- ;
1084+ PM . ghosts . forEach ( function ( g ) {
1085+ pmMoveGhost ( g ) ;
1086+ // collision with pac
1087+ var dx = g . x - PM . px , dy = g . y - PM . py ;
1088+ if ( Math . sqrt ( dx * dx + dy * dy ) < PM_CELL * 0.75 ) {
1089+ if ( PM . scared > 0 && ! g . dead ) {
1090+ g . dead = true ; PM . score += 200 ;
1091+ } else if ( ! g . dead ) {
1092+ PM . lives -- ;
1093+ if ( PM . lives <= 0 ) { PM . run = false ; PM . over = true ; PM . newHi = checkNewHi ( 'pacman' , PM . score ) ; }
1094+ else { // respawn pac
1095+ PM . px = 15 * PM_CELL + PM_CELL / 2 ; PM . py = 13 * PM_CELL + PM_CELL / 2 ;
1096+ PM . pDir = { x :1 , y :0 } ; PM . pNxt = { x :1 , y :0 } ; PM . scared = 0 ;
1097+ }
1098+ }
1099+ }
1100+ // revive dead ghost after a moment
1101+ if ( g . dead ) { g . ret ++ ; if ( g . ret > 120 ) { g . dead = false ; g . ret = 0 ; } }
1102+ } ) ;
1103+ }
1104+ function pmDraw ( ) {
1105+ ctx . fillStyle = '#000' ; ctx . fillRect ( 0 , 0 , W , H ) ;
1106+ // maze
1107+ for ( var r = 0 ; r < PM_ROWS ; r ++ ) {
1108+ for ( var c = 0 ; c < PM_COLS ; c ++ ) {
1109+ var ch = PM_MAP [ r ] [ c ] ;
1110+ if ( ch === '#' ) {
1111+ ctx . fillStyle = '#1a3acc' ;
1112+ ctx . fillRect ( c * PM_CELL , r * PM_CELL , PM_CELL , PM_CELL ) ;
1113+ ctx . strokeStyle = '#3355ff' ; ctx . lineWidth = 1 ;
1114+ ctx . strokeRect ( c * PM_CELL + 0.5 , r * PM_CELL + 0.5 , PM_CELL - 1 , PM_CELL - 1 ) ;
1115+ }
1116+ }
1117+ }
1118+ // dots
1119+ PM . dots . forEach ( function ( d ) {
1120+ if ( d . eaten ) return ;
1121+ if ( d . type === 'o' ) {
1122+ ctx . fillStyle = '#ffcc00' ; ctx . beginPath ( ) ; ctx . arc ( d . c * PM_CELL + PM_CELL / 2 , d . r * PM_CELL + PM_CELL / 2 , 4 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
1123+ } else {
1124+ ctx . fillStyle = '#ffeecc' ; ctx . beginPath ( ) ; ctx . arc ( d . c * PM_CELL + PM_CELL / 2 , d . r * PM_CELL + PM_CELL / 2 , 2 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
1125+ }
1126+ } ) ;
1127+ // pac
1128+ var ang = Math . atan2 ( PM . pDir . y , PM . pDir . x ) ;
1129+ ctx . fillStyle = '#ffff00' ;
1130+ ctx . beginPath ( ) ; ctx . moveTo ( PM . px , PM . py ) ;
1131+ ctx . arc ( PM . px , PM . py , PM_CELL / 2 - 2 , ang + PM . pMouth * Math . PI , ang + ( 2 - PM . pMouth ) * Math . PI ) ;
1132+ ctx . closePath ( ) ; ctx . fill ( ) ;
1133+ // ghosts
1134+ PM . ghosts . forEach ( function ( g ) {
1135+ if ( g . dead ) {
1136+ ctx . fillStyle = 'rgba(200,200,255,0.3)' ; ctx . beginPath ( ) ; ctx . arc ( g . x , g . y , PM_CELL / 2 - 3 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; return ;
1137+ }
1138+ var col = PM . scared > 0 ?( PM . scared < 60 && Math . floor ( PM . scared / 8 ) % 2 === 0 ?'#fff' :'#2244ff' ) :g . col ;
1139+ ctx . fillStyle = col ;
1140+ // ghost body
1141+ ctx . beginPath ( ) ; ctx . arc ( g . x , g . y - 2 , PM_CELL / 2 - 2 , Math . PI , 0 ) ;
1142+ ctx . lineTo ( g . x + PM_CELL / 2 - 2 , g . y + PM_CELL / 2 - 2 ) ;
1143+ var bw = ( PM_CELL - 4 ) / 3 ;
1144+ for ( var i = 0 ; i < 3 ; i ++ ) { ctx . lineTo ( g . x + PM_CELL / 2 - 2 - i * bw , g . y + PM_CELL / 2 - 2 - ( i % 2 === 0 ?4 :0 ) ) ; ctx . lineTo ( g . x + PM_CELL / 2 - 2 - ( i + 1 ) * bw , g . y + PM_CELL / 2 - 2 - ( i % 2 === 1 ?4 :0 ) ) ; }
1145+ ctx . lineTo ( g . x - ( PM_CELL / 2 - 2 ) , g . y + PM_CELL / 2 - 2 ) ; ctx . closePath ( ) ; ctx . fill ( ) ;
1146+ // eyes (skip when scared)
1147+ if ( PM . scared === 0 ) {
1148+ ctx . fillStyle = '#fff' ; ctx . beginPath ( ) ; ctx . arc ( g . x - 3 , g . y - 3 , 3 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; ctx . beginPath ( ) ; ctx . arc ( g . x + 3 , g . y - 3 , 3 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
1149+ ctx . fillStyle = '#00f' ; ctx . beginPath ( ) ; ctx . arc ( g . x - 3 , g . y - 3 , 1.5 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; ctx . beginPath ( ) ; ctx . arc ( g . x + 3 , g . y - 3 , 1.5 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ;
1150+ }
1151+ } ) ;
1152+ // HUD
1153+ ctx . save ( ) ; ctx . fillStyle = 'rgba(0,0,0,0.6)' ; ctx . fillRect ( 0 , 0 , W , 18 ) ;
1154+ ctx . font = 'bold 11px monospace' ; ctx . fillStyle = '#ffff00' ; ctx . textAlign = 'left' ; ctx . fillText ( 'LIVES: ' + '\u2665' . repeat ( PM . lives ) , 8 , 13 ) ;
1155+ ctx . fillStyle = '#fff' ; ctx . textAlign = 'right' ; ctx . fillText ( String ( PM . score ) . padStart ( 6 , '0' ) , W - 8 , 13 ) ;
1156+ var lb = lbData [ 'pacman' ] ; if ( lb && lb . length > 0 ) { ctx . fillStyle = '#ffcc00' ; ctx . textAlign = 'center' ; ctx . font = '9px monospace' ; ctx . fillText ( '\uD83C\uDFC6 ' + ( lb [ 0 ] . n || 'Anonymous' ) + ' ' + lb [ 0 ] . s , W / 2 , 13 ) ; }
1157+ ctx . restore ( ) ;
1158+ drawParticles ( ) ;
1159+ if ( ! PM . run && ! PM . over && ! PM . win ) drawWelcome ( 'Pac-Man' , 'Arrow keys or d-pad \u2022 eat all dots' ) ;
1160+ if ( PM . win ) {
1161+ ctx . fillStyle = 'rgba(0,0,0,0.7)' ; ctx . fillRect ( 0 , 0 , W , H ) ;
1162+ ctx . textAlign = 'center' ; ctx . fillStyle = '#ffff00' ; ctx . font = 'bold 18px monospace' ; ctx . fillText ( 'YOU WIN!' , W / 2 , H / 2 - 12 ) ;
1163+ ctx . fillStyle = '#fff' ; ctx . font = '12px monospace' ; ctx . fillText ( 'Score: ' + PM . score , W / 2 , H / 2 + 8 ) ;
1164+ ctx . fillStyle = '#f57c00' ; ctx . font = 'bold 12px monospace' ; ctx . fillText ( 'SPACE or TAP to play again' , W / 2 , H / 2 + 28 ) ;
1165+ }
1166+ if ( PM . over ) drawGameOver ( PM . score , PM . newHi ) ;
1167+ }
1168+
8731169/* ── Input ──────────────────────────────────────── */
8741170var keysDown = { } ;
8751171document . addEventListener ( 'keydown' , function ( e ) {
@@ -898,6 +1194,22 @@ document.addEventListener('keydown',function(e){
8981194 }
8991195 return ;
9001196 }
1197+ if ( currentGame === 'snake' ) {
1198+ if ( e . code === 'ArrowUp' || e . code === 'KeyW' ) { e . preventDefault ( ) ; SN . nxt = { x :0 , y :- 1 } ; }
1199+ else if ( e . code === 'ArrowDown' || e . code === 'KeyS' ) { e . preventDefault ( ) ; SN . nxt = { x :0 , y :1 } ; }
1200+ else if ( e . code === 'ArrowLeft' || e . code === 'KeyA' ) { e . preventDefault ( ) ; SN . nxt = { x :- 1 , y :0 } ; }
1201+ else if ( e . code === 'ArrowRight' || e . code === 'KeyD' ) { e . preventDefault ( ) ; SN . nxt = { x :1 , y :0 } ; }
1202+ if ( e . code === 'Space' ) { if ( ! SN . run && ! SN . over ) snReset ( ) ; else if ( SN . over ) snReset ( ) ; }
1203+ return ;
1204+ }
1205+ if ( currentGame === 'pacman' ) {
1206+ if ( e . code === 'ArrowUp' || e . code === 'KeyW' ) { e . preventDefault ( ) ; PM . pNxt = { x :0 , y :- 1 } ; }
1207+ else if ( e . code === 'ArrowDown' || e . code === 'KeyS' ) { e . preventDefault ( ) ; PM . pNxt = { x :0 , y :1 } ; }
1208+ else if ( e . code === 'ArrowLeft' || e . code === 'KeyA' ) { e . preventDefault ( ) ; PM . pNxt = { x :- 1 , y :0 } ; }
1209+ else if ( e . code === 'ArrowRight' || e . code === 'KeyD' ) { e . preventDefault ( ) ; PM . pNxt = { x :1 , y :0 } ; }
1210+ if ( e . code === 'Space' ) { if ( ! PM . run && ! PM . over && ! PM . win ) pmReset ( ) ; else if ( PM . over || PM . win ) pmReset ( ) ; }
1211+ return ;
1212+ }
9011213 if ( e . code === 'Space' || e . key === ' ' ) {
9021214 var el = document . getElementById ( 'cs404-game' ) ;
9031215 if ( el ) { var r = el . getBoundingClientRect ( ) ; if ( r . top < window . innerHeight && r . bottom > 0 ) { e . preventDefault ( ) ; onAction ( ) ; } }
@@ -943,11 +1255,14 @@ function onAction(){
9431255 else if ( currentGame === 'racer' ) { if ( ! RC . run && ! RC . over ) rcReset ( ) ; else if ( RC . over ) rcReset ( ) ; }
9441256 else if ( currentGame === 'miner' ) { if ( ! MM . run && ! MM . over ) mmReset ( ) ; else if ( MM . over ) mmReset ( ) ; }
9451257 else if ( currentGame === 'asteroids' ) { if ( ! AS . run && ! AS . over ) asReset ( ) ; else if ( AS . over ) asReset ( ) ; }
1258+ else if ( currentGame === 'snake' ) { if ( ! SN . run && ! SN . over ) snReset ( ) ; else if ( SN . over ) snReset ( ) ; }
1259+ else if ( currentGame === 'pacman' ) { if ( ! PM . run && ! PM . over && ! PM . win ) pmReset ( ) ; else if ( PM . over || PM . win ) pmReset ( ) ; }
9461260}
9471261
9481262/* ── Tab switching ──────────────────────────────── */
9491263var mcCtrl = document . getElementById ( 'cs404-miner-ctrl' ) ;
9501264var asCtrl = document . getElementById ( 'cs404-asteroids-ctrl' ) ;
1265+ var d4Ctrl = document . getElementById ( 'cs404-4dir-ctrl' ) ;
9511266document . querySelectorAll ( '.cs404-tab' ) . forEach ( function ( tab ) {
9521267 tab . addEventListener ( 'click' , function ( ) {
9531268 currentGame = tab . getAttribute ( 'data-game' ) ;
@@ -959,6 +1274,7 @@ document.querySelectorAll('.cs404-tab').forEach(function(tab){
9591274 asKeys . left = false ; asKeys . right = false ; asKeys . up = false ; asKeys . shoot = false ;
9601275 if ( mcCtrl ) mcCtrl . style . display = currentGame === 'miner' ?'flex' :'none' ;
9611276 if ( asCtrl ) asCtrl . style . display = currentGame === 'asteroids' ?'flex' :'none' ;
1277+ if ( d4Ctrl ) d4Ctrl . style . display = ( currentGame === 'snake' || currentGame === 'pacman' ) ?'grid' :'none' ;
9621278 renderLeaderboard ( currentGame ) ;
9631279 } ) ;
9641280} ) ;
@@ -970,6 +1286,8 @@ function loop(){
9701286 else if ( currentGame === 'racer' ) { rcUpdate ( ) ; rcDraw ( ) ; }
9711287 else if ( currentGame === 'miner' ) { mmUpdate ( ) ; mmDraw ( ) ; }
9721288 else if ( currentGame === 'asteroids' ) { asUpdate ( ) ; asDraw ( ) ; }
1289+ else if ( currentGame === 'snake' ) { snUpdate ( ) ; snDraw ( ) ; }
1290+ else if ( currentGame === 'pacman' ) { pmUpdate ( ) ; pmDraw ( ) ; }
9731291 updateParticles ( ) ;
9741292 requestAnimationFrame ( loop ) ;
9751293}
0 commit comments