Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Scala 로 풀어본 라이프게임(Life Game)

요즘 스칼라로 알고리즘을 풀어보면서 라이프게임을 풀어보았습니다.
위의 위키피디아 링크에 자세한 내용이 나와있지만 간단히 설명하자면 제가 구현한 라이프게임은 존 호튼 코웨이가 고안해낸 세포자동자의 일종으로 라이프게임은 이번에 처음 알았습니다만 라이프게임 중에서 가장 유명한 것중 하나인 듯 합니다.

룰은 간단합니다.
  • 바둑판 같은 특정 크기의 판에서 각 셀에는 세포가 살수 있으며 다음 세대의 세포의 생존 여부는 주변 8개에 있는 살아있는 세포에 갯수에 의해 결정됩니다.
  • 세포가 죽어있을때 주변에 3개의 세포가 살아있으면 세포는 살아납니다.
  • 세포가 살아있을때 주변에 2개 혹은 3개의 세포가 살아있으면 계속 살아있고 2개 미만일때는 외로워서 죽고 4개 이상일때는 숨이 막혀서 죽습니다.

라이프게임이 재미있는 점은 시작시에 살아있는 세포의 패턴을 어떻게 지정하느냐에 따라 다양항 패턴의 움직임이 나타난다는 것입니다. GUI까지 할 생각은 없었으므로 단순히 콘솔에 뿌려줬습니다.


class LifeGame(lifeGameSize:Int) {
    val playGround = Array.fill(lifeGameSize, lifeGameSize) {Life.Dead}
    val prePlayGround = Array.fill(lifeGameSize, lifeGameSize) {Life.Dead}

    def init(alivedCell: List[(Int, Int)]) {
        alivedCell.foreach( arg =>
            playGround(arg._1)(arg._2) = Life.Alive
        )
        showPlayGround
    }

    def run(count:Int) = {
        for(i <- 1 to count) {
            evaluateLife
        }
    }

    def evaluateLife {
        for {
            i<- playGround.indices
            j<- playGround(i).indices
        } prePlayGround(i)(j) = isAliveOrDie(i, j)

        for {
            i<- playGround.indices
            j<- playGround(i).indices
        } playGround(i)(j) = prePlayGround(i)(j)

        showPlayGround
    }

    def isAliveOrDie(x:Int, y:Int):Life.Value = {
        val alivedNeighborCellCount = getNeighborsSum(x, y)
        val thisCell = playGround(x)(y) 
        (thisCell, alivedNeighborCellCount) match {
            case (Life.Dead, 3) => Life.Alive
            case (Life.Dead, _) => Life.Dead
            case (Life.Alive, 2) => Life.Alive
            case (Life.Alive, 3) => Life.Alive
            case (Life.Alive, _) => Life.Dead
        }
    }

    def getNeighborsSum(x:Int, y:Int):Int = {
        val neighbors = List(getValueOfNeighborCell(x-1, y-1), getValueOfNeighborCell(x-1, y), getValueOfNeighborCell(x-1, y+1),
                                              getValueOfNeighborCell(x, y-1), getValueOfNeighborCell(x, y+1),
                                              getValueOfNeighborCell(x+1, y-1), getValueOfNeighborCell(x+1, y), getValueOfNeighborCell(x+1, y+1))
        (0 /: neighbors) { _ + _.id }
    }

    def getValueOfNeighborCell(x:Int, y:Int):Life.Value = {
        try {
            playGround(x)(y)
        }    catch {
            case ex : ArrayIndexOutOfBoundsException => Life.Dead
            case _ => playGround(x)(y)
        }
    }

    def showPlayGround {
        for {
            i<- playGround.indices
            j<- playGround(i).indices
        } {
            if (j == playGround(i).size-1) println(playGround(i)(j).id)
            else print(playGround(i)(j).id)
        }
    }
}

object Life extends Enumeration {
    type Life = Value
    val Dead, Alive = Value
}

// val ex = new LifeGame(5)
// ex.init(List((0,0), (0,1), (0,2), (0,4), (1,0), (2,3), (2,4), (3,1), (3,2), (3,4), (4,0), (4,2), (4,4) ))
// ex.run(10)

playGround는 전체 세포가 사는 판을 2차원배열로 정의하는 변수고 prePlayGround는 다음세대의 세포상태를 저장해 놓기 위한 편수입니다. init()함수는 List를 이용해서 시작하는 세포의 위치를 지정하는 함수고 run은 세포를 지정한 세대만큼 반복해서 실행해 주는 함수 있습니다. 세포의 상태를 좀 명시적으로 하기 위해서 Enumeration을 사용했는데 적절한 방법이었는지는 작성하는 내내 고민스럽기는 했습니다. 55번 라인에서는 모서리에 있는 세포의 이웃을 세기위해서 패턴매칭에 익센션을 사용했습니다. 스터디때 배웠던 예외 매칭을 사용해보고 싶어서 해보았지만 리뷰때 듣고 보니 로직에 대한 부분을 예외로 처리한 부분은 가독성 부분에서 그다지 좋은 선택은 아니었던것 같습니다.

지난번에 작성해봤던 읽고 말하기 수열보다는 좀더 Scala-ble하지만(스칼라스럽다는 표현을 이렇게 한다더군요.) 그래도 많이 스칼라 스럽지는 못했던것 같고 사실 공유할 정도로 잘 작성된 코드는 아니지만 로그차원에서 남겨봅니다. 시간이 흐른뒤에 다시 이 문제와 코드를 보았을 때 어떤 생각이 들지도 궁금하군요. ㅎ
2010/09/13 00:14 2010/09/13 00:14