4
\$\begingroup\$

I created a little Kotlin clone of the 2048 game that is playable in the console. My goals were readability and statelessness for no other reason than practice. Consider it a code kata wherein I didn't want to mutate the original grid, instead returning a new permutation for every operation.

To this end, I would greatly appreciate review and comments of my code pertaining to style, simplicity and readability. Performance is not an issue.

import java.io.BufferedReader
import java.io.InputStreamReader

const val positiveGameOverMessage = "So sorry, but you won the game."
const val negativeGameOverMessage = "So sorry, but you lost the game."

fun main(args: Array<String>) {
    val grid = arrayOf(
            arrayOf(0, 0, 0, 0),
            arrayOf(0, 0, 0, 0),
            arrayOf(0, 0, 0, 0),
            arrayOf(0, 0, 0, 0)
    )

    val gameOverMessage = run2048(grid)
    println(gameOverMessage)
}

fun run2048(grid: Array<Array<Int>>): String {
    if (isGridSolved(grid))    return positiveGameOverMessage
    else if (isGridFull(grid)) return negativeGameOverMessage

    val populatedGrid = spawnNumber(grid)
    display(populatedGrid)

    return run2048(manipulateGrid(populatedGrid, waitForValidInput()))
}

fun isGridSolved(grid: Array<Array<Int>>): Boolean = grid.any { row -> row.contains(2048) }
fun isGridFull(grid: Array<Array<Int>>): Boolean   = grid.all { row -> !row.contains(0) }

fun spawnNumber(grid: Array<Array<Int>>):Array<Array<Int>> {
    val coordinates = locateSpawnCoordinates(grid)
    val number = generateNumber()

    return updateGrid(grid, coordinates, number)
}

fun locateSpawnCoordinates(grid: Array<Array<Int>>): Pair<Int, Int> {
    val emptyCells = arrayListOf<Pair<Int, Int>>()
    grid.forEachIndexed { x, row ->
        row.forEachIndexed { y, cell ->
            if (cell == 0) emptyCells.add(Pair(x, y))
        }
    }

    return emptyCells[(Math.random() * (emptyCells.size()-1)).toInt()]
}
fun generateNumber(): Int = if (Math.random() > 0.10) 2 else 4

fun updateGrid(grid: Array<Array<Int>>, at: Pair<Int, Int>, value: Int): Array<Array<Int>> {
    val updatedGrid = grid.copyOf()
    updatedGrid[at.first][at.second] = value
    return updatedGrid
}

fun waitForValidInput():String {
    val input = waitForInput()
    return if (isValidInput(input)) input else waitForValidInput()
}
fun isValidInput(input: String): Boolean = arrayOf("a", "s", "d", "w").contains(input)

fun waitForInput(): String {
    val br = BufferedReader(InputStreamReader(System.`in`));
    println("Direction?  ")
    return br.readLine()
}

fun manipulateGrid(grid: Array<Array<Int>>, input: String): Array<Array<Int>> = when (input) {
    "a" -> shiftCellsLeft(grid)
    "s" -> shiftCellsDown(grid)
    "d" -> shiftCellsRight(grid)
    "w" -> shiftCellsUp(grid)
    else -> throw IllegalArgumentException("Expected one of [a, s, d, w]")
}

fun shiftCellsLeft(grid: Array<Array<Int>>): Array<Array<Int>> =
    grid.map { row -> mergeAndOrganizeCells(row) }.toTypedArray()

fun shiftCellsRight(grid: Array<Array<Int>>): Array<Array<Int>> =
    grid.map { row -> mergeAndOrganizeCells(row.reversed().toTypedArray()).reversed().toTypedArray() }.toTypedArray()

fun shiftCellsUp(grid: Array<Array<Int>>): Array<Array<Int>> {
    val rows: Array<Array<Int>> = arrayOf(
            arrayOf(grid[0][0], grid[1][0], grid[2][0], grid[3][0]),
            arrayOf(grid[0][1], grid[1][1], grid[2][1], grid[3][1]),
            arrayOf(grid[0][2], grid[1][2], grid[2][2], grid[3][2]),
            arrayOf(grid[0][3], grid[1][3], grid[2][3], grid[3][3])
    )

    val updatedGrid = grid.copyOf()

    rows.map { row ->
        mergeAndOrganizeCells(row)
    }.forEachIndexed { rowIdx, row ->
        updatedGrid[0][rowIdx] = row[0]
        updatedGrid[1][rowIdx] = row[1]
        updatedGrid[2][rowIdx] = row[2]
        updatedGrid[3][rowIdx] = row[3]
    }

    return updatedGrid
}

