// Written in TypeScript. import axios from "axios"; import Vue from "vue"; const WALL = 1; const PLAYER = 2; const ENEMY = 3; const EXIT = 6; const VOID = 4; const FLOOR = 5; const WALKABLE = [VOID, FLOOR]; const ALL_COORDS: Array<{ x: number, y: number }> = []; let INITIAL_GRID = []; new Vue({ el: '#app', data: { grid: [], enemies: {} as { [key: string]: { played: number, direction: number } }, turn: 0, timeout: 1 }, async created() { // Level made with TilEd const response = await axios.get('./level.json', { responseType: 'json' }); const layer = response.data.layers[0]; for (let y = 0; y < layer.height; y++) { this.grid.push(response.data.layers[0].data.slice(y * layer.width, (y + 1) * layer.width)); for (let x = 0; x < layer.width; x++) { ALL_COORDS.push({ x, y }); } } INITIAL_GRID = JSON.parse(JSON.stringify((this.grid))); document.addEventListener('keydown', this.onKeyDown.bind(this)); }, methods: { move(dx: number, dy: number) { for (const { x, y } of ALL_COORDS) { if (this.grid[y][x] === PLAYER) { const target = { x, y }; target.x += dx; target.y += dy; if (WALKABLE.includes(this.grid[target.y][target.x])) { this.set(x, y, WALL); this.set(target.x, target.y, PLAYER); this.enemyTurn(); } else if (this.grid[target.y][target.x] === ENEMY) { this.set(x, y, ENEMY); this.restart(); } else if (this.grid[target.y][target.x] === EXIT) { this.set(x, y, FLOOR); this.outro(); } break; } } }, outro() { ALL_COORDS.forEach(({x, y}) => { const neighbors = this.countNeighbors(x, y); // Any live cell with fewer than two live neighbours dies, as if by underpopulation. // Any live cell with two or three live neighbours lives on to the next generation. // Any live cell with more than three live neighbours dies, as if by overpopulation. // Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. if (this.grid[y][x] === WALL && (neighbors < 2 || neighbors > 3)) { this.set(x, y, VOID); } if (this.grid[y][x] !== WALL && neighbors === 3) { this.set(x, y, WALL); } }); setTimeout(() => { this.outro(); }, 100); }, countNeighbors(x, y) { let n = 0; for (let nx = x - 1; nx <= x + 1; nx++) { for (let ny = y - 1; ny <= y + 1; ny++) { if ((nx != x || ny != y) && nx >= 0 && nx < this.grid[0].length && ny >= 0 && ny < this.grid.length) { if (this.grid[ny][nx] === WALL) { n++; } } } } return n; }, set(x, y, v) { this.grid[y].splice(x, 1, v); // Use splice to let Vue detect the change }, enemyTurn() { this.turn++; for (const { x, y } of ALL_COORDS) { if (this.grid[y][x] === ENEMY) { const key = x + this.grid.length * y; if (!this.enemies[key]) { this.enemies[key] = { direction: -1, turn: -1 }; } if (this.enemies[key].turn < this.turn) { const dy = this.enemies[key].direction; if (WALKABLE.includes(this.grid[y + dy][x])) { this.set(x, y, FLOOR); this.set(x, y + dy, ENEMY); const newKey = x + this.grid.length * (y + dy); this.enemies[newKey] = { direction: dy, turn: this.turn }; } else if (this.grid[y + dy][x] === PLAYER) { this.set(x, y + dy, ENEMY); this.restart(); } else { this.enemies[key].direction *= -1; } } } } }, restart(delay?: number) { if (this.timeout) { clearTimeout(this.timeout); this.timeout = undefined; } this.timeout = setTimeout(() => { this.grid = JSON.parse(JSON.stringify(INITIAL_GRID)); }, delay || 1000); }, onKeyDown(e: KeyboardEvent) { if (['ArrowLeft', 'q', 'a'].includes(e.key)) { this.move(-1, 0); } else if (['ArrowRight', 'd'].includes(e.key)) { this.move(1, 0); } else if (['ArrowUp', 'z', 'w'].includes(e.key)) { this.move(0, -1); } else if (['ArrowDown', 's'].includes(e.key)) { this.move(0, 1); } else if (e.key === 'r') { this.restart(1); } else if (!e.key.startsWith('F')) { e.preventDefault(); console.log(e.key); } } } });