简体   繁体   English

康威的生命游戏 - 超越网格

[英]Conway's Game of Life - beyond the grid

Ok so there are a lot of "Conway's Game of Life" questions but this one is pretty specific. 好吧,有很多“康威的生命游戏”问题,但这个问题非常具体。 I'm going to have to throw a bunch of code at you first, break it down and show you where the issue is. 我将不得不首先向您抛出一堆代码,将其分解并告诉您问题所在。

So here is my Conway's Game of Life implementation so far, right now it is limited to the console for debugging (JSfiddle - http://jsfiddle.net/georeith/C9Gyr/8/ - fire it up, open your console): 所以这是迄今为止我的Conway的生命游戏实现,现在它仅限于调试控制台(JSfiddle - http://jsfiddle.net/georeith/C9Gyr/8/ - 启动它,打开你的控制台):

var utils = {};

/*
 * utils.extend()
 * - Extend initial object with all properties of following objects, objects later in the argument list take precedence.
 */
utils.extend = function(obj) {
  var args = Array.prototype.slice.call(arguments, 1);
  for (var i = args.length; i--;) {
    for (var prop in args[i]) {
      obj[prop] = args[i][prop];
    }
  }
  return obj;
}

/*
 * utils.defaults()
 * - Overwrite initial object with properties of following objects only if key is present in the initial object.
 */
utils.defaults = function(obj) {
  var args = Array.prototype.slice.call(arguments, 1);
  for (var i = args.length; i--;) {
    for (var prop in args[i]) {
      if (obj.hasOwnProperty(prop)) {
        obj[prop] = args[i][prop];
      }
    }
  }
  return obj;
}

/* no-wrap positioning functions */
var calcPos = {
  ul: function(cell) {
    return [cell.x - 1, cell.y - 1];
  },
  um: function(cell) {
    return [cell.x, cell.y - 1];
  },
  ur: function(cell) {
    return [cell.x + 1, cell.y - 1];
  },
  l: function(cell) {
    return [cell.x - 1, cell.y];
  },
  r: function(cell) {
    return [cell.x + 1, cell.y];
  },
  ll: function(cell) {
    return [cell.x - 1, cell.y + 1];
  },
  lm: function(cell) {
    return [cell.x, cell.y + 1];
  },
  lr: function(cell) {
    return [cell.x + 1, cell.y + 1];
  }
}

var worldDefaults = {
  rows: 50,
  columns: 50,
  wrap: true, // left edge is mirrored on right, top edge is mirrored on bottom. Vice versa
  speed: -1, // milliseconds (minimum time, waits until end of last tick to calculate from)
  grid: []
}
var World = function (opts) {
  this.settings = utils.defaults(worldDefaults, opts);

  this.maxX = this.settings.columns - 1;
  this.maxY = this.settings.rows -1;
  for (var y = 0, yLen = this.settings.rows; y < yLen; ++y) {
    for (var x = 0, xLen = this.settings.columns; x < xLen; ++x) { 
      if (y === 0) {
        this.cellList.push([]);
        if (this.settings.grid.length <= x) {
          this.settings.grid.push([]);
        }
      }
      var cell = new Cell();
      cell.x = x;
      cell.y = y;
      cell.alive = !!this.settings.grid[x][y];

      if (cell.alive) {
        this.lifeList.push(cell);
      }

      var lx = (x) ? x - 1 : this.maxX;
      var uy = (y) ? y - 1 : this.maxY;
      var ux = (x == this.maxX) ? 0 : x + 1;
      var ly = (y == this.maxY) ? 0 : y + 1;

      cell.neighbourCoords = (this.settings.wrap) ?
      [
        [lx, uy],   [x, uy],  [ux, uy],
        [lx,  y], /*[x,  y]*/ [ux,  y],
        [lx, ly],   [x, ly],  [ux, ly]
      ]
      :
      [
        calcPos.ul, calcPos.um, calcPos.ur,
        calcPos.l, calcPos.r,
        calcPos.ll, calcPos.lm, calcPos.lr
      ]
      ;
      this.cellList[x][y] = cell;
    }
  }
}
World.prototype.generation = 0;
World.prototype.cellList = [];
World.prototype.lifeList = [];
World.prototype.changeList = [];
World.prototype.nextTick = null;

