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/Cell
s 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 toBoard
- Different types of pieces have different rules for moves
- A
Piece
must be able to figure out where it can go, given access to theBoard
- 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