6
\$\begingroup\$

To learn Scala, coming from Java and Python, I'm attempting to program an implementation of Conway's Game of Life.

I've implemented two different approaches so far, with the first being so bad I'm not even going to ask for review.

The second is a bit cleaner, and produces the correct output, but since I'm new to Scala, I want to know a more Scala-centric way to write things.

I'm also curious about feasibility for my third approach to this game, such as a bit-map approach to neighborhoods, and value caching.

What I want to know is:

  1. Does my implementation fit well with the Scala way of writing code?
  2. Can my code by reduced and made more concise than it already may or may not be?
  3. Would representing all outcomes as a 9-bit map, then pre-caching all 512 possible outcomes have a positive or negative effect on performance?

The code in question is here, and also on Github.

package com.himself12794.main

import scala.collection.mutable.HashMap

object GameLife2 {

  def mappify(str: String, pad: Char = ' ') = {
    val its = new HashMap[(Int,Int), Boolean]
    var x,y = 0
    var items = str.stripMargin split("\n") toBuffer
    val m = items.reduceLeft((a,b) => a max b ).length
    if (items.length < 100) 0 to 100 - items.length foreach { _ => items append ""  }
    items = items map { _ padTo(100, pad) }
    items foreach { z => z foreach { c => its put((x,y),c == 'X'); x += 1}; y += 1; x = 0 }
    its
  }

}

class GameLife2(m: String, val living: Char = 'X', val dead: Char = ' ') {

  private var cells = GameLife2.mappify(m, dead) 
  private val hw = cells.keySet reduceLeft { (a,b) => (a._1 max b._1, a._2 max b._2) }
  val height = hw._2 + 1
  val width = hw._1 + 1

  def getCount(x1: Int, y1: Int) = {
    var am = 0
    for {
      x <- x1 - 1 to x1 + 1
      y <- y1 - 1 to y1 + 1
    } {
      if (x > -1 && y > -1 && x < width && y < height && (x,y) != (x1,y1) && this(x,y)) am += 1            
    }
    am
  }

  def apply(x: Int, y: Int) = {
    cells.get((x,y)).get
  }

  def doCycle = {
    val cp = cells.clone
    cells foreach {
      cell => {
        val coords = cell._1
        val c = cell._2
        val am = getCount(coords._1, coords._2)

        if (c && (am < 2 || am > 3)) cp.put(coords, false)
        else if (am == 3) cp.put(coords, true)

      }
    }; cells = cp
  }

  def clearConsole = print("\n" * 9)

  def getCells = cells
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

You are storing the board state as a HashMap[(Int,Int), Boolean]. Why not store it as a Set[(Int,Int)] instead, containing only the live cells? Alternatively, if you want to list every cell, whether dead or alive, a two-dimensional array seems like it would be more appropriate and efficient.

You're using a mutable HashMap. An immutable HashMap would be a more natural fit for the Game of Life, and would also promote good functional programming practices. You're already calling val cp = cells.clone followed by cells = cp in doCycle, which indicates that each generation involves building a new board state.

For comparison, I have written an implementation that uses immutable structures and functional programming.

\$\endgroup\$
2
  • \$\begingroup\$ I did it with a HashMap because it was easier for me to iterate through the values, though on my 3rd implementation I'm going back to an array \$\endgroup\$ Jul 14, 2016 at 21:31
  • 1
    \$\begingroup\$ That's a great idea, only storing the live values. It seems like it would use less data. \$\endgroup\$ Jul 14, 2016 at 21:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.