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

Conway's Game of Life - Gliders not moving

所以我在 Javascript 和 canvas 中实现了一个简单的生命游戏,我认为它运行良好(固定时间步长,一个临时 'next board' 来存储更改直到它们是必需的,等等)但是当我添加一个 'glider' 模式时,它没有按预期运行。它们稍微移动但随后停止。

我已经检查了代码一百遍,没有发现任何错误,但我确定这是我在某个地方犯的一个简单错误。下面的代码。非常感谢任何建议!

更新:

正如 Jonas 在下面指出的那样,我未能深入复制数组。我现在已经解决了这个问题,模拟现在可以像 Game of Life 一样工作了。 (感谢乔纳斯!)

更新了下面的代码。不幸的是,滑翔机问题仍然存在——它们在模拟的第一帧正确移动,然后完全停止。如果有人能发现剩余的错误,我将不胜感激。

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>

slice() 只浅拷贝数组,这意味着

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

所以你实际上使用一组单元格,这不适用于 Convays Game,因为它需要单元格根据当前状态更新,如果只有一个 table 一些单元格会根据已经更新的邻居之一计算他们的状态。要更改它,我将更改 Cell 的 tick() 方法,以便它 returns 下一个单元格:

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

现在在您的主要 tick() 函数中,只需将当前 table 映射到一个新函数:

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

这样一来,您根本不需要 nextTable,因为 table 将指向旧状态,直到所有单元格都已更新,然后 table 会被新状态重写返回的单元格。

在整个代码中,您通过 x 访问网格,然后是 y 坐标。为了使其工作,网格应定义为列数组,而不是行数组。

作为快速修复,我在 init()pattern() 中定义网格时只是交换了 xy。您可能希望重命名变量以反映意图。


tick 函数的这一部分存在一个大问题。您正在更改 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);

根据个人喜好,我使用以下一种方式对其进行了修复,只要您不直接更改现有图块,就可以保留 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>