简体   繁体   English

康威的人生游戏-滑翔机不动

[英]Conway's Game of Life - Gliders not moving

So I've implemented a simple Game of Life in Javascript and canvas, and I thought it was working perfectly (fixed timestep, a temporary 'next board' to store changes until they're needed, etc.) but when I added a 'glider' pattern it didn't behave as expected. 因此,我在Javascript和画布上实现了一个简单的“人生游戏”,我认为它运行得很好(固定的时间步长,临时的“下一块板”,用于存储更改,直到需要更改为止,等等),但是当我添加“滑翔机的模式,其行为不符合预期。 They shift slightly but then stop. 他们稍微移动,然后停下来。

I've gone over the code a hundred times and can't see anything wrong, but I'm sure it's a simple error I'm making somewhere. 我已经看了一百遍代码,看不到任何错误,但是我敢肯定,这是我在某个地方犯的一个简单错误。 Code below. 下面的代码。 Any advice much appreciated! 任何建议,不胜感激!

UPDATE: 更新:

I was failing to deep copy the array, as Jonas pointed out below. 正如乔纳斯(Jonas)指出的那样,我未能对阵列进行深度复制。 I've fixed that now, and the simulation now works as the Game of Life is supposed to. 我现在已经解决了这一问题,并且模拟现在可以按照“人生游戏”的预期进行工作。 (Thanks Jonas!) (感谢乔纳斯!)

Updated code below. 更新了下面的代码。 Unfortunately the glider issue is still there - they move correctly for the first frame of the simulation and then stop completely. 不幸的是,滑翔机问题仍然存在-它们在模拟的第一帧中正确移动,然后完全停止。 If anyone can spot the remaining error I'd be very grateful. 如果有人能发现剩余的错误,我将不胜感激。

 let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); const tableSize = 64; const cellSize = 4; let tickDelay = 60; let table = []; let loop; let deadChance = 0.5; const colors = { alive: '#f2b630', dead: '#333' }; function init() { //build table table = []; for (let y = 0; y < tableSize; y++) { let row = []; for (let x = 0; x < tableSize; x++) { let randomAlive = true; if (Math.random() > deadChance) { randomAlive = false; } let cell = new Cell(x, y, randomAlive); row.push(cell); } table.push(row); } } function tick() { console.log("tick"); table = table.map(row => row.map(cell => cell.tick())); render(); } function render() { for (let y = 0; y < tableSize; y++) { for (let x = 0; x < tableSize; x++) { table[x][y].draw(); } } } function start() { console.log("Starting"); loop = setInterval(tick, tickDelay); } function stop() { console.log("Stopping"); clearInterval(loop); } function reset() { console.log("Resetting"); clearInterval(loop); init(); render(); } class Cell { constructor(x, y, isAlive) { //The x and y values are table indices, not pixel values this.x = x; this.y = y; this.isAlive = isAlive; } tick() { let currentNeighbours = getNeighbours(this.x, this.y); let numAliveNeighbours = 0; for (let i = 0; i < currentNeighbours.length; i++) { if (currentNeighbours[i].isAlive) { numAliveNeighbours++; } } switch (numAliveNeighbours) { case 0: this.makeDead(); break; case 1: this.makeDead(); break; case 2: break; case 3: this.makeAlive(); break; case 4: this.makeDead(); break; case 5: this.makeDead(); break; case 6: this.makeDead(); break; case 7: this.makeDead(); break; case 8: this.makeDead(); break; } return new Cell(this.x, this.y, this.isAlive); } draw() { if (this.isAlive) { ctx.fillStyle = colors.alive; } else { ctx.fillStyle = colors.dead; } let margin = 1; ctx.fillRect(this.x * cellSize + (this.x * margin), this.y * cellSize + (this.y * margin), cellSize, cellSize); } makeAlive() { this.isAlive = true; } makeDead() { this.isAlive = false; } } //Helper functions function getNeighbours(x, y) { //return a list of all eight neighbours of this cell in North-East-South-West (NESW) order let result = []; //wrap at the edges of the table for each neighbour let targetX; let targetY; //get NORTH neighbour targetX = x; targetY = y-1; if (targetY < 0) targetY = tableSize-1; result.push(table[targetX][targetY]); //get NORTHEAST neighbour targetX = x+1; targetY = y-1; if (targetY < 0) targetY = tableSize-1; if (targetX > tableSize-1) targetX = 0; result.push(table[targetX][targetY]); //get EAST neighbour targetX = x+1; targetY = y; if (targetX >= tableSize) targetX = 0; result.push(table[targetX][targetY]); //get SOUTHEAST neighbour targetX = x+1; targetY = y+1; if (targetY > tableSize-1) targetY = 0; if (targetX > tableSize-1) targetX = 0; result.push(table[targetX][targetY]); //get SOUTH neighbour targetX = x; targetY = y+1; if (targetY >= tableSize) targetY = 0; result.push(table[targetX][targetY]); //get SOUTHWEST neighbour targetX = x-1; targetY = y+1; if (targetY > tableSize-1) targetY = 0; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); //get WEST neighbour targetX = x-1; targetY = y; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); //get NORTHWEST neighbour targetX = x-1; targetY = y-1; if (targetY < 0) targetY = tableSize-1; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); return result; } //Patterns function pattern() { //Set up the board using a random preset pattern console.log("Creating pattern"); clearInterval(loop); //build dead table table = []; for (let y = 0; y < tableSize; y++) { let row = []; for (let x = 0; x < tableSize; x++) { let cell = new Cell(x, y, false); row.push(cell); } table.push(row); } //add living cells for patterns //Blinker table[1][0].isAlive = true; table[2][0].isAlive = true; table[3][0].isAlive = true; /* //Glider table[1][1].isAlive = true; table[2][2].isAlive = true; table[2][3].isAlive = true; table[3][2].isAlive = true; table[3][1].isAlive = true; table[12][12].isAlive = true; table[13][13].isAlive = true; table[14][13].isAlive = true; table[13][14].isAlive = true; table[12][14].isAlive = true; */ render(); } //Build board and render initial state init(); render(); 
 html { background: slategray; } .game { background: #ddc; border-radius: 2px; padding-left: 0; padding-right: 0; margin-left: auto; margin-right: auto; margin-top: 10%; display: block; } h1 { color: white; text-align: center; font-family: sans-serif; } button { text-align: center; padding: 12px; border-radius: 2px; font-size: 1.2em; margin-left: auto; margin-right: auto; margin-top: 12px; display: block; } .controls { display: flex; width: 300px; margin: auto; } 
 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Conway's Game of Life</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <h1>Conway's Game of Life</h1> <canvas id="canvas" class="game" width="319px" height="319px"></canvas> <div class="controls"> <button onclick='start()'>Start</button> <button onclick='stop()'>Stop</button> <button onclick='reset()'>Reset</button> <button onclick='pattern()'>Pattern</button> </div> <script src="js/game.js"></script> </body> </html> 

Throughout the code, you're accessing the grid by x , then y coordinate. 在整个代码中,您将通过x ,然后是y坐标来访问网格。 In order for that to work, the grid should be defined as an array of columns, not array of rows. 为了使它起作用,应该将网格定义为列的数组,而不是行的数组。

As a quick fix, I simply swapped x and y when defining the grid in init() and pattern() . 作为快速解决方案,在init()pattern()定义网格时,我只是交换了xy You'll probably want to rename your variables to reflect the intention. 您可能需要重命名变量以反映意图。


There's a big issue with this part of the tick function. tick功能的这一部分存在一个大问题。 You're changing the value of isAlive property of a tile before the other tiles were checked for their future states. 您需要检查一个isAlive贴的isAlive属性值, 然后再检查其他磁贴的未来状态。

 switch (numAliveNeighbours) { case 0: this.makeDead(); break; case 1: this.makeDead(); break; case 2: break; case 3: this.makeAlive(); break; case 4: this.makeDead(); break; case 5: this.makeDead(); break; case 6: this.makeDead(); break; case 7: this.makeDead(); break; case 8: this.makeDead(); break; } return new Cell(this.x, this.y, this.isAlive); 

I fixed it with the following one liner as a personal preference, you can keep the switch statement as long as you're not directly changing the existing tile. 我根据个人喜好使用以下一种衬纸对其进行了修复,只要不直接更改现有磁贴,就可以保留switch语句。

const isAlive = this.isAlive ? (numAliveNeighbours === 2 || numAliveNeighbours === 3) : (numAliveNeighbours === 3)
return new Cell(this.x, this.y, isAlive);

 let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); const tableSize = 64; const cellSize = 4; let tickDelay = 60; let table = []; let loop; let deadChance = 0.5; const colors = { alive: '#f2b630', dead: '#333' }; function init() { //build table table = []; for (let y = 0; y < tableSize; y++) { let row = []; for (let x = 0; x < tableSize; x++) { let randomAlive = true; if (Math.random() > deadChance) { randomAlive = false; } let cell = new Cell(y, x, randomAlive); row.push(cell); } table.push(row); } } function tick() { //console.log("tick"); table = table.map(row => row.map(cell => cell.tick())); render(); } function render() { for (let y = 0; y < tableSize; y++) { for (let x = 0; x < tableSize; x++) { table[x][y].draw(); } } } function start() { console.log("Starting"); loop = setInterval(tick, tickDelay); } function stop() { console.log("Stopping"); clearInterval(loop); } function reset() { console.log("Resetting"); clearInterval(loop); init(); render(); } class Cell { constructor(x, y, isAlive) { //The x and y values are table indices, not pixel values this.x = x; this.y = y; this.isAlive = isAlive; } tick() { let currentNeighbours = getNeighbours(this.x, this.y); let numAliveNeighbours = 0; for (let i = 0; i < currentNeighbours.length; i++) { if (currentNeighbours[i].isAlive) { numAliveNeighbours++; } } const isAlive = this.isAlive ? (numAliveNeighbours === 2 || numAliveNeighbours === 3) : (numAliveNeighbours === 3) return new Cell(this.x, this.y, isAlive); } draw() { if (this.isAlive) { ctx.fillStyle = colors.alive; } else { ctx.fillStyle = colors.dead; } let margin = 1; ctx.fillRect(this.x * cellSize + (this.x * margin), this.y * cellSize + (this.y * margin), cellSize, cellSize); } makeAlive() { this.isAlive = true; } makeDead() { this.isAlive = false; } } //Helper functions function getNeighbours(x, y) { //return a list of all eight neighbours of this cell in North-East-South-West (NESW) order let result = []; //wrap at the edges of the table for each neighbour let targetX; let targetY; //get NORTH neighbour targetX = x; targetY = y-1; if (targetY < 0) targetY = tableSize-1; result.push(table[targetX][targetY]); //get NORTHEAST neighbour targetX = x+1; targetY = y-1; if (targetY < 0) targetY = tableSize-1; if (targetX > tableSize-1) targetX = 0; result.push(table[targetX][targetY]); //get EAST neighbour targetX = x+1; targetY = y; if (targetX >= tableSize) targetX = 0; result.push(table[targetX][targetY]); //get SOUTHEAST neighbour targetX = x+1; targetY = y+1; if (targetY > tableSize-1) targetY = 0; if (targetX > tableSize-1) targetX = 0; result.push(table[targetX][targetY]); //get SOUTH neighbour targetX = x; targetY = y+1; if (targetY >= tableSize) targetY = 0; result.push(table[targetX][targetY]); //get SOUTHWEST neighbour targetX = x-1; targetY = y+1; if (targetY > tableSize-1) targetY = 0; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); //get WEST neighbour targetX = x-1; targetY = y; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); //get NORTHWEST neighbour targetX = x-1; targetY = y-1; if (targetY < 0) targetY = tableSize-1; if (targetX < 0) targetX = tableSize-1; result.push(table[targetX][targetY]); return result; } //Patterns function pattern() { //Set up the board using a random preset pattern console.log("Creating pattern"); clearInterval(loop); //build dead table table = []; for (let y = 0; y < tableSize; y++) { let row = []; for (let x = 0; x < tableSize; x++) { let cell = new Cell(y, x, false); row.push(cell); } table.push(row); } //add living cells for patterns //Glider table[1][1].isAlive = true; table[2][2].isAlive = true; table[2][3].isAlive = true; table[3][2].isAlive = true; table[3][1].isAlive = true; render(); } //Build board and render initial state pattern(); render(); 
 html { background: slategray; } .game { background: #ddc; border-radius: 2px; padding-left: 0; padding-right: 0; margin-left: auto; margin-right: auto; margin-top: 10%; display: block; } h1 { color: white; text-align: center; font-family: sans-serif; } button { text-align: center; padding: 12px; border-radius: 2px; font-size: 1.2em; margin-left: auto; margin-right: auto; margin-top: 12px; display: block; } .controls { display: flex; width: 300px; margin: auto; } 
 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Conway's Game of Life</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <h1>Conway's Game of Life</h1> <canvas id="canvas" class="game" width="319px" height="319px"></canvas> <div class="controls"> <button onclick='start()'>Start</button> <button onclick='stop()'>Stop</button> <button onclick='reset()'>Reset</button> <button onclick='pattern()'>Pattern</button> </div> <script src="js/game.js"></script> </body> </html> 

slice() does only shallow copy the array, that means that slice()只对数组进行浅拷贝,这意味着

 nextTable[this.x][this.y] === this // true

So you actually work with one set of cells, which won't work with Convays Game as it requires the cells to update based on the current state, and if there is only one table some cells will calculate their state based on the one of already updated neighbours. 因此,您实际上使用的是一组单元格,而不适用于Convays Game,因为它要求这些单元格根据当前状态进行更新,并且如果只有一张表,则某些单元格将根据已经存在的状态来计算其状态。更新的邻居。 To change that, I would change the tick() method of the Cell so that it returns the next cell: 要更改此设置,我将更改Cell的tick()方法,以便它返回下一个单元格:

tick() {
  //...
  return new Cell(this.x, this.y, true /* false */);
}

Now in your main tick() function, just map the current table to a new one: 现在在您的主要tick()函数中,只需将当前表映射到一个新表:

table = table.map(row => row.map(cell => cell.tick()));

Through that you don't need nextTable at all, as table will point to the old state until all cells were updated, then the table gets rewritten with the newly returned cells. 通过它,您根本不需要nextTable ,因为table将指向旧状态,直到所有单元都被更新,然后该表将被新返回的单元重写。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM