Eason's blog

【100sites #011】LifeGame,生命遊戲模擬無限的可能性

Published on

LifeGame,生命遊戲模擬無限的可能性

點我玩生命遊戲線上 demo

點我查看 Github 原始碼

  • ENTER:暫停
  • 滑鼠左鍵:放置或刪除細胞

螢幕快照 2016-03-27 上午12.35.39.png

什麼是生命遊戲?

生命遊戲,是由英國數學家John Horton Conway研發的一套細胞自動機模型,地圖中每個細胞會根據周圍 8 格細胞的狀態來決定自身的存亡,詳細規則如下:(摘錄自維基百科

  1. 當前細胞為存活狀態時,當周圍低於 2 個(不包含 2 個)存活細胞時,該細胞變成死亡狀態。(模擬生命數量稀少)
  2. 當前細胞為存活狀態時,當周圍有 2 個或 3 個存活細胞時,該細胞保持原樣。
  3. 當前細胞為存活狀態時,當周圍有 3 個以上(不包含 3 個)的存活細胞時,該細胞變成死亡狀態。(模擬生命數量過多)
  4. 當前細胞為死亡狀態時,當周圍有 3 個存活細胞時,該細胞變成存活狀態。 (模擬繁殖)

僅僅這 4 條規則,就可以建構出一個複雜而美妙的生物遊戲世界。我們可以找到一些特殊形狀的生物單位,他們會表現出一些有趣的行為模式。

三種經典的特殊生物單位: 螢幕快照 2016-03-28 上午11.36.59.png

  • 左邊的 Blinker(信號燈)會以特定週期變化
  • 中間的 Spaceship(太空船)會往右下角移動(你也可以設計出往其他方向移動的船)
  • 右邊的 Beehive(蜂窩)則會保持靜止

今天的 LifeGame

而在今天的 LifeGame 裡頭,我已經為你在左上角放置一個 Blinker 了,剩下的空間,就交給你來創造無限的可能性了!

今天的程式碼:

<!DOCTYPE html>
<html>
  <head>
    <meta charset-"UTF-8">
    <title>LifeGame</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.23/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.23/addons/p5.dom.min.js"></script>
    <script src="lifegame.js"></script>
  </head>

  <body></body>
</html>
* {
  margin: 0;
  padding: 0;
}

body {
  overflow: hidden;
}
var SIZE = 20
var oldMap = [],
  newMap = [],
  num_x = 0,
  num_y = 0,
  paused = false,
  frameCount = 0
var pauseButton

function setup() {
  createCanvas(windowWidth, windowHeight)
  frameRate(30)

  // init map
  num_x = windowWidth / SIZE
  num_y = windowHeight / SIZE
  for (var i = 0; i < num_x; ++i) {
    oldMap.push([])
    newMap.push([])
    for (var j = 0; j < num_y; ++j) {
      oldMap[i].push(false)
      newMap[i].push(false)
    }
  }
  loadDefaultMap()

  // init pause button
  pauseButton = createButton('Pause')
  pauseButton.size(80, 30)
  pauseButton.position(windowWidth / 2 - 40, windowHeight - 40)
  pauseButton.mousePressed(togglePauseSimulate)
}

function draw() {
  // fresh map every 5 frames
  if (!paused) {
    if (frameCount % 5 == 0) {
      freshMap()
    }
  }
  // increase frame count
  ++frameCount
  if (frameCount >= 30) {
    frameCount = 0
  }

  drawMap()
}

// default map: a blink unit at top left corner
function loadDefaultMap() {
  oldMap[1][1] = true
  oldMap[1][2] = true
  oldMap[1][3] = true
}

function drawMap() {
  background(0)
  // draw grid
  stroke(30)
  for (var i = 0; i < num_x; ++i) {
    line(i * SIZE, 0, i * SIZE, windowHeight)
  }
  for (var i = 0; i < num_y; ++i) {
    line(0, i * SIZE, windowWidth, i * SIZE)
  }
  fill(255)
  // draw cells
  for (var i = 0; i < num_x; ++i) {
    for (var j = 0; j < num_y; ++j) {
      if (oldMap[i][j]) {
        rect(i * SIZE, j * SIZE, SIZE, SIZE)
      }
    }
  }
  // draw mouse cell
  fill(color('rgba(100,100,100,0.5)'))
  var mouseCellX = int(mouseX / SIZE)
  var mouseCellY = int(mouseY / SIZE)
  rect(mouseCellX * SIZE, mouseCellY * SIZE, SIZE, SIZE)
}

// press ENTER to pause simulate
function keyPressed() {
  if (keyCode == ENTER) {
    togglePauseSimulate()
  }
}

// press mouse to add or remove cell
function mousePressed() {
  var mouseCellX = int(mouseX / SIZE)
  var mouseCellY = int(mouseY / SIZE)
  oldMap[mouseCellX][mouseCellY] = !oldMap[mouseCellX][mouseCellY]
}

function togglePauseSimulate() {
  if (paused) {
    paused = false
  } else {
    paused = true
  }
}

// count how many neighbors there are of a cell
function neighbors(xpos, ypos) {
  var total = 0
  // four corners
  if (xpos != 0 && ypos != 0) {
    if (oldMap[xpos - 1][ypos - 1]) {
      ++total
    }
  }
  if (xpos != 0 && ypos != num_y - 1) {
    if (oldMap[xpos - 1][ypos + 1]) {
      ++total
    }
  }
  if (xpos != num_x - 1 && ypos != 0) {
    if (oldMap[xpos + 1][ypos - 1]) {
      ++total
    }
  }
  if (xpos != num_x - 1 && ypos != num_y - 1) {
    if (oldMap[xpos + 1][ypos + 1]) {
      ++total
    }
  }
  // left and right
  if (xpos != 0) {
    if (oldMap[xpos - 1][ypos]) {
      ++total
    }
  }
  if (xpos != num_x - 1) {
    if (oldMap[xpos + 1][ypos]) {
      ++total
    }
  }
  if (ypos != 0) {
    if (oldMap[xpos][ypos - 1]) {
      ++total
    }
  }
  if (ypos != num_y - 1) {
    if (oldMap[xpos][ypos + 1]) {
      ++total
    }
  }
  return total
}

// calculate the next map according to the Life Game rule
function freshMap() {
  for (var i = 0; i < num_x; ++i) {
    for (var j = 0; j < num_y; ++j) {
      var neighbor_count = neighbors(i, j)
      if (neighbor_count <= 1 || neighbor_count >= 4) {
        newMap[i][j] = false
      } else if (neighbor_count === 2) {
        newMap[i][j] = oldMap[i][j]
      } else {
        // neighbor_count === 3
        newMap[i][j] = true
      }
    }
  }

  for (var i = 0; i < num_x; ++i) {
    for (var j = 0; j < num_y; ++j) {
      oldMap[i][j] = newMap[i][j]
    }
  }
}