diff --git a/chess-game/.gitignore b/chess-game/.gitignore
new file mode 100644
index 000000000..58f7f18ac
--- /dev/null
+++ b/chess-game/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+app/build/
diff --git a/chess-game/app/build.gradle b/chess-game/app/build.gradle
new file mode 100644
index 000000000..663cf6abd
--- /dev/null
+++ b/chess-game/app/build.gradle
@@ -0,0 +1,38 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.devin.chess'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.devin.chess"
+ minSdk 26
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+}
diff --git a/chess-game/app/proguard-rules.pro b/chess-game/app/proguard-rules.pro
new file mode 100644
index 000000000..fb164d666
--- /dev/null
+++ b/chess-game/app/proguard-rules.pro
@@ -0,0 +1 @@
+# Add project specific ProGuard rules here.
diff --git a/chess-game/app/src/main/AndroidManifest.xml b/chess-game/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c0df9fb63
--- /dev/null
+++ b/chess-game/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chess-game/app/src/main/java/com/devin/chess/ChessBoardView.kt b/chess-game/app/src/main/java/com/devin/chess/ChessBoardView.kt
new file mode 100644
index 000000000..5d657d6df
--- /dev/null
+++ b/chess-game/app/src/main/java/com/devin/chess/ChessBoardView.kt
@@ -0,0 +1,236 @@
+package com.devin.chess
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+
+class ChessBoardView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+ private val lightSquare = Paint().apply { color = Color.parseColor("#F0D9B5") }
+ private val darkSquare = Paint().apply { color = Color.parseColor("#B58863") }
+ private val selectedPaint = Paint().apply { color = Color.parseColor("#7BF06292") }
+ private val validMovePaint = Paint().apply {
+ color = Color.parseColor("#6044CC44")
+ style = Paint.Style.FILL
+ }
+ private val lastMovePaint = Paint().apply { color = Color.parseColor("#40FFFF00") }
+ private val checkPaint = Paint().apply { color = Color.parseColor("#80FF0000") }
+ private val piecePaint = Paint().apply {
+ textAlign = Paint.Align.CENTER
+ typeface = Typeface.DEFAULT_BOLD
+ isAntiAlias = true
+ }
+ private val coordinatePaint = Paint().apply {
+ color = Color.parseColor("#80000000")
+ textAlign = Paint.Align.CENTER
+ isAntiAlias = true
+ }
+
+ private var squareSize = 0f
+ private var boardOffset = 0f
+ var model: ChessModel? = null
+ private var selectedPos: Position? = null
+ private var validMoves: List = emptyList()
+
+ var onMoveListener: (() -> Unit)? = null
+
+ private val pieceSymbols = mapOf(
+ PieceType.KING to ("♔" to "♚"),
+ PieceType.QUEEN to ("♕" to "♛"),
+ PieceType.ROOK to ("♖" to "♜"),
+ PieceType.BISHOP to ("♗" to "♝"),
+ PieceType.KNIGHT to ("♘" to "♞"),
+ PieceType.PAWN to ("♙" to "♟")
+ )
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val size = minOf(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec)
+ )
+ setMeasuredDimension(size, size)
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ val boardSize = minOf(w, h)
+ squareSize = boardSize / 8.4f
+ boardOffset = squareSize * 0.2f
+ piecePaint.textSize = squareSize * 0.78f
+ coordinatePaint.textSize = squareSize * 0.18f
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val game = model ?: return
+
+ drawBoard(canvas, game)
+ drawPieces(canvas, game)
+ drawCoordinates(canvas)
+ }
+
+ private fun drawBoard(canvas: Canvas, game: ChessModel) {
+ val lastMove = game.lastMove
+ val kingPos = if (game.isCurrentPlayerInCheck()) {
+ findKingPosition(game, game.currentTurn)
+ } else null
+
+ for (row in 0..7) {
+ for (col in 0..7) {
+ val x = boardOffset + col * squareSize
+ val y = boardOffset + row * squareSize
+ val isLight = (row + col) % 2 == 0
+ canvas.drawRect(x, y, x + squareSize, y + squareSize, if (isLight) lightSquare else darkSquare)
+
+ // Highlight last move
+ if (lastMove != null &&
+ ((row == lastMove.from.row && col == lastMove.from.col) ||
+ (row == lastMove.to.row && col == lastMove.to.col))) {
+ canvas.drawRect(x, y, x + squareSize, y + squareSize, lastMovePaint)
+ }
+
+ // Highlight check
+ if (kingPos != null && row == kingPos.row && col == kingPos.col) {
+ canvas.drawRect(x, y, x + squareSize, y + squareSize, checkPaint)
+ }
+
+ // Highlight selected
+ val sel = selectedPos
+ if (sel != null && row == sel.row && col == sel.col) {
+ canvas.drawRect(x, y, x + squareSize, y + squareSize, selectedPaint)
+ }
+
+ // Valid move indicators
+ val pos = Position(row, col)
+ if (pos in validMoves) {
+ val piece = game.getPiece(row, col)
+ if (piece != null) {
+ // Capture indicator — ring around the square
+ val ringPaint = Paint().apply {
+ color = Color.parseColor("#6044CC44")
+ style = Paint.Style.STROKE
+ strokeWidth = squareSize * 0.08f
+ }
+ canvas.drawRect(
+ x + squareSize * 0.04f, y + squareSize * 0.04f,
+ x + squareSize * 0.96f, y + squareSize * 0.96f, ringPaint
+ )
+ } else {
+ // Move indicator — small circle
+ canvas.drawCircle(
+ x + squareSize / 2, y + squareSize / 2,
+ squareSize * 0.15f, validMovePaint
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun drawPieces(canvas: Canvas, game: ChessModel) {
+ for (row in 0..7) {
+ for (col in 0..7) {
+ val piece = game.getPiece(row, col) ?: continue
+ val x = boardOffset + col * squareSize + squareSize / 2
+ val y = boardOffset + row * squareSize + squareSize * 0.72f
+ val symbol = pieceSymbols[piece.type] ?: continue
+ val text = if (piece.color == PieceColor.WHITE) symbol.first else symbol.second
+
+ // Draw outline for visibility
+ piecePaint.color = if (piece.color == PieceColor.WHITE) Color.BLACK else Color.parseColor("#333333")
+ piecePaint.style = Paint.Style.STROKE
+ piecePaint.strokeWidth = squareSize * 0.03f
+ canvas.drawText(text, x, y, piecePaint)
+
+ // Draw fill
+ piecePaint.color = if (piece.color == PieceColor.WHITE) Color.WHITE else Color.BLACK
+ piecePaint.style = Paint.Style.FILL
+ canvas.drawText(text, x, y, piecePaint)
+ }
+ }
+ }
+
+ private fun drawCoordinates(canvas: Canvas) {
+ val files = "abcdefgh"
+ val ranks = "87654321"
+ for (i in 0..7) {
+ // File labels (bottom)
+ canvas.drawText(
+ files[i].toString(),
+ boardOffset + i * squareSize + squareSize / 2,
+ boardOffset + 8 * squareSize + squareSize * 0.18f,
+ coordinatePaint
+ )
+ // Rank labels (left)
+ coordinatePaint.textAlign = Paint.Align.RIGHT
+ canvas.drawText(
+ ranks[i].toString(),
+ boardOffset - squareSize * 0.05f,
+ boardOffset + i * squareSize + squareSize * 0.6f,
+ coordinatePaint
+ )
+ coordinatePaint.textAlign = Paint.Align.CENTER
+ }
+ }
+
+ private fun findKingPosition(game: ChessModel, color: PieceColor): Position? {
+ for (r in 0..7) {
+ for (c in 0..7) {
+ val piece = game.getPiece(r, c)
+ if (piece != null && piece.type == PieceType.KING && piece.color == color) {
+ return Position(r, c)
+ }
+ }
+ }
+ return null
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (event.action != MotionEvent.ACTION_DOWN) return true
+ val game = model ?: return true
+ if (game.gameOver) return true
+
+ val col = kotlin.math.floor((event.x - boardOffset) / squareSize).toInt()
+ val row = kotlin.math.floor((event.y - boardOffset) / squareSize).toInt()
+ if (row !in 0..7 || col !in 0..7) return true
+
+ val tappedPos = Position(row, col)
+ val sel = selectedPos
+
+ if (sel != null && tappedPos in validMoves) {
+ game.makeMove(sel, tappedPos)
+ selectedPos = null
+ validMoves = emptyList()
+ invalidate()
+ onMoveListener?.invoke()
+ return true
+ }
+
+ val piece = game.getPiece(row, col)
+ if (piece != null && piece.color == game.currentTurn) {
+ selectedPos = tappedPos
+ validMoves = game.getValidMoves(tappedPos)
+ } else {
+ selectedPos = null
+ validMoves = emptyList()
+ }
+
+ invalidate()
+ return true
+ }
+
+ fun clearSelection() {
+ selectedPos = null
+ validMoves = emptyList()
+ invalidate()
+ }
+}
diff --git a/chess-game/app/src/main/java/com/devin/chess/ChessModel.kt b/chess-game/app/src/main/java/com/devin/chess/ChessModel.kt
new file mode 100644
index 000000000..26a6d9280
--- /dev/null
+++ b/chess-game/app/src/main/java/com/devin/chess/ChessModel.kt
@@ -0,0 +1,393 @@
+package com.devin.chess
+
+enum class PieceType {
+ KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN
+}
+
+enum class PieceColor {
+ WHITE, BLACK;
+
+ fun opposite(): PieceColor = if (this == WHITE) BLACK else WHITE
+}
+
+data class ChessPiece(
+ val type: PieceType,
+ val color: PieceColor
+)
+
+data class Position(val row: Int, val col: Int) {
+ fun isValid(): Boolean = row in 0..7 && col in 0..7
+}
+
+data class Move(val from: Position, val to: Position)
+
+class ChessModel {
+
+ private val board = Array>(8) { arrayOfNulls(8) }
+ var currentTurn: PieceColor = PieceColor.WHITE
+ private set
+ var gameOver: Boolean = false
+ private set
+ var winner: PieceColor? = null
+ private set
+ var lastMove: Move? = null
+ private set
+ private var whiteKingMoved = false
+ private var blackKingMoved = false
+ private var whiteRookAMoved = false
+ private var whiteRookHMoved = false
+ private var blackRookAMoved = false
+ private var blackRookHMoved = false
+
+ init {
+ setupBoard()
+ }
+
+ private fun setupBoard() {
+ val backRow = arrayOf(
+ PieceType.ROOK, PieceType.KNIGHT, PieceType.BISHOP, PieceType.QUEEN,
+ PieceType.KING, PieceType.BISHOP, PieceType.KNIGHT, PieceType.ROOK
+ )
+ for (col in 0..7) {
+ board[0][col] = ChessPiece(backRow[col], PieceColor.BLACK)
+ board[1][col] = ChessPiece(PieceType.PAWN, PieceColor.BLACK)
+ board[6][col] = ChessPiece(PieceType.PAWN, PieceColor.WHITE)
+ board[7][col] = ChessPiece(backRow[col], PieceColor.WHITE)
+ }
+ }
+
+ fun getPiece(row: Int, col: Int): ChessPiece? {
+ if (row !in 0..7 || col !in 0..7) return null
+ return board[row][col]
+ }
+
+ fun makeMove(from: Position, to: Position): Boolean {
+ if (gameOver) return false
+ val piece = board[from.row][from.col] ?: return false
+ if (piece.color != currentTurn) return false
+ if (!isValidMove(from, to)) return false
+
+ // Handle castling
+ if (piece.type == PieceType.KING && kotlin.math.abs(to.col - from.col) == 2) {
+ performCastling(from, to)
+ } else {
+ // Handle en passant capture
+ if (piece.type == PieceType.PAWN && to.col != from.col && board[to.row][to.col] == null) {
+ board[from.row][to.col] = null
+ }
+
+ board[to.row][to.col] = piece
+ board[from.row][from.col] = null
+ }
+
+ // Pawn promotion — auto-promote to queen
+ if (piece.type == PieceType.PAWN && (to.row == 0 || to.row == 7)) {
+ board[to.row][to.col] = ChessPiece(PieceType.QUEEN, piece.color)
+ }
+
+ updateCastlingFlags(piece, from)
+
+ lastMove = Move(from, to)
+ currentTurn = currentTurn.opposite()
+
+ checkGameState()
+ return true
+ }
+
+ private fun performCastling(kingFrom: Position, kingTo: Position) {
+ val piece = board[kingFrom.row][kingFrom.col]!!
+ board[kingTo.row][kingTo.col] = piece
+ board[kingFrom.row][kingFrom.col] = null
+
+ if (kingTo.col == 6) {
+ // Kingside
+ board[kingFrom.row][5] = board[kingFrom.row][7]
+ board[kingFrom.row][7] = null
+ } else if (kingTo.col == 2) {
+ // Queenside
+ board[kingFrom.row][3] = board[kingFrom.row][0]
+ board[kingFrom.row][0] = null
+ }
+ }
+
+ private fun updateCastlingFlags(piece: ChessPiece, from: Position) {
+ if (piece.type == PieceType.KING) {
+ if (piece.color == PieceColor.WHITE) whiteKingMoved = true
+ else blackKingMoved = true
+ }
+ if (piece.type == PieceType.ROOK) {
+ when {
+ piece.color == PieceColor.WHITE && from.row == 7 && from.col == 0 -> whiteRookAMoved = true
+ piece.color == PieceColor.WHITE && from.row == 7 && from.col == 7 -> whiteRookHMoved = true
+ piece.color == PieceColor.BLACK && from.row == 0 && from.col == 0 -> blackRookAMoved = true
+ piece.color == PieceColor.BLACK && from.row == 0 && from.col == 7 -> blackRookHMoved = true
+ }
+ }
+ }
+
+ fun getValidMoves(pos: Position): List {
+ val piece = board[pos.row][pos.col] ?: return emptyList()
+ if (piece.color != currentTurn) return emptyList()
+ return getAllPossibleMoves(pos).filter { to ->
+ !wouldBeInCheck(pos, to, piece.color)
+ }
+ }
+
+ private fun isValidMove(from: Position, to: Position): Boolean {
+ return to in getValidMoves(from)
+ }
+
+ private fun getAllPossibleMoves(pos: Position): List {
+ val piece = board[pos.row][pos.col] ?: return emptyList()
+ return when (piece.type) {
+ PieceType.PAWN -> getPawnMoves(pos, piece.color)
+ PieceType.KNIGHT -> getKnightMoves(pos, piece.color)
+ PieceType.BISHOP -> getBishopMoves(pos, piece.color)
+ PieceType.ROOK -> getRookMoves(pos, piece.color)
+ PieceType.QUEEN -> getBishopMoves(pos, piece.color) + getRookMoves(pos, piece.color)
+ PieceType.KING -> getKingMoves(pos, piece.color)
+ }
+ }
+
+ private fun getPawnMoves(pos: Position, color: PieceColor): List {
+ val moves = mutableListOf()
+ val direction = if (color == PieceColor.WHITE) -1 else 1
+ val startRow = if (color == PieceColor.WHITE) 6 else 1
+
+ // Forward one
+ val oneForward = Position(pos.row + direction, pos.col)
+ if (oneForward.isValid() && board[oneForward.row][oneForward.col] == null) {
+ moves.add(oneForward)
+ // Forward two from start
+ if (pos.row == startRow) {
+ val twoForward = Position(pos.row + 2 * direction, pos.col)
+ if (board[twoForward.row][twoForward.col] == null) {
+ moves.add(twoForward)
+ }
+ }
+ }
+
+ // Diagonal captures
+ for (dc in listOf(-1, 1)) {
+ val capture = Position(pos.row + direction, pos.col + dc)
+ if (capture.isValid()) {
+ val target = board[capture.row][capture.col]
+ if (target != null && target.color != color) {
+ moves.add(capture)
+ }
+ // En passant
+ if (target == null && isEnPassant(pos, capture, color)) {
+ moves.add(capture)
+ }
+ }
+ }
+ return moves
+ }
+
+ private fun isEnPassant(from: Position, to: Position, color: PieceColor): Boolean {
+ val last = lastMove ?: return false
+ val lastPiece = board[last.to.row][last.to.col] ?: return false
+ if (lastPiece.type != PieceType.PAWN) return false
+ if (kotlin.math.abs(last.from.row - last.to.row) != 2) return false
+ if (last.to.row != from.row) return false
+ if (last.to.col != to.col) return false
+ return true
+ }
+
+ private fun getKnightMoves(pos: Position, color: PieceColor): List {
+ val offsets = listOf(
+ -2 to -1, -2 to 1, -1 to -2, -1 to 2,
+ 1 to -2, 1 to 2, 2 to -1, 2 to 1
+ )
+ return offsets.map { (dr, dc) -> Position(pos.row + dr, pos.col + dc) }
+ .filter { it.isValid() && (board[it.row][it.col] == null || board[it.row][it.col]!!.color != color) }
+ }
+
+ private fun getSlidingMoves(pos: Position, color: PieceColor, directions: List>): List {
+ val moves = mutableListOf()
+ for ((dr, dc) in directions) {
+ var r = pos.row + dr
+ var c = pos.col + dc
+ while (r in 0..7 && c in 0..7) {
+ val target = board[r][c]
+ if (target == null) {
+ moves.add(Position(r, c))
+ } else {
+ if (target.color != color) moves.add(Position(r, c))
+ break
+ }
+ r += dr
+ c += dc
+ }
+ }
+ return moves
+ }
+
+ private fun getBishopMoves(pos: Position, color: PieceColor): List {
+ return getSlidingMoves(pos, color, listOf(-1 to -1, -1 to 1, 1 to -1, 1 to 1))
+ }
+
+ private fun getRookMoves(pos: Position, color: PieceColor): List {
+ return getSlidingMoves(pos, color, listOf(-1 to 0, 1 to 0, 0 to -1, 0 to 1))
+ }
+
+ private fun getKingMoves(pos: Position, color: PieceColor): List {
+ val moves = mutableListOf()
+ for (dr in -1..1) {
+ for (dc in -1..1) {
+ if (dr == 0 && dc == 0) continue
+ val to = Position(pos.row + dr, pos.col + dc)
+ if (to.isValid() && (board[to.row][to.col] == null || board[to.row][to.col]!!.color != color)) {
+ moves.add(to)
+ }
+ }
+ }
+ // Castling
+ moves.addAll(getCastlingMoves(pos, color))
+ return moves
+ }
+
+ private fun getCastlingMoves(kingPos: Position, color: PieceColor): List {
+ val moves = mutableListOf()
+ if (isInCheck(color)) return moves
+
+ val row = kingPos.row
+ val kingMoved = if (color == PieceColor.WHITE) whiteKingMoved else blackKingMoved
+
+ if (kingMoved) return moves
+
+ // Kingside
+ val rookHMoved = if (color == PieceColor.WHITE) whiteRookHMoved else blackRookHMoved
+ if (!rookHMoved && board[row][5] == null && board[row][6] == null) {
+ val rook = board[row][7]
+ if (rook != null && rook.type == PieceType.ROOK && rook.color == color) {
+ if (!isSquareAttacked(Position(row, 5), color.opposite()) &&
+ !isSquareAttacked(Position(row, 6), color.opposite())) {
+ moves.add(Position(row, 6))
+ }
+ }
+ }
+
+ // Queenside
+ val rookAMoved = if (color == PieceColor.WHITE) whiteRookAMoved else blackRookAMoved
+ if (!rookAMoved && board[row][1] == null && board[row][2] == null && board[row][3] == null) {
+ val rook = board[row][0]
+ if (rook != null && rook.type == PieceType.ROOK && rook.color == color) {
+ if (!isSquareAttacked(Position(row, 2), color.opposite()) &&
+ !isSquareAttacked(Position(row, 3), color.opposite())) {
+ moves.add(Position(row, 2))
+ }
+ }
+ }
+
+ return moves
+ }
+
+ private fun isSquareAttacked(pos: Position, byColor: PieceColor): Boolean {
+ for (r in 0..7) {
+ for (c in 0..7) {
+ val piece = board[r][c] ?: continue
+ if (piece.color != byColor) continue
+ val attackerPos = Position(r, c)
+ val rawMoves = when (piece.type) {
+ PieceType.PAWN -> {
+ val dir = if (byColor == PieceColor.WHITE) -1 else 1
+ listOf(Position(r + dir, c - 1), Position(r + dir, c + 1))
+ .filter { it.isValid() }
+ }
+ PieceType.KNIGHT -> getKnightMoves(attackerPos, byColor)
+ PieceType.BISHOP -> getBishopMoves(attackerPos, byColor)
+ PieceType.ROOK -> getRookMoves(attackerPos, byColor)
+ PieceType.QUEEN -> getBishopMoves(attackerPos, byColor) + getRookMoves(attackerPos, byColor)
+ PieceType.KING -> {
+ val kingMoves = mutableListOf()
+ for (dr in -1..1) {
+ for (dc in -1..1) {
+ if (dr == 0 && dc == 0) continue
+ val to = Position(r + dr, c + dc)
+ if (to.isValid()) kingMoves.add(to)
+ }
+ }
+ kingMoves
+ }
+ }
+ if (pos in rawMoves) return true
+ }
+ }
+ return false
+ }
+
+ private fun isInCheck(color: PieceColor): Boolean {
+ val kingPos = findKing(color) ?: return false
+ return isSquareAttacked(kingPos, color.opposite())
+ }
+
+ private fun findKing(color: PieceColor): Position? {
+ for (r in 0..7) {
+ for (c in 0..7) {
+ val piece = board[r][c]
+ if (piece != null && piece.type == PieceType.KING && piece.color == color) {
+ return Position(r, c)
+ }
+ }
+ }
+ return null
+ }
+
+ private fun wouldBeInCheck(from: Position, to: Position, color: PieceColor): Boolean {
+ val captured = board[to.row][to.col]
+ val moving = board[from.row][from.col]
+
+ // Handle en passant capture in simulation
+ var enPassantCaptured: ChessPiece? = null
+ var enPassantPos: Position? = null
+ if (moving?.type == PieceType.PAWN && to.col != from.col && captured == null) {
+ enPassantPos = Position(from.row, to.col)
+ enPassantCaptured = board[from.row][to.col]
+ board[from.row][to.col] = null
+ }
+
+ board[to.row][to.col] = moving
+ board[from.row][from.col] = null
+
+ val inCheck = isInCheck(color)
+
+ board[from.row][from.col] = moving
+ board[to.row][to.col] = captured
+ if (enPassantPos != null) {
+ board[enPassantPos.row][enPassantPos.col] = enPassantCaptured
+ }
+
+ return inCheck
+ }
+
+ private fun checkGameState() {
+ val hasValidMove = (0..7).any { r ->
+ (0..7).any { c ->
+ val piece = board[r][c]
+ piece != null && piece.color == currentTurn && getValidMoves(Position(r, c)).isNotEmpty()
+ }
+ }
+ if (!hasValidMove) {
+ gameOver = true
+ winner = if (isInCheck(currentTurn)) currentTurn.opposite() else null
+ }
+ }
+
+ fun isCurrentPlayerInCheck(): Boolean = isInCheck(currentTurn)
+
+ fun reset() {
+ for (r in 0..7) for (c in 0..7) board[r][c] = null
+ currentTurn = PieceColor.WHITE
+ gameOver = false
+ winner = null
+ lastMove = null
+ whiteKingMoved = false
+ blackKingMoved = false
+ whiteRookAMoved = false
+ whiteRookHMoved = false
+ blackRookAMoved = false
+ blackRookHMoved = false
+ setupBoard()
+ }
+}
diff --git a/chess-game/app/src/main/java/com/devin/chess/MainActivity.kt b/chess-game/app/src/main/java/com/devin/chess/MainActivity.kt
new file mode 100644
index 000000000..a3584315f
--- /dev/null
+++ b/chess-game/app/src/main/java/com/devin/chess/MainActivity.kt
@@ -0,0 +1,65 @@
+package com.devin.chess
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var chessBoard: ChessBoardView
+ private lateinit var statusText: TextView
+ private lateinit var turnIndicator: TextView
+ private lateinit var newGameButton: Button
+
+ private val chessModel = ChessModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ chessBoard = findViewById(R.id.chessBoard)
+ statusText = findViewById(R.id.statusText)
+ turnIndicator = findViewById(R.id.turnIndicator)
+ newGameButton = findViewById(R.id.newGameButton)
+
+ chessBoard.model = chessModel
+ chessBoard.onMoveListener = { updateStatus() }
+
+ newGameButton.setOnClickListener {
+ chessModel.reset()
+ chessBoard.clearSelection()
+ updateStatus()
+ }
+
+ updateStatus()
+ }
+
+ private fun updateStatus() {
+ if (chessModel.gameOver) {
+ val message = when (chessModel.winner) {
+ PieceColor.WHITE -> "White wins by checkmate!"
+ PieceColor.BLACK -> "Black wins by checkmate!"
+ null -> "Stalemate — it's a draw!"
+ }
+ statusText.text = message
+ turnIndicator.text = "Game Over"
+
+ AlertDialog.Builder(this)
+ .setTitle("Game Over")
+ .setMessage(message)
+ .setPositiveButton("New Game") { _, _ ->
+ chessModel.reset()
+ chessBoard.clearSelection()
+ updateStatus()
+ }
+ .setNegativeButton("OK", null)
+ .show()
+ } else {
+ val turnStr = if (chessModel.currentTurn == PieceColor.WHITE) "White" else "Black"
+ turnIndicator.text = "$turnStr's Turn"
+ statusText.text = if (chessModel.isCurrentPlayerInCheck()) "Check!" else ""
+ }
+ }
+}
diff --git a/chess-game/app/src/main/res/drawable/ic_launcher_foreground.xml b/chess-game/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..675119048
--- /dev/null
+++ b/chess-game/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/chess-game/app/src/main/res/layout/activity_main.xml b/chess-game/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..12b759cff
--- /dev/null
+++ b/chess-game/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chess-game/app/src/main/res/mipmap-hdpi/ic_launcher.xml b/chess-game/app/src/main/res/mipmap-hdpi/ic_launcher.xml
new file mode 100644
index 000000000..0d57b7072
--- /dev/null
+++ b/chess-game/app/src/main/res/mipmap-hdpi/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/chess-game/app/src/main/res/values/colors.xml b/chess-game/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..10a810f1f
--- /dev/null
+++ b/chess-game/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #F0D9B5
+ #B58863
+ #2C2C2C
+
diff --git a/chess-game/app/src/main/res/values/strings.xml b/chess-game/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..082f8073c
--- /dev/null
+++ b/chess-game/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Chess
+
diff --git a/chess-game/app/src/main/res/values/themes.xml b/chess-game/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..88003c7f5
--- /dev/null
+++ b/chess-game/app/src/main/res/values/themes.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/chess-game/build.gradle b/chess-game/build.gradle
new file mode 100644
index 000000000..ddbca11a1
--- /dev/null
+++ b/chess-game/build.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'com.android.application' version '8.2.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
+}
diff --git a/chess-game/gradle.properties b/chess-game/gradle.properties
new file mode 100644
index 000000000..f0a2e55f8
--- /dev/null
+++ b/chess-game/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+android.nonTransitiveRClass=true
diff --git a/chess-game/gradle/wrapper/gradle-wrapper.jar b/chess-game/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..033e24c4c
Binary files /dev/null and b/chess-game/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/chess-game/gradle/wrapper/gradle-wrapper.properties b/chess-game/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..62f495dfe
--- /dev/null
+++ b/chess-game/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/chess-game/gradlew b/chess-game/gradlew
new file mode 100755
index 000000000..e0c44dce7
--- /dev/null
+++ b/chess-game/gradlew
@@ -0,0 +1,118 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+app_path=$0
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld -- "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NonStop* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ ;;
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ ;;
+ esac
+fi
+
+# Collect all arguments for the java command, stracks://am so 1.8 upgrade paths work
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$@"
+
+exec "$JAVACMD" "$@"
diff --git a/chess-game/settings.gradle b/chess-game/settings.gradle
new file mode 100644
index 000000000..1ab8fca00
--- /dev/null
+++ b/chess-game/settings.gradle
@@ -0,0 +1,17 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "ChessGame"
+include ':app'