class Cell { constructor (grid, index, x, y) { this.grid = grid; this.index = index; this.x = x; this.y = y; this.open = false; this.bomb = false; this.flagged = false; this.query = false; this.exploded = false; // 0 = empty, 1,2,3,4,5,6,7,8 = number of adjacent bombs this.value = 0; this.tile = grid.scene.make.image({ key: 'tiles', frame: 0, x: grid.offset.x + (x * 16), y: grid.offset.y + (y * 16), origin: 0 }); grid.board.add(this.tile); this.tile.setInteractive(); this.tile.on('pointerdown', this.onPointerDown, this); this.tile.on('pointerup', this.onPointerUp, this); } reset () { this.open = false; this.bomb = false; this.flagged = false; this.query = false; this.exploded = false; this.value = 0; this.tile.setFrame(0); } onPointerDown (pointer) { if (!this.grid.populated) { this.grid.generate(this.index); } if (this.open || !this.grid.playing) { return; } if (pointer.rightButtonDown()) { if (this.query) { this.query = false; this.tile.setFrame(0); } else if (this.flagged) { this.query = true; this.flagged = false; this.grid.updateBombs(-1); this.tile.setFrame(3); } else if (!this.flagged) { this.flagged = true; this.tile.setFrame(2); this.grid.updateBombs(1); this.grid.checkWinState(); } } else if (!this.flagged && !this.query) { this.onClick(); } } onClick () { if (this.bomb) { this.exploded = true; this.grid.gameOver(); } else { if (this.value === 0) { this.grid.floodFill(this.x, this.y); } else { this.show(); } this.grid.button.setFrame(2); this.grid.checkWinState(); } } onPointerUp () { if (this.grid.button.frame.name === 2) { this.grid.button.setFrame(0); } } reveal () { if (this.exploded) { this.tile.setFrame(6); } else if (!this.bomb && (this.flagged || this.query)) { this.tile.setFrame(7); } else if (this.bomb) { this.tile.setFrame(5); } else { this.show(); } } show () { const values = [ 1, 8, 9, 10, 11, 12, 13, 14, 15 ]; this.tile.setFrame(values[this.value]); this.open = true; } debug () { const values = [ '⬜️', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣' ]; if (this.bomb) { return '💣'; } else { return values[this.value]; } } } class Grid { constructor (scene, width, height, bombs) { this.scene = scene; this.width = width; this.height = height; this.size = width * height; this.offset = new Phaser.Math.Vector2(12, 55); this.timeCounter = 0; this.bombQty = bombs; this.bombsCounter = bombs; this.playing = false; this.populated = false; this.timer = scene.time.addEvent(); // 0 = waiting to create the grid // 1 = playing // 2 = game won // 3 = game lost this.state = 0; this.data = []; const x = Math.floor((scene.scale.width / 2) - (20 + (width * 16)) / 2); const y = Math.floor((scene.scale.height / 2) - (63 + (height * 16)) / 2); this.board = scene.add.container(x, y); this.digit1; this.digit2; this.digit3; this.time1; this.time2; this.time3; this.button; this.createBackground(); this.createCells(); this.updateDigits(); this.button.setInteractive(); this.button.on('pointerdown', this.onButtonDown, this); this.button.on('pointerup', this.onButtonUp, this); } createCells () { let i = 0; for (let x = 0; x < this.width; x++) { this.data[x] = []; for (let y = 0; y < this.height; y++) { this.data[x][y] = new Cell(this, i, x, y); i++; } } } createBackground () { const board = this.board; const factory = this.scene.add; // 55 added to the top, 8 added to the bottom (63) // 12 added to the left, 8 added to the right (20) // cells start at 12 x 55 const width = this.width * 16; const height = this.height * 16; // Top board.add(factory.image(0, 0, 'topLeft').setOrigin(0)); const topBgWidth = (width + 20) - 60 - 56; board.add(factory.tileSprite(60, 0, topBgWidth, 55, 'topBg').setOrigin(0)); board.add(factory.image(width + 20, 0, 'topRight').setOrigin(1, 0)); // Sides const sideHeight = (height + 63) - 55 - 8; board.add(factory.tileSprite(0, 55, 12, sideHeight, 'left').setOrigin(0)); board.add(factory.tileSprite(width + 20, 55, 8, sideHeight, 'right').setOrigin(1, 0)); // Bottom board.add(factory.image(0, height + 63, 'botLeft').setOrigin(0, 1)); const botBgWidth = (width + 20) - 12 - 8; board.add(factory.tileSprite(12, height + 63, botBgWidth, 8, 'botBg').setOrigin(0, 1)); board.add(factory.image(width + 20, height + 63, 'botRight').setOrigin(1, 1)); // Bombs Digits this.digit1 = factory.image(17, 16, 'digits', 0).setOrigin(0); this.digit2 = factory.image(17 + 13, 16, 'digits', 0).setOrigin(0); this.digit3 = factory.image(17 + 26, 16, 'digits', 0).setOrigin(0); board.add([ this.digit1, this.digit2, this.digit3 ]); // Timer Digits const x = (width + 20) - 54; this.time1 = factory.image(x, 16, 'digits', 0).setOrigin(0); this.time2 = factory.image(x + 13, 16, 'digits', 0).setOrigin(0); this.time3 = factory.image(x + 26, 16, 'digits', 0).setOrigin(0); board.add([ this.time1, this.time2, this.time3 ]); // Button const buttonX = Math.floor(((width + 20) / 2) - 13); this.button = factory.image(buttonX, 15, 'buttons', 0).setOrigin(0); board.add(this.button); } updateBombs (diff) { this.bombsCounter -= diff; this.updateDigits(); } updateDigits () { const count = Phaser.Utils.String.Pad(this.bombsCounter.toString(), 3, '0', 1); this.digit1.setFrame(parseInt(count[0])); this.digit2.setFrame(parseInt(count[1])); this.digit3.setFrame(parseInt(count[2])); } onButtonDown () { this.button.setFrame(1); } onButtonUp () { if (this.state > 0) { this.button.setFrame(0); this.restart(); } } restart () { this.populated = false; this.playing = false; this.bombsCounter = this.bombQty; this.state = 0; this.timeCounter = -1; this.timer.paused = true; let location = 0; do { this.getCell(location).reset(); location++; } while (location < this.size); this.updateDigits(); this.time1.setFrame(0); this.time2.setFrame(0); this.time3.setFrame(0); } gameOver () { this.playing = false; this.state = 3; this.timer.paused = true; this.button.setFrame(4); let location = 0; do { this.getCell(location).reveal(); location++; } while (location < this.size); } gameWon () { this.playing = false; this.state = 2; this.timer.paused = true; this.button.setFrame(3); } checkWinState () { let correct = 0; let location = 0; let open = 0; do { const cell = this.getCell(location); if (cell.open) { open++; } if (cell.bomb && cell.flagged) { open++; correct++; } location++; } while (location < this.size); // console.log('Check', correct, 'out of', this.bombQty, 'open', open, 'of', this.size); if (correct === this.bombQty && open === this.size) { this.gameWon(); } } generate (startIndex) { let qty = this.bombQty; const bombs = []; do { const location = Phaser.Math.Between(0, this.size - 1); const cell = this.getCell(location); if (!cell.bomb && cell.index !== startIndex) { cell.bomb = true; qty--; bombs.push(cell); } } while (qty > 0); bombs.forEach(cell => { // Update the 8 cells around this bomb cell const adjacent = this.getAdjacentCells(cell); adjacent.forEach(adjacentCell => { if (adjacentCell) { adjacentCell.value++; } }); }); this.playing = true; this.populated = true; this.state = 1; this.timer.reset({ delay: 1000, callback: this.onTimer, callbackScope: this, loop: true }); this.debug(); } onTimer () { this.timeCounter++; if (this.timeCounter < 1000) { const count = Phaser.Utils.String.Pad(this.timeCounter.toString(), 3, '0', 1); this.time1.setFrame(parseInt(count[0])); this.time2.setFrame(parseInt(count[1])); this.time3.setFrame(parseInt(count[2])); } } getCell (index) { const pos = Phaser.Math.ToXY(index, this.width, this.height); return this.data[pos.x][pos.y]; } getCellXY (x, y) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return null; } return this.data[x][y]; } getAdjacentCells (cell) { return [ // Top-Left, Top-Middle, Top-Right this.getCellXY(cell.x - 1, cell.y - 1), this.getCellXY(cell.x, cell.y - 1), this.getCellXY(cell.x + 1, cell.y - 1), // Left, Right this.getCellXY(cell.x - 1, cell.y), this.getCellXY(cell.x + 1, cell.y), // Bottom-Left, Bottom-Middle, Bottom-Right this.getCellXY(cell.x - 1, cell.y + 1), this.getCellXY(cell.x, cell.y + 1), this.getCellXY(cell.x + 1, cell.y + 1) ]; } floodFill (x, y) { const cell = this.getCellXY(x, y); if (cell && !cell.open && !cell.bomb) { cell.show(); if (cell.value === 0) { this.floodFill(x, y - 1); this.floodFill(x, y + 1); this.floodFill(x - 1, y); this.floodFill(x + 1, y); } } } debug () { for (let y = 0; y < this.height; y++) { let row = ''; for (let x = 0; x < this.width; x++) { let cell = this.data[x][y]; if (x === 0) { row = row.concat(`|`); } row = row.concat(`${cell.debug()}|`); } console.log(row); } console.log(''); } } class Intro extends Phaser.Scene { constructor () { super(); } preload () { this.load.setBaseURL('https://cdn.phaserfiles.com/v355'); this.load.setPath('assets/games/minesweeper/'); this.load.spritesheet('tiles', 'tiles.png', { frameWidth: 16 }); this.load.spritesheet('digits', 'digits.png', { frameWidth: 13, frameHeight: 23, endFrame: 9 }); this.load.spritesheet('buttons', 'digits.png', { frameWidth: 26, frameHeight: 26, startFrame: 5 }); this.load.image('topLeft', 'top-left.png'); this.load.image('topRight', 'top-right.png'); this.load.image('topBg', 'top-bg.png'); this.load.image('botLeft', 'bot-left.png'); this.load.image('botRight', 'bot-right.png'); this.load.image('botBg', 'bot-bg.png'); this.load.image('left', 'left.png'); this.load.image('right', 'right.png'); this.load.image('intro', 'intro.png'); this.load.image('win95', 'win95.png'); } create () { // this.input.mouse.disableContextMenu(); this.highlight = this.add.rectangle(0, 334, 800, 70, 0x0182fb).setOrigin(0).setAlpha(0.75); this.intro = this.add.image(0, 0, 'intro').setOrigin(0); const zone1 = this.add.zone(0, 334, 800, 70).setOrigin(0); const zone2 = this.add.zone(0, 411, 800, 70).setOrigin(0); const zone3 = this.add.zone(0, 488, 800, 70).setOrigin(0); zone1.setInteractive(); zone2.setInteractive(); zone3.setInteractive(); zone1.on('pointerover', () => { this.highlight.y = zone1.y; }); zone2.on('pointerover', () => { this.highlight.y = zone2.y; }); zone3.on('pointerover', () => { this.highlight.y = zone3.y; }); zone1.once('pointerdown', () => { this.startGame(9, 9, 10); }); zone2.once('pointerdown', () => { this.startGame(16, 16, 40); }); zone3.once('pointerdown', () => { this.startGame(16, 30, 99); }); } startGame (width, height, bombs) { this.scene.start('MineSweeper', { width, height, bombs }); } } class MineSweeper extends Phaser.Scene { constructor () { super('MineSweeper'); } init (data) { this.width = data.width; this.height = data.height; this.bombs = data.bombs; } create () { this.add.image(0, 0, 'win95').setOrigin(0); this.grid = new Grid(this, this.width, this.height, this.bombs); } } const config = { type: Phaser.AUTO, width: 800, height: 600, backgroundColor: 0x2d2d2d, parent: 'phaser-example', scene: [ Intro, MineSweeper ] }; const game = new Phaser.Game(config);
Scan to open on your mobile device
class Cell { constructor (grid, index, x, y) { this.grid = grid; this.index = index; this.x = x; this.y = y; this.open = false; this.bomb = false; this.flagged = false; this.query = false; this.exploded = false; // 0 = empty, 1,2,3,4,5,6,7,8 = number of adjacent bombs this.value = 0; this.tile = grid.scene.make.image({ key: 'tiles', frame: 0, x: grid.offset.x + (x * 16), y: grid.offset.y + (y * 16), origin: 0 }); grid.board.add(this.tile); this.tile.setInteractive(); this.tile.on('pointerdown', this.onPointerDown, this); this.tile.on('pointerup', this.onPointerUp, this); } reset () { this.open = false; this.bomb = false; this.flagged = false; this.query = false; this.exploded = false; this.value = 0; this.tile.setFrame(0); } onPointerDown (pointer) { if (!this.grid.populated) { this.grid.generate(this.index); } if (this.open || !this.grid.playing) { return; } if (pointer.rightButtonDown()) { if (this.query) { this.query = false; this.tile.setFrame(0); } else if (this.flagged) { this.query = true; this.flagged = false; this.grid.updateBombs(-1); this.tile.setFrame(3); } else if (!this.flagged) { this.flagged = true; this.tile.setFrame(2); this.grid.updateBombs(1); this.grid.checkWinState(); } } else if (!this.flagged && !this.query) { this.onClick(); } } onClick () { if (this.bomb) { this.exploded = true; this.grid.gameOver(); } else { if (this.value === 0) { this.grid.floodFill(this.x, this.y); } else { this.show(); } this.grid.button.setFrame(2); this.grid.checkWinState(); } } onPointerUp () { if (this.grid.button.frame.name === 2) { this.grid.button.setFrame(0); } } reveal () { if (this.exploded) { this.tile.setFrame(6); } else if (!this.bomb && (this.flagged || this.query)) { this.tile.setFrame(7); } else if (this.bomb) { this.tile.setFrame(5); } else { this.show(); } } show () { const values = [ 1, 8, 9, 10, 11, 12, 13, 14, 15 ]; this.tile.setFrame(values[this.value]); this.open = true; } debug () { const values = [ '⬜️', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣' ]; if (this.bomb) { return '💣'; } else { return values[this.value]; } } } class Grid { constructor (scene, width, height, bombs) { this.scene = scene; this.width = width; this.height = height; this.size = width * height; this.offset = new Phaser.Math.Vector2(12, 55); this.timeCounter = 0; this.bombQty = bombs; this.bombsCounter = bombs; this.playing = false; this.populated = false; this.timer = scene.time.addEvent(); // 0 = waiting to create the grid // 1 = playing // 2 = game won // 3 = game lost this.state = 0; this.data = []; const x = Math.floor((scene.scale.width / 2) - (20 + (width * 16)) / 2); const y = Math.floor((scene.scale.height / 2) - (63 + (height * 16)) / 2); this.board = scene.add.container(x, y); this.digit1; this.digit2; this.digit3; this.time1; this.time2; this.time3; this.button; this.createBackground(); this.createCells(); this.updateDigits(); this.button.setInteractive(); this.button.on('pointerdown', this.onButtonDown, this); this.button.on('pointerup', this.onButtonUp, this); } createCells () { let i = 0; for (let x = 0; x < this.width; x++) { this.data[x] = []; for (let y = 0; y < this.height; y++) { this.data[x][y] = new Cell(this, i, x, y); i++; } } } createBackground () { const board = this.board; const factory = this.scene.add; // 55 added to the top, 8 added to the bottom (63) // 12 added to the left, 8 added to the right (20) // cells start at 12 x 55 const width = this.width * 16; const height = this.height * 16; // Top board.add(factory.image(0, 0, 'topLeft').setOrigin(0)); const topBgWidth = (width + 20) - 60 - 56; board.add(factory.tileSprite(60, 0, topBgWidth, 55, 'topBg').setOrigin(0)); board.add(factory.image(width + 20, 0, 'topRight').setOrigin(1, 0)); // Sides const sideHeight = (height + 63) - 55 - 8; board.add(factory.tileSprite(0, 55, 12, sideHeight, 'left').setOrigin(0)); board.add(factory.tileSprite(width + 20, 55, 8, sideHeight, 'right').setOrigin(1, 0)); // Bottom board.add(factory.image(0, height + 63, 'botLeft').setOrigin(0, 1)); const botBgWidth = (width + 20) - 12 - 8; board.add(factory.tileSprite(12, height + 63, botBgWidth, 8, 'botBg').setOrigin(0, 1)); board.add(factory.image(width + 20, height + 63, 'botRight').setOrigin(1, 1)); // Bombs Digits this.digit1 = factory.image(17, 16, 'digits', 0).setOrigin(0); this.digit2 = factory.image(17 + 13, 16, 'digits', 0).setOrigin(0); this.digit3 = factory.image(17 + 26, 16, 'digits', 0).setOrigin(0); board.add([ this.digit1, this.digit2, this.digit3 ]); // Timer Digits const x = (width + 20) - 54; this.time1 = factory.image(x, 16, 'digits', 0).setOrigin(0); this.time2 = factory.image(x + 13, 16, 'digits', 0).setOrigin(0); this.time3 = factory.image(x + 26, 16, 'digits', 0).setOrigin(0); board.add([ this.time1, this.time2, this.time3 ]); // Button const buttonX = Math.floor(((width + 20) / 2) - 13); this.button = factory.image(buttonX, 15, 'buttons', 0).setOrigin(0); board.add(this.button); } updateBombs (diff) { this.bombsCounter -= diff; this.updateDigits(); } updateDigits () { const count = Phaser.Utils.String.Pad(this.bombsCounter.toString(), 3, '0', 1); this.digit1.setFrame(parseInt(count[0])); this.digit2.setFrame(parseInt(count[1])); this.digit3.setFrame(parseInt(count[2])); } onButtonDown () { this.button.setFrame(1); } onButtonUp () { if (this.state > 0) { this.button.setFrame(0); this.restart(); } } restart () { this.populated = false; this.playing = false; this.bombsCounter = this.bombQty; this.state = 0; this.timeCounter = -1; this.timer.paused = true; let location = 0; do { this.getCell(location).reset(); location++; } while (location < this.size); this.updateDigits(); this.time1.setFrame(0); this.time2.setFrame(0); this.time3.setFrame(0); } gameOver () { this.playing = false; this.state = 3; this.timer.paused = true; this.button.setFrame(4); let location = 0; do { this.getCell(location).reveal(); location++; } while (location < this.size); } gameWon () { this.playing = false; this.state = 2; this.timer.paused = true; this.button.setFrame(3); } checkWinState () { let correct = 0; let location = 0; let open = 0; do { const cell = this.getCell(location); if (cell.open) { open++; } if (cell.bomb && cell.flagged) { open++; correct++; } location++; } while (location < this.size); // console.log('Check', correct, 'out of', this.bombQty, 'open', open, 'of', this.size); if (correct === this.bombQty && open === this.size) { this.gameWon(); } } generate (startIndex) { let qty = this.bombQty; const bombs = []; do { const location = Phaser.Math.Between(0, this.size - 1); const cell = this.getCell(location); if (!cell.bomb && cell.index !== startIndex) { cell.bomb = true; qty--; bombs.push(cell); } } while (qty > 0); bombs.forEach(cell => { // Update the 8 cells around this bomb cell const adjacent = this.getAdjacentCells(cell); adjacent.forEach(adjacentCell => { if (adjacentCell) { adjacentCell.value++; } }); }); this.playing = true; this.populated = true; this.state = 1; this.timer.reset({ delay: 1000, callback: this.onTimer, callbackScope: this, loop: true }); this.debug(); } onTimer () { this.timeCounter++; if (this.timeCounter < 1000) { const count = Phaser.Utils.String.Pad(this.timeCounter.toString(), 3, '0', 1); this.time1.setFrame(parseInt(count[0])); this.time2.setFrame(parseInt(count[1])); this.time3.setFrame(parseInt(count[2])); } } getCell (index) { const pos = Phaser.Math.ToXY(index, this.width, this.height); return this.data[pos.x][pos.y]; } getCellXY (x, y) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) { return null; } return this.data[x][y]; } getAdjacentCells (cell) { return [ // Top-Left, Top-Middle, Top-Right this.getCellXY(cell.x - 1, cell.y - 1), this.getCellXY(cell.x, cell.y - 1), this.getCellXY(cell.x + 1, cell.y - 1), // Left, Right this.getCellXY(cell.x - 1, cell.y), this.getCellXY(cell.x + 1, cell.y), // Bottom-Left, Bottom-Middle, Bottom-Right this.getCellXY(cell.x - 1, cell.y + 1), this.getCellXY(cell.x, cell.y + 1), this.getCellXY(cell.x + 1, cell.y + 1) ]; } floodFill (x, y) { const cell = this.getCellXY(x, y); if (cell && !cell.open && !cell.bomb) { cell.show(); if (cell.value === 0) { this.floodFill(x, y - 1); this.floodFill(x, y + 1); this.floodFill(x - 1, y); this.floodFill(x + 1, y); } } } debug () { for (let y = 0; y < this.height; y++) { let row = ''; for (let x = 0; x < this.width; x++) { let cell = this.data[x][y]; if (x === 0) { row = row.concat(`|`); } row = row.concat(`${cell.debug()}|`); } console.log(row); } console.log(''); } } class Intro extends Phaser.Scene { constructor () { super(); } preload () { this.load.setBaseURL('https://cdn.phaserfiles.com/v355'); this.load.setPath('assets/games/minesweeper/'); this.load.spritesheet('tiles', 'tiles.png', { frameWidth: 16 }); this.load.spritesheet('digits', 'digits.png', { frameWidth: 13, frameHeight: 23, endFrame: 9 }); this.load.spritesheet('buttons', 'digits.png', { frameWidth: 26, frameHeight: 26, startFrame: 5 }); this.load.image('topLeft', 'top-left.png'); this.load.image('topRight', 'top-right.png'); this.load.image('topBg', 'top-bg.png'); this.load.image('botLeft', 'bot-left.png'); this.load.image('botRight', 'bot-right.png'); this.load.image('botBg', 'bot-bg.png'); this.load.image('left', 'left.png'); this.load.image('right', 'right.png'); this.load.image('intro', 'intro.png'); this.load.image('win95', 'win95.png'); } create () { // this.input.mouse.disableContextMenu(); this.highlight = this.add.rectangle(0, 334, 800, 70, 0x0182fb).setOrigin(0).setAlpha(0.75); this.intro = this.add.image(0, 0, 'intro').setOrigin(0); const zone1 = this.add.zone(0, 334, 800, 70).setOrigin(0); const zone2 = this.add.zone(0, 411, 800, 70).setOrigin(0); const zone3 = this.add.zone(0, 488, 800, 70).setOrigin(0); zone1.setInteractive(); zone2.setInteractive(); zone3.setInteractive(); zone1.on('pointerover', () => { this.highlight.y = zone1.y; }); zone2.on('pointerover', () => { this.highlight.y = zone2.y; }); zone3.on('pointerover', () => { this.highlight.y = zone3.y; }); zone1.once('pointerdown', () => { this.startGame(9, 9, 10); }); zone2.once('pointerdown', () => { this.startGame(16, 16, 40); }); zone3.once('pointerdown', () => { this.startGame(16, 30, 99); }); } startGame (width, height, bombs) { this.scene.start('MineSweeper', { width, height, bombs }); } } class MineSweeper extends Phaser.Scene { constructor () { super('MineSweeper'); } init (data) { this.width = data.width; this.height = data.height; this.bombs = data.bombs; } create () { this.add.image(0, 0, 'win95').setOrigin(0); this.grid = new Grid(this, this.width, this.height, this.bombs); } } const config = { type: Phaser.AUTO, width: 800, height: 600, backgroundColor: 0x2d2d2d, parent: 'phaser-example', scene: [ Intro, MineSweeper ] }; const game = new Phaser.Game(config);