whitewind664/sdp

View on GitHub
app/src/main/java/com/github/gogetters/letsgo/game/Board.kt

Summary

Maintainability
A
2 hrs
Test Coverage
A
98%
package com.github.gogetters.letsgo.game

import com.github.gogetters.letsgo.game.exceptions.KoException
import com.github.gogetters.letsgo.game.exceptions.NotEmptyException
import com.github.gogetters.letsgo.game.exceptions.OutOfBoardException
import com.github.gogetters.letsgo.game.exceptions.SuicideException
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet

class Board(private val boardSize: Size) {

    val size
    get() = boardSize.size

    enum class Size(val size: Int) {
        SMALL(9), MEDIUM(13), LARGE(19);

        companion object {
            fun withSize(size: Int): Size {
                return when (size) {
                    19 -> LARGE
                    13 -> MEDIUM
                    else -> SMALL
                }
            }
        }
    }

    /**
     * Requirements for Ko to be possible when stone X is placed:
     *  - Stone X has captured a single stone Y
     *  - Stone X is left with only 1 liberty
     *  - Stone X is its own group of size 1
     * Then Ko will occur if we play where stone Y was
     *
     * Only one of these positions can exist each turn
     * We keep track of this position in the koPoint variable
     */
    private var koMove: Move? = null
    private val board = BoardState.emptyBoard(boardSize)

    private fun insideBoard(c: Point): Boolean {
        return c.first in 1..size && c.second in 1..size
    }

    private fun isEmpty(c: Point): Boolean {
        if (!insideBoard(c)) throw OutOfBoardException()
        val temp = board[c]
        return temp == Stone.EMPTY
    }


    /**
     * Retrieves the neighbors of a point. Points diagonally away do not count as neighbors.
     * The neighbors are restricted to points on the board.
     *
     * @param c point to get the neighbors of
     * @return list of neighboring points
     */
    private fun getNeighbors(c: Point): List<Point> {
        if (!insideBoard(c)) throw OutOfBoardException()

        val neighbors = ArrayList<Point>()

        var d = Point(0, 1)
        for (i in 0..3) {
            val neighbor = c + d
            if (insideBoard(neighbor)) neighbors.add(neighbor)
            d = Point(d.second, -d.first)
        }

        return neighbors
    }


    /**
     * Retrieves the "group" connected to a point. A group is defined as a set of connected points
     * that are of the same type (e.g. all white stones). Connected empty points can also form a
     * group.
     *
     * @param c Point that should be one of the members of the group
     * @return Set of points corresponding to the group
     * @throws OutOfBoardException if the point given is outside of the board
     */
    private fun getGroup(c: Point): Set<Point> {
        if (!insideBoard(c)) throw OutOfBoardException()
        val groupColor = board[c]
        val considering = LinkedList<Point>()
        val connected = HashSet<Point>()
        considering.add(c)

        while (considering.size != 0) {
            val head = considering.removeFirst()
            for (neighbor in getNeighbors(head)) {
                val sameColor = board[neighbor] == groupColor
                if (neighbor !in considering && neighbor !in connected && sameColor) {
                    considering.add(neighbor)
                }
            }

            connected.add(head)
        }

        return connected
    }


    /**
     * Counts the number of liberties of a given group. A liberty is defined as an empty point that
     * is connected to the group. This function therefore returns the number of empty points that
     * are connected to any member of a group of points.
     *
     * @param group iterable representing the group of points
     * @return the number of liberties of "group"
     */
    private fun countLiberties(group: Iterable<Point>): Int {
        val liberties = HashSet<Point>()
        for (stone in group) {
            for (neighbor in getNeighbors(stone)) {
                if (isEmpty(neighbor)) liberties.add(neighbor)
            }
        }

        return liberties.size
    }


    /**
     * Checks if playing a move would result in suicide (illegal move). Suicide happens if a player
     * reduces the number of liberties of a group to zero (*after* capture).
     *
     * @param m move that could potentially result in suicide
     * @returns true if move would be suicide, false otherwise
     * @throws OutOfBoardException if the point given is outside the board
     * @throws NotEmptyException if the point given is already occupied by a stone
     *
     */
    private fun isSuicide(m: Move): Boolean {
        if (!insideBoard(m.point)) throw OutOfBoardException()
        if (!isEmpty(m.point)) throw NotEmptyException()

        val neighbors = getNeighbors(m.point)
        //CHECK IF WE HAVE AIR TO BREATHE, OR IF WE KILLED AN ENEMY GROUP
        for (neighbor in neighbors) {
            if (isEmpty(neighbor)) return false

            if (board[neighbor] == m.stone.otherColor()) {
                val enemyGroup = getGroup(neighbor)
                if (countLiberties(enemyGroup) == 1) {
                    return false
                }
            }
        }

        //OTHERWISE CHECK IF WE ARE CONNECTED TO A GROUP WITH MORE THAN ONE LIBERTY
        for (neighbor in neighbors) {
            if (board[neighbor] == m.stone) {
                val ourGroup = getGroup(neighbor)
                if (countLiberties(ourGroup) > 1) {
                    return false
                }
            }
        }
        return true

    }

    /**
     * Checks if a move would break the Ko rule. There can only be at most one of these moves per
     * turn.
     *
     * @param m move that might break the Ko rule
     * @return true if playing m would break the Ko rule, false otherwise
     */
    private fun isKo(m: Move): Boolean = m == koMove


    /**
     * Returns a data object that represents the current board state
     */
    fun getBoardState(whiteScore: Int, blackScore: Int,
                      gameOver: Boolean=false, lastMove: Move? = null): BoardState =
            BoardState(HashMap(board), koMove = koMove,
                    whiteScore = whiteScore, blackScore = blackScore,
                    gameOver = gameOver, lastMove = lastMove)


    /**
     * Executes a given move, throwing an exception if the move is illegal. Returns the number of
     * stones captured by the move.
     *
     * @param m move to play
     * @return number of stones captured by playing m
     * @throws OutOfBoardException if the move places a stone outside the board
     * @throws NotEmptyException if the move places a stone on top of another stone
     * @throws SuicideException if the move would result in suicide
     * @throws KoException if the move would break the Ko rule
     */
    fun playMove(m: Move): Int {
        val capturedStones = HashSet<Point>()

        if (m.stone != Stone.EMPTY) {
            if (!insideBoard(m.point)) throw OutOfBoardException()
            if (!isEmpty(m.point)) throw NotEmptyException()
            if (isSuicide(m)) throw SuicideException()
            if (isKo(m)) throw KoException()

            board[m.point] = m.stone
            for (neighbor in getNeighbors(m.point)) {
                if (board[neighbor] == m.stone.otherColor()) {

                    val enemyGroup = getGroup(neighbor)
                    if (countLiberties(enemyGroup) == 0) {
                        for (enemy in enemyGroup) {
                            board[enemy] = Stone.EMPTY
                            capturedStones.add(enemy)
                        }
                    }
                }
            }

            koMove =
                    if (capturedStones.size == 1)
                        Move(m.stone.otherColor(), capturedStones.random())
                    else null

        } else {
            if (insideBoard(m.point)) {
                board[m.point] = m.stone
            }
            koMove = null
        }

        return capturedStones.size
    }

    fun playMove(s: Stone, p: Point) = playMove(Move(s, p))
}