Skip to content

Instantly share code, notes, and snippets.

@fomkin
Last active December 27, 2016 14:15
Show Gist options
  • Save fomkin/60c101d0c39884ebe9db279308b568ee to your computer and use it in GitHub Desktop.
Save fomkin/60c101d0c39884ebe9db279308b568ee to your computer and use it in GitHub Desktop.
Idiomatic Scala implementation of Conway's Game of Life with toroidal universe https://en.wikipedia.org/wiki/Conway's_Game_of_Life
object GameOfLife extends App with Shtml {
import korolev.EventResult._
val universeSize = 20
val cellRadius = 10
val cellGap = 2
val cellWidth = cellRadius * 2
val viewSide = cellRadius + universeSize * (cellWidth + cellGap)
val viewSideS = viewSide.toString
val cellRadiusS = cellRadius.toString
KorolevServer[Universe](
port = 7581,
stateStorage = StateStorage.default(Universe(universeSize)),
render = { access =>
// Generate actions when clicking checkboxes
val cellClick: EventFactory[(Int, Int)] =
access.event("click") {
case (x, y) =>
immediateTransition { case state =>
state.check(x, y)
}
}
val stepClick: EventFactory[Unit] =
access.event("click") { _ =>
immediateTransition { case state =>
state.next
}
}
// Create a DOM using state
{ case universe =>
'body(
'div(
'input(
'type /= "button",
'value /= "Step",
stepClick(())
)
),
'svg('width /= viewSideS, 'height /= viewSideS,
for {
x <- 0 until universe.size
y <- 0 until universe.size
} yield {
def pos(n: Int) = {
val p = cellRadius + n * (cellWidth + cellGap)
p.toString
}
'circle(
'cx /= pos(x),
'cy /= pos(y),
'r /= cellRadiusS,
'fill /= {
if (universe(x, y).alive) "#000000"
else "#EEEEEE"
},
cellClick((x, y))
)
}
)
)
}
}
)
}
case class Universe(cells: Vector[Universe.Cell], size: Int) {
import scala.annotation.tailrec
import Universe._
private def index(x: Int, y: Int) = {
// Torus
(x % size + size) % size +
(y % size + size) % size * size
}
/**
* Get a cell in (x, y)
* @return the cell
*/
def apply(x: Int, y: Int) = cells(index(x, y))
/**
* Kill or resurrect a cell in (x, y)
* @return Modified universe
*/
def check(x: Int, y: Int) = {
val i = index(x, y)
val cell = cells(i)
copy(cells = cells.updated(i, cell.copy(alive = !cell.alive)))
}
/**
* Create a new generation of universe
*/
def next: Universe = {
def aliveNeighbors(px: Int, py: Int): Int = {
@tailrec def aux(num: Int = 0, x: Int = px + 1, y: Int = py + 1): Int = {
if (y < py - 1) num
else if (x < px - 1) aux(num, y = y - 1)
else if (x == px && y == py) aux(num, x - 1, y)
else if (apply(x, y).alive) aux(num + 1, x - 1, y)
else aux(num, x - 1, y)
}
aux()
}
def mustResurrect(x: Int, y: Int) = aliveNeighbors(x, y) == 3
def mustDie(x: Int, y: Int) = {
val n = aliveNeighbors(x, y)
n < 2 || n > 3
}
val updated = cells map {
case p @ Cell(x, y, false) if mustResurrect(x, y) => p.copy(alive = true)
case p @ Cell(x, y, true) if mustDie(x, y) => p.copy(alive = false)
case p => p
}
copy(cells = updated)
}
}
object Universe {
case class Cell(x: Int, y: Int, alive: Boolean)
def apply(size: Int): Universe = {
val cells = for (y <- 0 until size; x <- 0 until size)
yield Cell(x, y, alive = false)
Universe(cells.toVector, size)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment