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 '19 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 '19 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 '19 at 5:25
  • \$\begingroup\$ I remember and can relate to your code here, 20 years ago when learning C++ I use chess. Queen is multiple inheritance of bishop and rook. And then, I had to ahead compute the board because it's the only way to validate a move to examine if the king is in check as a result. And then suddenly, somebody commented out why I have a Queen, pawn, Bishop class because for him he wants the classes to be the squares instead such as a1, a2,... e4.. etc and the type of pieces becomes. attributes of the squares. At that time I thought his conception was stupid why he represents the squares. \$\endgroup\$ – eigenfield May 21 at 17:12

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.