/* Progresses the world */
World.prototype.tick = function() {
  var newLifeList = [];
  this.changeList = [];

  // This hash goes out of scope after each tick allowing any dead shadowCells to be garbage collected
  if (!this.settings.wrap) {
    var shadowCellHash = {};
  }

  for (var i = 0, iLen = this.lifeList.length; i < iLen; ++i) {
    var cell = this.lifeList[i];
    if (cell.key) {
      shadowCellHash[cell.key] = cell;
    }
    cell.neighbours = 0;
    cell.lastIterated = this.generation;

    for (var j = 0, jLen = cell.neighbourCoords.length; j < jLen; ++j) {

      var coords;
      var neighbour;
      if (this.settings.wrap) {
        coords = cell.neighbourCoords[j];
        neighbour = this.cellList[coords[0]][coords[1]];

      } else {
        coords = cell.neighbourCoords[j](cell);
        if (coords[0] > this.maxX || coords[0] < 0 || coords[1] > this.maxY || coords[1] < 0) {
          // This neighbour is off the screen so will require a shadowCell
          var key = ''+coords[0]+','+coords[1];
          if (!shadowCellHash[key]) {
            neighbour = shadowCellHash[key] = new ShadowCell(coords[0], coords[1]);
            neighbour.neighbourCoords = cell.neighbourCoords;
          } else {
            neighbour = shadowCellHash[key];
          }
        } else {
          neighbour = this.cellList[coords[0]][coords[1]];
        }
      }


      if (neighbour.lastIterated !== this.generation) {
        neighbour.neighbours = 0;
        neighbour.lastIterated = this.generation;
      }
      if (neighbour.alive !== neighbour.changed) {
        // neighbour started as alive
        ++cell.neighbours;
      } else {
        // neighbour started as dead
        ++neighbour.neighbours;
        if (neighbour.neighbours === 3) {
          neighbour.alive = true;
          neighbour.changed = true;
          neighbour.changeIndex = this.changeList.push(neighbour) - 1;
        } else if (neighbour.neighbours === 4) {
          // neighbour has reverted to dead
          neighbour.alive = false;
          neighbour.changed = false;
          neighbour.changeIndex = -1;
          this.changeList[neighbour.changeIndex] = undefined;
        }
      }
    }
    if (cell.neighbours < 2 || cell.neighbours > 3) {
      cell.changed = true;
      cell.alive = false;
      cell.changeIndex = this.changeList.push(cell) - 1;
    } else {
      newLifeList.push(cell);
    }
  }

  for (var i = 0, iLen = this.changeList.length; i < iLen; ++i) {
    var cell = this.changeList[i];
    if (cell !== undefined) {
      cell.changeIndex = -1;
      if (cell.alive) {
        newLifeList.push(cell);
      }
      cell.update();
      cell.changed = false;
    }
  }

  this.lifeList = newLifeList;
  ++this.generation;
  this.onTick();

  var that = this;
  if (this.settings.speed >= 0) {
    this.nextTick = setTimeout(function() {
      that.tick();
    }, this.settings.speed);
  }
  return this;
}

World.prototype.out = function() {
  var s = '';
  for (var y = 0, yLen = this.settings.rows; y < yLen; ++y) {
    for (var x = 0, xLen = this.settings.columns; x < xLen; ++x) {
      s += (this.cellList[x][y].alive)? '\u2B1B' : '\u2B1C';
    }
    s += '\n';
  }
  s += '\u21B3 Generation: ' + this.generation + ' -- Cells: ' + this.lifeList.length + ' \u21B5';
  s += '\n';
  return s;    
}

World.prototype.stop = function() {
  this.speed = -1;
}

World.prototype.onTick = function() {
  return this;
}

var Cell = function() {
  return this;
}
Cell.prototype.x = 0;
Cell.prototype.y = 0;
Cell.prototype.neighbours = 0;
Cell.prototype.alive = false;
Cell.prototype.changed = false;
Cell.prototype.changeIndex = -1;
Cell.prototype.lastIterated = -1;

/*
 * ShadowCell
 * - non rendered cell for use in no-wrap
 */
var ShadowCell = function(x,y) {
  this.x = x;
  this.y = y;
  this.key = ''+this.x+','+this.y;
  return this;
}
ShadowCell.prototype = utils.extend({}, Cell.prototype);
ShadowCell.prototype.isShadow = true;
ShadowCell.prototype.update = function(){
  return this;
};

/*
 * Cell.update()
 * - Update cell after tick
 */
Cell.prototype.update = function() {
  this.render();
  return this;
}

/*
 * Cell.render()
 * - Placeholder function to be overwritten by rendering engine
 */
Cell.prototype.render = function() {
  return this;
}

The method I have chosen involves an array of all the cells that are alive at the start of each generation. 我选择的方法涉及在每一代开始时存活的所有细胞的阵列。 I then iterate over each of their 8 neighbours and decide whether to create/delete them. 然后我迭代他们的8个邻居中的每一个并决定是否创建/删除它们。

This works great when I pass wrap: false to the World constructor (see JSfiddle for implementation), this tells it to mirror the sides and not allow overflow. 当我将wrap: false传递给World构造函数(请参阅JSfiddle实现)时,这很有效,这告诉它镜像边并且不允许溢出。 However that style of layout breaks a lot of patterns as it causes cells to come back on themselves so I also want to allow it to calculate beyond the grid. 然而,这种布局风格打破了很多模式,因为它导致细胞回归自身,所以我也想让它在网格之外进行计算。

For this purpose I created the ShadowCell class which behaves mostly the same as the Cell class (each grid cell dead or alive is an instance of it) except that the ShadowClass is only created when a non-existent cell is required outside of the grid and is offered for garbage collection the moment it is no longer required (if it is dead after each generation). 为此,我创建了ShadowCell类,其行为与Cell类大致相同(每个网格单元死或活是它的一个实例),只是ShadowClass只在网格外部需要不存在的单元格时创建。垃圾收集在不再需要的时候提供(如果它在每一代之后死亡)。 Otherwise it mimics the Cell classes attributes and fits directly into the same logic that Cell does. 否则它模仿Cell类属性并直接适合Cell所做的相同逻辑。

The issue 问题

If you go to "generation 4" in the console output you may notice it isn't quite right... 如果你在控制台输出中转到“第4代”,你可能会注意到它不太正确......

Hmmmmm

I have narrowed this issue down to the ShadowCell implementation because this works if I provide enough padding around the shape so that it does not overflow the grid (which is when ShadowCell kicks in), although like I said earlier ShadowCell is a copy of the Cell class, it has the same attributes and gets passed in as if it was a Cell . 我已将此问题缩小到ShadowCell实现,因为如果我在形状周围提供足够的填充以使其不会溢出网格(这是ShadowCell启动时),这ShadowCell ,尽管我之前说的ShadowCellCell的副本class,它具有相同的属性并传入,就像它是一个Cell

Because I want these to be garbage collected I do not include these in the overall grid array World.cellList ... this leads me to believe the problem lies in this section of code: 因为我希望这些被垃圾收集,所以我不在整个网格数组World.cellList包含这些...这让我相信问题出在这段代码中:

// This hash goes out of scope after each tick allowing any dead shadowCells to be garbage collected

if (!this.settings.wrap) {
  var shadowCellHash = {};
}

for (var i = 0, iLen = this.lifeList.length; i < iLen; ++i) {
  var cell = this.lifeList[i];
  if (cell.key) {
    shadowCellHash[cell.key] = cell;
  }
  cell.neighbours = 0;
  cell.lastIterated = this.generation;

  for (var j = 0, jLen = cell.neighbourCoords.length; j < jLen; ++j) {

    var coords;
    var neighbour;
    if (this.settings.wrap) {
      coords = cell.neighbourCoords[j];
      neighbour = this.cellList[coords[0]][coords[1]];

    } else {
      coords = cell.neighbourCoords[j](cell);
      if (coords[0] > this.maxX || coords[0] < 0 || coords[1] > this.maxY || coords[1] < 0) {
        // This neighbour is off the screen so will require a shadowCell
        var key = ''+coords[0]+','+coords[1];
        if (!shadowCellHash[key]) {
          // ShadowCell not in hash, let's create one
          neighbour = shadowCellHash[key] = new ShadowCell(coords[0], coords[1]);
          neighbour.neighbourCoords = cell.neighbourCoords; 
          // NOTE: neighbourCoords are a set of functions that return values relative to the cell you pass to them. I am not literally giving the `ShadowCell` the same neighbour positions here.
        } else {
          neighbour = shadowCellHash[key];
        }
      } else {
        // This neighbour is on screen, grab its cell.
        neighbour = this.cellList[coords[0]][coords[1]];
      }
    }
    ...

Note: Alive ShadowCell s will not be garbage collected as they get stored in an Array with the other cells (I am certain of this from my debugging, see the cell count in your console output and count the visible cells). 注意: Alive ShadowCell不会被垃圾收集,因为它们与其他单元一起存储在一个数组中(我从调试中确定这一点,请参阅控制台输出中的单元格计数并计算可见单元格)。

For some reason the ShadowCell class appears to cause incorrect reporting of neighbours. 由于某种原因, ShadowCell类似乎导致不正确的邻居报告。 I have attempted to debug it by following the creation, deletion and counted neighbours of each individual cell during each generation but my brain dies before it can put it all together. 我试图通过在每一代中跟踪每个单独细胞的创建,删除和计数邻居来调试它,但是我的大脑在它可以将它们组合在一起之前死亡。 For all my debugging efforts I can't see why this behaviour should occur . 对于我的所有调试工作,我不明白为什么会出现这种情况。 ShadowCell is pretty much the same as a Cell to everything else that uses it (they use the exact same position functions .etc), the fact it doesn't get rendered shouldn't be the cause of this. ShadowCell与使用它的所有其他东西的Cell几乎相同(它们使用完全相同的位置函数.etc),它不会被渲染的事实不应该是这个的原因。

For generation 4 I get the following output by logging the creation of shadow maps, I can see that each is being created once per generation (note: The class doesn't show because I used utils.extend() to create a snapshot of them): 对于第4代,我通过记录阴影贴图的创建得到以下输出,我可以看到每一代都被创建一次(注意:该类没有显示,因为我使用utils.extend()来创建它们的快照):

Object {x: 5, y: -1, key: "5,-1", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 6, y: -1, key: "6,-1", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 7, y: -1, key: "7,-1", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 4, y: -1, key: "4,-1", neighbourCoords: Array[8], neighbours: 0…}
Object {x: -1, y: 1, key: "-1,1", neighbourCoords: Array[8], neighbours: 0…}
Object {x: -1, y: 2, key: "-1,2", neighbourCoords: Array[8], neighbours: 0…}
Object {x: -1, y: 3, key: "-1,3", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 5, y: -2, key: "5,-2", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 6, y: -2, key: "6,-2", neighbourCoords: Array[8], neighbours: 0…}
Object {x: 7, y: -2, key: "7,-2", neighbourCoords: Array[8], neighbours: 0…}
Object {x: -1, y: 4, key: "-1,4", neighbourCoords: Array[8], neighbours: 0…}

Logged on line 152 like so: 像这样登录152行:

if (!shadowCellHash[key]) {
  neighbour = shadowCellHash[key] = new ShadowCell(coords[0], coords[1]);
  neighbour.neighbourCoords = cell.neighbourCoords;
  console.log(utils.extend({}, neighbour));
} else {

shadowCellHash is not initialized with all of the ShadowCell s before you start looping through every cell looking for neighbours. 在开始遍历查找邻居的每个单元格之前, ShadowCell未使用所有shadowCellHash进行初始化。 When the loop checks [5,-1] for neighbors, it doesn't find [6,-1] because it's not in shadowCellHash . 当循环检查邻居的[5,-1] ,它找不到[6,-1]因为它不在shadowCellHash Since [6,-1] is not found, a new dead [6,-1] is created, and [5,-1] is not born because it does not have enough live neighbours. 自从[6,-1]没有找到,一个新的死亡 [6,-1]被创建, [5,-1]不生,因为没有足够的活邻居。

I think I've resolved your issue by eagerly re-populating shadowCellHash at the beginning of each World.tick 我想我已经通过在每个World.tick的开头急切地重新填充shadowCellHash解决你的问题。

JSFiddle 的jsfiddle

  // This hash goes out of scope after each tick allowing any dead shadowCells to be garbage collected
  if (!this.settings.wrap) {
    var shadowCellHash = {};
    for (var i = 0; i < this.lifeList.length; i++) {
        var cell = this.lifeList[i];
        if (cell.key) {
          shadowCellHash[cell.key] = cell;
        }
    }
  }

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

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