Code Review Asked by Christian Hujer on December 31, 2021
I’ve implemented a Game of Life in Kotlin.
There are some major requirements on this solution:
src/test/kotlin/…/PointTest.kt
package com.nelkinda.training.gameoflife
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
internal class PointTest {
@Test
fun testToString() = assertEquals("P(0, 1)", P(0, 1).toString())
@Test
fun plus() = assertEquals(P(3, 30), P(2, 20) + P(1, 10))
@Test
fun neighbors() = assertEquals(
setOf(P(4, 49), P(4, 50), P(4, 51), P(5, 49), P(5, 51), P(6, 49), P(6, 50), P(6, 51)),
P(5, 50).neighbors().toSet()
)
}
src/main/kotlin/…/Point.kt
package com.nelkinda.training.gameoflife
private val neighbors = (P(-1, -1)..P(1, 1)).filter { it != P(0, 0) }
typealias P = Point
data class Point constructor(private val x: Int, private val y: Int) {
operator fun plus(p: Point) = P(x + p.x, y + p.y)
operator fun rangeTo(p: Point) = (x..p.x).map { x -> (y..p.y).map { y -> P(x, y) } }.flatten()
fun neighbors() = neighbors.map { this + it }
fun neighbors(predicate: (Point) -> Boolean) = neighbors().filter(predicate)
override fun toString() = "P($x, $y)"
}
src/test/kotlin/…/RulesTest.kt
package com.nelkinda.training.gameoflife
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import kotlin.test.assertEquals
internal class RulesTest {
private fun assertSurvival(rules: Rules, liveNeighbors: Set<Int>) = assertAll(
(0..8).map {
{ assertEquals(it in liveNeighbors, rules.survives(it)) }
}
)
private fun assertBirth(rules: Rules, liveNeighbors: Set<Int>) = assertAll(
(0..8).map {
{ assertEquals(it in liveNeighbors, rules.born(it)) }
}
)
@Test
fun testConwayRules() = assertAll(
{ assertEquals("R 23/3", ConwayRules.toString()) },
{ assertSurvival(ConwayRules, setOf(2, 3)) },
{ assertBirth(ConwayRules, setOf(3)) }
)
}
src/main/kotlin/…/Rules.kt
package com.nelkinda.training.gameoflife
@Suppress("MagicNumber")
val ConwayRules: Rules = StandardRules(setOf(2, 3), setOf(3))
interface Rules {
fun survives(liveNeighbors: Int): Boolean
fun born(liveNeighbors: Int): Boolean
}
data class StandardRules(
private val liveNeighborsForSurvival: Set<Int>,
private val liveNeighborsForBirth: Set<Int>
) : Rules {
private fun Set<Int>.toStr() = sorted().joinToString("")
override fun survives(liveNeighbors: Int) = liveNeighbors in liveNeighborsForSurvival
override fun born(liveNeighbors: Int) = liveNeighbors in liveNeighborsForBirth
override fun toString() = "R ${liveNeighborsForSurvival.toStr()}/${liveNeighborsForBirth.toStr()}"
}
src/test/kotlin/…/UniverseTest.kt
package com.nelkinda.training.gameoflife
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
internal class UniverseTest {
@Test
fun testToString() = assertEquals("Universe{R 23/3n[P(0, 1)]}", Universe(life = setOf(P(0, 1))).toString())
}
src/main/kotlin/…/Universe.kt
package com.nelkinda.training.gameoflife
internal typealias Cell = Point
@Suppress("TooManyFunctions")
data class Universe constructor(
private val rules: Rules = ConwayRules,
private val life: Set<Cell>
) {
operator fun inc() = Universe(rules, survivingCells() + bornCells())
private fun survivingCells() = life.filter { it.survives() }.toSet()
private fun bornCells() = deadNeighborsOfLivingCells().filter { it.born() }.toSet()
private fun deadNeighborsOfLivingCells() = life.flatMap { it.deadNeighbors() }
private fun Cell.isAlive() = this in life
private fun Cell.survives() = rules.survives(countLiveNeighbors())
private fun Cell.born() = rules.born(countLiveNeighbors())
private fun Cell.deadNeighbors() = neighbors { !it.isAlive() }
private fun Cell.liveNeighbors() = neighbors { it.isAlive() }
private fun Cell.countLiveNeighbors() = liveNeighbors().count()
override fun toString() = "Universe{$rulesn$life}"
}
At this point, you may suspect missing tests, but wait until you’ve seen the .feature
file.
src/test/kotlin/…/ParserTest.kt
package com.nelkinda.training.gameoflife
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
internal class ParserTest {
private fun parses(spec: String, vararg cells: Cell) =
assertEquals(Universe(life = setOf(*cells)), `parse simplified Life 1_05`(spec))
@Test
fun testParses() = assertAll(
{ parses("") },
{ parses("*", P(0, 0)) },
{ parses("**", P(0, 0), P(1, 0)) },
{ parses("*n*", P(0, 0), P(0, 1)) },
{ parses("*.*", P(0, 0), P(2, 0)) }
)
@Test
fun testInvalid() {
val e = assertThrows<IllegalArgumentException> { parses("o") }
assertEquals("Unexpected character 'o' at line 1, column 1", e.message)
}
}
src/test/kotlin/…/Parser.kt
package com.nelkinda.training.gameoflife
internal fun `parse simplified Life 1_05`(life1_05: String): Universe {
val cells = HashSet<Cell>()
var line = 1
var column = 1
val syntax = mapOf(
'n' to { line++; column = 0 },
'*' to { cells.add(P(column - 1, line - 1)) },
'.' to { }
).withDefault { c -> throw IllegalArgumentException("Unexpected character '$c' at line $line, column $column") }
life1_05.forEach {
syntax.getValue(it).invoke()
column++
}
return Universe(life = cells.toSet())
}
src/test/kotlin/…/GameOfLifeSteps.kt
package com.nelkinda.training.gameoflife
import io.cucumber.java.en.Given
import io.cucumber.java.en.Then
import kotlin.test.assertEquals
class GameOfLifeSteps {
private lateinit var universe: Universe
@Given("the following universe:")
fun defineUniverse(spec: String) {
universe = `parse simplified Life 1_05`(spec)
}
@Then("the next generation MUST be:")
fun assertNextGenerationEquals(spec: String) {
++universe
assertEquals(`parse simplified Life 1_05`(spec), universe)
}
}
src/test/kotlin/…/RunCukesTest.kt
package com.nelkinda.training.gameoflife
import io.cucumber.junit.Cucumber
import io.cucumber.junit.CucumberOptions
import org.junit.runner.RunWith
@CucumberOptions(
features = ["src/test/resources/features"],
strict = true
)
@RunWith(Cucumber::class)
class RunCukesTest
src/test/resources/features/GameOfLife.feature
Feature: Conway Game of Life
Scenario: Empty universe
Given the following universe:
"""
"""
Then the next generation MUST be:
"""
"""
Scenario: Single cell universe
Given the following universe:
"""
*
"""
Then the next generation MUST be:
"""
"""
Scenario: Block
Given the following universe:
"""
**
**
"""
Then the next generation MUST be:
"""
**
**
"""
Scenario: Blinker
Given the following universe:
"""
.*
.*
.*
"""
Then the next generation MUST be:
"""
***
"""
Then the next generation MUST be:
"""
.*
.*
.*
"""
Note: The actual feature file includes additional documentation and a scenario for the Glider which I have omitted from here for brevity, and has Conway's Game of Life
as feature title, which breaks StackOverflow/Google Prettify Gherkin syntax highlighting, so I’ve removed the 's
from it in this copy here.
If you prefer to review the code in GitHub: https://github.com/nelkinda/gameoflife-kotlin
Poke at it, tell me anything you find.
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP