3
\$\begingroup\$

I know there's a similar question to this: See here.

And I have taken the points mentioned there into consideration. However, I wanted to learn Kotlin and thought of writing OOP based Chess myself. My attempt at writing a simplistic chess game (between White and Black basically, player does not have much to do).

Basically I want to know, is my thought process correct for approaching this problem? Have I adhered to OOP principles? I've followed a primitive version of BDD to design and implement this.

Also, since this is my first Kotlin program, is there some Kotlin's groovy feature that I am missing?

    enum class Color { BLACK, WHITE }

    class Cell(var x: Int, var y: Int) {
        companion object {
            /**
             * Returns Cell if indexes are in boundary of a board. //TODO: Should this be moved to Board class, as it works on constraints of Board.
             */
            fun at(x: Int, y: Int) : Cell? {
                if(x < 0 || x > 7 || y < 0 || y > 7) return null
                return Cell(x,y)
            }

            /**
             * Takes a string like "a5" and returns a Cell with zero-indexed row number and column number, or error string
             */
            fun at(string: String) : Either<String,Cell> {
                val matchResult = "([a-h])(\\d)+".toRegex().matchEntire(string)

                val chars = listOf("a","b", "c", "d", "e", "f", "g", "h")

                val extractedValues = matchResult?.groupValues?.takeLast(2) ?: return Either.left("Could not get cell for $string. Try something like a4, b1")

                try {
                    val row = Integer.parseInt(extractedValues[1])
                    if(row < 1 || row > 8) return Either.left("Incorrect row number:$row")

                    return Either.right(Cell( row -1, chars.indexOf(extractedValues[0])))

                } catch (e: Exception){
                    return Either.left(e.toString())
                }
            }
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Cell

            if (x != other.x) return false
            if (y != other.y) return false

            return true
        }

        override fun hashCode(): Int {
            var result = x
            result = 31 * result + y
            return result
        }

        override fun toString(): String {
            val columns = listOf("a","b", "c", "d", "e", "f", "g", "h")

            return "${columns[y]}${x+1}"
        }

    }

    abstract class Piece(val color: Color) {
        var board: Board? = null

        /**
         * Useful for calculating valid cells to move.
         */
        val getCellsTillFirstNonEmpty = { from: Cell, rowDir : Int, colDir: Int ->
            val returnList: MutableList<Cell> = mutableListOf()
            var i = 1
            while (true) {
                val nextCell = Cell.at(from.x + i*rowDir, from.y + i*colDir) ?: break

                returnList += nextCell
                if(board!!.pieceAt(nextCell) != null ) {
                    break
                }
                i++
            }

            returnList
        }

        open fun calculateTargetSquares(from: Cell) : List<Cell?> {
            return emptyList()
        }

        fun possibleTargetSquares(from: Cell): List<Cell> {
            return calculateTargetSquares(from)
                .filterNotNull()
                .filter { board!!.pieceAt(it)?.color != this.color } //Cannot capture own's color!
        }
    }

    class Pawn(color: Color) : Piece(color) {
        var forwardStep = { x: Int -> if(color == Color.WHITE) -x else +x}
        var backwardStep = { x: Int -> if(color == Color.WHITE) +x else -x}

        /**
         * Pawn has special moves to capture opponent's pieces.
         */
        private fun capturableCells(from: Cell): List<Cell?> {
            return listOf(Cell.at(from.x + forwardStep(1), from.y + 1), Cell.at(from.x + forwardStep(1), from.y - 1))
                .filter { board!!.pieceAt(it) != null }
        }

        private fun forwardMoves(from: Cell): List<Cell> {
            val firstMove = from.x == 1 || from.x == 6
            if(firstMove) {
                return listOf(Cell(from.x + forwardStep(1), from.y), Cell(from.x + forwardStep(2), from.y))
                    .takeWhile { board!!.pieceAt(it) == null }
            } else {
                return listOf(Cell(from.x + forwardStep(1), from.y))
                    .takeWhile { board!!.pieceAt(it) == null }
            }
        }

        override fun calculateTargetSquares(from: Cell): List<Cell> {
           return forwardMoves(from) + capturableCells(from).filterNotNull()
        }

        override fun toString(): String {
            return if(color == Color.BLACK) "♟" else  "♙"
        }
    }

    class Knight(color: Color) : Piece(color) {

        override fun calculateTargetSquares(from: Cell): List<Cell?> {
            val x = from.x
            val y = from.y

            return listOf(Cell.at(x+2, y-1), Cell.at(x+2, y+1),
                          Cell.at(x-2, y-1), Cell.at(x-2, y+1),
                          Cell.at(x-1, y+2), Cell.at(x+1, y+2),
                          Cell.at(x-1, y-2), Cell.at(x+1, y-2))

        }

        override fun toString(): String {
            return if(color == Color.BLACK) "♞" else "♘"
        }
    }

    class Bishop(color: Color) : Piece(color) {
        override fun toString(): String {
            return if(color == Color.BLACK) "♝" else "♗"
        }

        override fun calculateTargetSquares(from: Cell): List<Cell?> {
            return getCellsTillFirstNonEmpty(from, +1, +1) +
            getCellsTillFirstNonEmpty(from, -1, -1) +
            getCellsTillFirstNonEmpty(from, +1, -1) +
            getCellsTillFirstNonEmpty(from, -1, +1)

        }
    }

    class Rook(color: Color) : Piece(color) {
        override fun toString(): String {
            return if(color == Color.BLACK) "♜" else "♖"
        }

        override fun calculateTargetSquares(from: Cell): List<Cell?> {
            return getCellsTillFirstNonEmpty(from, 0, +1) +
                    getCellsTillFirstNonEmpty(from, 0, -1) +
                    getCellsTillFirstNonEmpty(from, +1, 0) +
                    getCellsTillFirstNonEmpty(from, -1, 0)
        }
    }

    class Queen(color: Color) : Piece(color) {
        override fun toString(): String {
            return if(color == Color.BLACK) "♛" else "♕"
        }

        override fun calculateTargetSquares(from: Cell): List<Cell?> {
            return getCellsTillFirstNonEmpty(from, +1, +1) +
                    getCellsTillFirstNonEmpty(from, -1, -1) +
                    getCellsTillFirstNonEmpty(from, +1, -1) +
                    getCellsTillFirstNonEmpty(from, -1, +1) +
                    getCellsTillFirstNonEmpty(from, 0, +1) +
                    getCellsTillFirstNonEmpty(from, 0, -1) +
                    getCellsTillFirstNonEmpty(from, +1, 0) +
                    getCellsTillFirstNonEmpty(from, -1, 0)
        }
    }

    class King(color: Color) : Piece(color) {
        override fun toString(): String {
            return if(color == Color.BLACK) "♚" else "♔"
        }

        override fun calculateTargetSquares(from: Cell): List<Cell?> {
            return getCellsTillFirstNonEmpty(from, +1, +1).take(1) +
                    getCellsTillFirstNonEmpty(from, -1, -1).take(1) +
                    getCellsTillFirstNonEmpty(from, +1, -1).take(1) +
                    getCellsTillFirstNonEmpty(from, -1, +1).take(1) +
                    getCellsTillFirstNonEmpty(from, 0, +1).take(1) +
                    getCellsTillFirstNonEmpty(from, 0, -1).take(1) +
                    getCellsTillFirstNonEmpty(from, +1, 0).take(1) +
                    getCellsTillFirstNonEmpty(from, -1, 0).take(1)
        }
    }


    class Board() {
        var selectedCell: Cell? = null
        var cellsToHighlight: List<Cell> = emptyList()

        fun render() {
            val ANSI_RED_BACKGROUND = "\u001B[41m";
            val ANSI_YELLOW_BACKGROUND = "\u001B[43m";
            val ANSI_CYAN_BACKGROUND = "\u001B[46m";
            val ANSI_WHITE_BACKGROUND = "\u001B[47m";

            val ANSI_RESET = "\u001B[0m";

            val backgrounds = arrayOf(ANSI_CYAN_BACKGROUND, ANSI_WHITE_BACKGROUND)

            var backgroundIndex = 0

            val columns = listOf(" ","a ","b ", "c ", "d", "e ", "f ", "g ", "h ")

            println(columns.joinToString(" "))
            val EMPTY_CELL = "\u3000"

            for (i in 0..7) {
                val row = (0..7).toList().map {j ->
                    val cell = Cell(i, j)
                    val piece = cellToPieceMap[cell]
                    var backgroundColor = backgrounds[backgroundIndex++%2]

                    if(cell.equals(selectedCell) || cell in cellsToHighlight) backgroundColor = ANSI_YELLOW_BACKGROUND

                    backgroundColor + ( piece ?: EMPTY_CELL)+" "+ANSI_RESET

                }.joinToString("")

                println("${i+1} "+row + " "+(i+1))
                backgroundIndex++
            }
            println(ANSI_RESET+columns.joinToString(" "))

        }

        private var cellToPieceMap: MutableMap<Cell?, Piece> = mutableMapOf()

        init {
            val placePiece = { it: String, piece: Piece -> cellToPieceMap[Cell.at(it).getOrElse { null }] = piece; piece.board = this }

            listOf("a1", "h1").forEach { placePiece(it, Rook(Color.BLACK)) }
            listOf("a8", "h8").forEach { placePiece(it,  Rook(Color.WHITE)) }
            listOf("a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2").forEach { placePiece(it,  Pawn(Color.BLACK)) }
            listOf("a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7").forEach { placePiece(it,  Pawn(Color.WHITE)) }

            listOf("b1", "g1").forEach { placePiece(it,  Knight(Color.BLACK)) }
            listOf("b8", "g8").forEach { placePiece(it,  Knight(Color.WHITE)) }

            listOf("c1", "f1").forEach { placePiece(it,  Bishop(Color.BLACK)) }
            listOf("c8", "f8").forEach { placePiece(it,  Bishop(Color.WHITE)) }

            listOf("d8").forEach { placePiece(it,  Queen(Color.WHITE)) }
            listOf("e8").forEach { placePiece(it,  King(Color.WHITE)) }


            listOf("d1").forEach { placePiece(it,  Queen(Color.BLACK)) }
            listOf("e1").forEach { placePiece(it,  King(Color.BLACK)) }

        }

        private fun isCheckMate(){

        }

        private fun isStaleMate() {

        }

        private fun isDraw(){

        }

        fun pieceAt(cell: Cell?): Piece? {
            return this.cellToPieceMap[cell]
        }

        fun setPieceAt(cell: Cell, piece: Piece) {
            this.cellToPieceMap[cell] = piece
        }

        fun removePiece(cell: Cell) {
            this.cellToPieceMap.remove(cell)
        }
    }


    class Player(private var name: String) {

        override fun toString(): String {
            return name
        }
    }

    enum class MoveType {
        NORMAL, EN_PASSANT, PROMOTION
    }

    class Move (val piece: Piece, val from: Cell, val to: Cell, var moveType: MoveType = MoveType.NORMAL) {
        var capturedPiece: Piece? = null
        var capturedCell: Cell? = null

        init {
            if(piece.color == Color.WHITE && to.x == 0) moveType = MoveType.PROMOTION
            if(piece.color == Color.BLACK && to.x == 7) moveType = MoveType.PROMOTION
        }

        fun algebraicNotation(): String {
            return "" //TODO
        }
    }

    class ChessGame(var playerA: Player, var playerB: Player) {
        var isRunning = false
        var moves: MutableList<Move> = mutableListOf()

        private var turn: Int = 0
        val colorA = Color.WHITE
        val colorB = Color.BLACK

        private val capturedPieces: Map<Color, MutableList<Piece>> = mapOf(
            Pair(Color.WHITE, mutableListOf()),
            Pair(Color.BLACK, mutableListOf())
        )

        //which color for which player? TODO

        val board: Board = Board()

        /**
         * One-time opportunity for pawn to capture opposite pawn.
         */
        fun enPassantMove(cell: Cell): Move? {
            val lastMove = moves.lastOrNull() ?: return null

            val pawnToCapture = lastMove.piece
            val positionOfCapture = lastMove.to

            if(pawnToCapture is Pawn && Math.abs(positionOfCapture.x - lastMove.from.x) == 2 && //was starting move of pawn && was 2-step move
                (cell.x == positionOfCapture.x) &&  //on same level
                Math.abs(cell.y - positionOfCapture.y) == 1) { //on adjacent column

                val move = Move(
                    board.pieceAt(cell)!!,
                    cell,
                    Cell.at(positionOfCapture.x + pawnToCapture.backwardStep(1), positionOfCapture.y)!!,
                    MoveType.EN_PASSANT
                )
                move.capturedPiece = pawnToCapture
                move.capturedCell = positionOfCapture

                return move
            }

            return null
        }

        private fun validCellsToMove(sourceCell: Cell) : Either<String, List<Cell>> {
            val selectedPiece = board.pieceAt(sourceCell) ?: return Either.left("No piece at $sourceCell")

            val selectedColor = selectedPiece.color
            if(selectedColor != getTurn()) return Either.left("You cannot pick $selectedColor")

            val possibleTargetSquares = selectedPiece.possibleTargetSquares(sourceCell)
            if(possibleTargetSquares.isEmpty()) return Either.left("Nowhere to go! Select some other square!")
            return Either.right(possibleTargetSquares)
        }

        fun makeMove(move: Move) : Piece? {
            val targetCell = move.to
            val sourceCell = move.from

            var capturedPiece = board.pieceAt(targetCell)
            val sourcePiece = board.pieceAt(sourceCell)

            board.setPieceAt(targetCell,  sourcePiece!!)
            board.removePiece(sourceCell)

            if(move.moveType == MoveType.EN_PASSANT) {
               board.removePiece(move.capturedCell!!)
                capturedPiece = move.capturedPiece
            }

            move.capturedPiece = capturedPiece
            moves.plusAssign(move)

            val piecesCapturedByThisColor = capturedPieces[getTurn()]!!

            if(capturedPiece != null) {
                piecesCapturedByThisColor.plusAssign(capturedPiece)
            }

            board.selectedCell = null
            board.cellsToHighlight = emptyList()

            return capturedPiece
        }

        fun getTurn(): Color {
            return if(turn == 0) Color.WHITE else Color.BLACK;
        }

        fun run() {
            if(isRunning) return

            isRunning = true
            while (true) {
                this.board.render()
                val move = getAValidMove(this.board)
                val capturedPieces = this.makeMove(move)

                if(capturedPieces != null) {
                    println("You now have bagged: $capturedPieces")
                }

                turn = (turn+1) % 2
            }
        }

        private fun getAValidMove(board: Board): Move {
            while (true) {
                println("select square [${this.getTurn()}] >")
                val sourceCellCmd: String = readLine().orEmpty()

                if(Cell.at(sourceCellCmd).isLeft()) {
                    println("Invalid cell! Choose something like a8, b4 etc!")
                    continue
                }

                val source = Cell.at(sourceCellCmd).get()
                val cellsToMove = this.validCellsToMove(source)

                val enPassant: Move? = this.enPassantMove(source)

                if(cellsToMove.isLeft() && enPassant == null) {
                    println("Invalid move! $cellsToMove")
                    continue
                }

                board.selectedCell = source
                var possibleTargetCells: List<Cell> = cellsToMove.fold( {x -> emptyList() },  { x -> x} )

                if(enPassant != null) {
                    possibleTargetCells += enPassant.to
                }

                board.cellsToHighlight = possibleTargetCells
                board.render()

                while (true) {
                    println("Type 'undo' to start over your move. Where do you want to move? Choose from $possibleTargetCells")
                    val targetCellCmd = readLine().orEmpty()

                    if(targetCellCmd == "undo") {
                        return getAValidMove(board)
                    }

                    val targetCell = Cell.at(targetCellCmd)
                    if(targetCell.isLeft()) continue

                    if(enPassant != null && targetCell.get() == enPassant.to) {
                        return enPassant
                    }

                    if(targetCell.get() in possibleTargetCells) {
                        return Move(board.pieceAt(source)!!, source, Cell.at(targetCellCmd).get())
                    }
                }
            }
        }

    }

Here's my thought process (design considerations) while implementing this:

Requirements Analysis

Core Requirements:

  • I can create a new game to play with my friend.
  • We can see the board with pieces arranged properly.
  • I get color white. White makes the first move.
  • Black gets his turn. We make turns after each move.
  • If white tries to move black piece, the game does not allow it. Similarly for white piece.
  • When I select a piece, valid moves are shown to me.
  • When I select from valid moves, pieces move appropriately and board is updated.
  • I must be able to save and resume the game.

Extra Requirements (which we can think of in advance):

  • I can play against the computer.
  • I can undo my moves.
  • I can play online with other people.
  • The UI of the game can be enhanced with rich interface.

Identify entities:

Some of them clearly obvious are: Game,Board,Square/Cell,Player

How are they related? How will they interact?

  • A Game will run in a loop, making turns, asking for input from the player.
  • Or, we can make this reactive. game will receive commands from player, and if it is valid a move will be executed.
  • Initially pieces are laid out on the board. Board knows which squares/Cells has which piece. Board manages the layout of the pieces.
  • Piece has certain properties like color.
  • Piece needs to move, and for a valid move it needs to have access to Board
  • Different types of pieces have different rules for moves
  • A Piece must be able to figure out where it can go, given access to the Board
  • There are certain moves which piece cannot figure out just by looking at the board, e.g. En-Passant which needs to know what was the last move
  • We can clearly see that moves should be stored for various purpose (undoing, validating en-passe etc). Hence we need to model Move

\$\endgroup\$
  • \$\begingroup\$ I would recommend that you post the readme file here as well, it will make your question here more self-contained. Also see this meta question \$\endgroup\$ – Simon Forsberg Jul 7 at 18:35
  • \$\begingroup\$ It looks like some features are still unfinished. You should probably declare what is in scope for this review. \$\endgroup\$ – 200_success Jul 7 at 19:36
  • \$\begingroup\$ @200_success I expect the review on the thought process and design considerations, do the classes have correct responsibility, is the relationship among them correct as per OOP principles? Also, am I missing something that can cause a huge blow need I have some legit requirement in future. \$\endgroup\$ – Satyendra Kumar Jul 8 at 5:25

Your Answer

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

Browse other questions tagged or ask your own question.