fun shiftCellsDown(grid: Array<Array<Int>>): Array<Array<Int>> {
    val rows: Array<Array<Int>> = arrayOf(
            arrayOf(grid[3][0], grid[2][0], grid[1][0], grid[0][0]),
            arrayOf(grid[3][1], grid[2][1], grid[1][1], grid[0][1]),
            arrayOf(grid[3][2], grid[2][2], grid[1][2], grid[0][2]),
            arrayOf(grid[3][3], grid[2][3], grid[1][3], grid[0][3])
    )

    val updatedGrid = grid.copyOf()

    rows.map { row ->
        mergeAndOrganizeCells(row)
    }.forEachIndexed { rowIdx, row ->
        updatedGrid[3][rowIdx] = row[0]
        updatedGrid[2][rowIdx] = row[1]
        updatedGrid[1][rowIdx] = row[2]
        updatedGrid[0][rowIdx] = row[3]
    }

    return updatedGrid
}

fun mergeAndOrganizeCells(row: Array<Int>): Array<Int> = organize(merge(row.copyOf()))

fun merge(row: Array<Int>, idxToMatch: Int = 0, idxToCompare: Int = 1): Array<Int> {
    if (idxToMatch >= row.size)
        return row
    if (idxToCompare >= row.size)
        return merge(row, idxToMatch + 1, idxToMatch + 2)
    if (row[idxToMatch] == 0)
        return merge(row, idxToMatch + 1, idxToMatch + 2)

    if (row[idxToMatch] == row[idxToCompare]) {
        row[idxToMatch] *= 2
        row[idxToCompare] = 0

        return merge(row, idxToMatch + 1, idxToMatch + 2)
    } else {
        return if (row[idxToCompare] != 0) merge(row, idxToMatch + 1, idxToMatch + 2)
               else merge(row, idxToMatch, idxToCompare + 1)
    }
}

fun organize(row: Array<Int>, idxToMatch: Int = 0, idxToCompare: Int = 1): Array<Int> {
    if (idxToMatch >= row.size)
        return row
    if (idxToCompare >= row.size)
        return organize(row, idxToMatch + 1, idxToMatch + 2)
    if (row[idxToMatch] != 0)
        return organize(row, idxToMatch + 1, idxToMatch + 2)

    if (row[idxToCompare] != 0) {
        row[idxToMatch] = row[idxToCompare]
        row[idxToCompare] = 0

        return organize(row, idxToMatch + 1, idxToMatch + 2)
    } else {
        return organize(row, idxToMatch, idxToCompare + 1)
    }
}

fun display(grid: Array<Array<Int>>) {
    val prettyPrintableGrid = grid.map { row ->
        row.map { cell ->
            if (cell == 0) "    " else java.lang.String.format("%4d", cell)
        }
    }

    println("New Grid:")
    prettyPrintableGrid.forEach { row ->
        println("+----+----+----+----+")
        row.forEach { print("|$it") }
        println("|")
    }
    println("+----+----+----+----+")
}

Caveat: The game currently only checks if the grid is full, not if there are no playable moves left. This is an improvement to the game, but one that I have not implemented yet for no good reason.

\$\endgroup\$
  • \$\begingroup\$ Regarding your caveat... aren't there always playable moves as long as the grid is neither full nor empty? \$\endgroup\$ – Jacob Raihle Oct 29 '15 at 17:07
  • \$\begingroup\$ A 10 point question: how can you guess OP is Canadian? \$\endgroup\$ – набиячлэвэлиь Oct 29 '15 at 21:08
  • \$\begingroup\$ @JacobRaihle yes, technically. I modeled the solution after Rosetta Code's version of 2048, which stops with a winning game over message if you reach 2048. \$\endgroup\$ – IAE Oct 29 '15 at 23:38
  • \$\begingroup\$ @AnalPhabet to both our surprise, I'm german :) \$\endgroup\$ – IAE Oct 29 '15 at 23:39
  • \$\begingroup\$ @IAE You apologized twice, both in your you-lose message ("So sorry, but you lost the game.") and the you-won message ("So sorry, but you won the game."). You are Canadian, even if you don't know it \$\endgroup\$ – набиячлэвэлиь Oct 30 '15 at 0:11
1
\$\begingroup\$

First of all, you don't need to specify the return type if it is a one-liner and returned with a =.

One thing that I'd change are the functions isGridFull(), isGridSolved(), and shiftCells().

I think it's more readable when you create extension methods/properties for your grid:

fun Array<Array<Int>>.isGridSolved() = this.any { row -> row.contains(2048) } 
fun Array<Array<Int>>.isGridFull() = this.all { row -> !row.contains(0) }

And then use it like that:

if (grid.isGridSolved())    return positiveGameOverMessage
else if (grid.isGridFull()) return negativeGameOverMessage

Something not Kotlin related:

I wouldn't use an array of array of ints for your grid. Better create a class which contains the grid and offers methods like isFull(), isSolved(), and shiftCells().

It won't be much of a problem in smaller projects like this, but later there is no real distinction between your "grid" and an array of array of ints and you could mix it up.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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