canvasRef.current?.focus()}
+ >
+
+
+ {maze?.title ?? ""}
+ Appelli: {"❤️".repeat(Math.max(0, hud.livesLeft))}
+ Punti: {hud.score}
+
+
+
+
+ {hud.cfu} / {targetCFU} CFU
+
+
+ {hud.goldenActive && (
+
+ ☕ Sessione d'oro!
+
+ )}
+
+
+
+
+
+ {!logoReady &&
Caricamento…
}
+
+ {hud.paused && hud.phase === "playing" && (
+
+ )}
+
+ {hud.phase === "intro" && (
+
+
Mangia i CFU e arriva a 180 per laurearti!
+
+
+ )}
+
+ {hud.phase === "levelComplete" && (
+
+
+ Anno superato! {hud.cfu} / {targetCFU} CFU
+
+
+
+ )}
+
+ {hud.phase === "gameOver" && (
+
+
Fuori corso…
+
+
+ )}
+
+ {hud.phase === "graduated" && (
+
+
+ 🎓 Dottore! {hud.cfu} CFU — punteggio {hud.score}
+
+
+
+ )}
+
+
+ {isTouch && hud.phase === "playing" && (
+
+
+
+
+
+
+ )}
+
+
+
+ 1 CFU
+
+
+ 3 CFU
+
+
+ 6 CFU
+
+
+ 12 CFU
+
+ ☕ sessione d'oro
+
+
+ )
+}
diff --git a/src/components/cfu-game/engine.ts b/src/components/cfu-game/engine.ts
new file mode 100644
index 0000000..de6320a
--- /dev/null
+++ b/src/components/cfu-game/engine.ts
@@ -0,0 +1,389 @@
+import { MAZES } from "./mazes"
+import { PALETTE } from "./palette"
+import { type CellType, CFU_VALUE_BY_CELL, type Direction, type EngineState, type GhostState, type Vec2 } from "./types"
+
+export const TILE = 24 // px logici per tile a scala 1
+const PLAYER_SPEED = 6.2 // tile/sec
+const GOLDEN_SESSION_MS = 7000
+const GOLDEN_WARNING_MS = 2000
+const STARTING_LIVES = 3
+const GHOST_COLORS = [PALETTE.red, PALETTE.green, PALETTE.blueSecondary, PALETTE.blueTertiary]
+
+let popupIdCounter = 0
+
+function cloneGrid(rows: string[]): CellType[][] {
+ return rows.map((row) => row.split("") as CellType[])
+}
+
+function findAll(grid: CellType[][], type: CellType): Vec2[] {
+ const found: Vec2[] = []
+ for (let y = 0; y < grid.length; y++) {
+ const row = grid[y]
+ if (!row) continue
+ for (let x = 0; x < row.length; x++) {
+ if (row[x] === type) found.push({ x, y })
+ }
+ }
+ return found
+}
+
+function cellAt(grid: CellType[][], x: number, y: number): CellType {
+ const row = grid[y]
+ if (!row) return "#"
+ return row[x] ?? "#"
+}
+
+function isWalkable(grid: CellType[][], x: number, y: number): boolean {
+ return cellAt(grid, x, y) !== "#"
+}
+
+function sumCfuInGrid(grid: CellType[][]): number {
+ let total = 0
+ for (const row of grid) {
+ for (const cell of row) {
+ total += CFU_VALUE_BY_CELL[cell] ?? 0
+ }
+ }
+ return total
+}
+
+const DIR_VECTORS: Record