Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Last active April 30, 2026 11:48
Show Gist options
  • Select an option

  • Save Kyriakos-Georgiopoulos/8e7ac7eda4c43f719d9dcc2fdb4176fa to your computer and use it in GitHub Desktop.

Select an option

Save Kyriakos-Georgiopoulos/8e7ac7eda4c43f719d9dcc2fdb4176fa to your computer and use it in GitHub Desktop.
/*
* Copyright 2026 Kyriakos Georgiopoulos
*
* 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.
*/
import android.app.Activity
import android.content.Context
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.EaseIn
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.EaseOutBack
import androidx.compose.animation.core.EaseOutCubic
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import com.zengrip.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.math.abs
import kotlin.math.max
import kotlin.random.Random
// --- DESIGN SYSTEM: CYBER MAGENTA GRADIENT ---
val CyberMagentaGradient = Brush.linearGradient(
colors = listOf(Color(0xFF11998E), Color(0xFFFF00FF))
)
val MagentaAccent = Color(0xFFFF00FF)
val TealAccent = Color(0xFF11998E)
val TextMuted = Color(0xFFA0AAB2)
val SurfaceDark = Color(0xFF1A1B22)
val SurfaceVariant = Color(0xFF2C2D35)
// --- 1. DATA MODELS ---
enum class PieceColor { WHITE, BLACK }
enum class PieceType { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
enum class GameStatus { PLAYING, CHECKMATE, STALEMATE, TIMEOUT_WHITE, TIMEOUT_BLACK, PUZZLE_SOLVED }
enum class ChessScreen { SETTINGS, GAME }
@Immutable
data class Piece(val type: PieceType, val color: PieceColor, val hasMoved: Boolean = false) {
val symbol: String
get() = when (type) {
PieceType.KING -> "♚"
PieceType.QUEEN -> "♛"
PieceType.ROOK -> "♜"
PieceType.BISHOP -> "♝"
PieceType.KNIGHT -> "♞"
PieceType.PAWN -> "♟"
}
}
@Immutable
data class Position(val row: Int, val col: Int) {
val index: Int get() = row * 8 + col
}
val POSITIONS = Array(64) { i -> Position(i / 8, i % 8) }
fun getPos(row: Int, col: Int): Position = POSITIONS[row * 8 + col]
@Immutable
data class Move(val from: Position, val to: Position)
@Immutable
data class ImmutableBoard(val squares: List<Piece?>)
data class BoardTheme(val name: String, val light: Color, val dark: Color)
val AppThemes = listOf(
BoardTheme("Wood", Color(0xFFE6D3B3), Color(0xFFB08C6A)),
BoardTheme("Marble", Color(0xFFE8EDF9), Color(0xFFB7C0D8)),
BoardTheme("Green", Color(0xFFE8EDD8), Color(0xFF7B9A6B)),
BoardTheme("Midnight", Color(0xFF9D84B7), Color(0xFF523A78)),
BoardTheme("Coral", Color(0xFFFFCCB6), Color(0xFFF38181)),
BoardTheme("Mono", Color(0xFFE0E0E0), Color(0xFF616161))
)
data class ChessPuzzle(
val name: String,
val context: String,
val fen: String,
val playerTurn: PieceColor,
val era: String = "",
val year: Int = 0,
val white: String = "",
val black: String = "",
val site: String = "",
val result: String = "",
val eco: String = "",
val opening: String = "",
val sourceBook: String = ""
)
data class GameSettings(
val theme: BoardTheme = AppThemes[0],
val difficultyDepth: Int = 3,
val timersEnabled: Boolean = true,
val timerMinutes: Int = 10,
val puzzleModeEnabled: Boolean = false,
val highlightLastMove: Boolean = true
)
// --- 2. RAW JSON LOADER ---
@Serializable
private data class RawBattle(
val id: String = "",
val name: String = "",
val white: String = "",
val black: String = "",
val event: String = "",
val site: String = "",
val date: String = "",
val year: Int = 0,
val era: String = "",
val result: String = "",
val eco: String = "",
val opening: String = "",
val variation: String = "",
@SerialName("sourceBook") val sourceBook: String = "",
val pgn: String = ""
)
private val lenientJson = Json { ignoreUnknownKeys = true }
object HistoricalBattleLoader {
fun load(context: Context): List<ChessPuzzle> {
val json = context.resources.openRawResource(R.raw.historical_battles)
.bufferedReader()
.use { it.readText() }
val battles = lenientJson.decodeFromString<List<RawBattle>>(json)
return battles.mapNotNull { battle ->
if (battle.name.isBlank() || battle.pgn.isBlank()) return@mapNotNull null
val fen = PgnToFen.puzzleFenFromPgn(battle.pgn) ?: return@mapNotNull null
val playerTurn = PgnToFen.sideToMoveFromFen(fen)
ChessPuzzle(
name = battle.name,
context = battle.event.ifBlank { "Historical battle." },
fen = fen,
playerTurn = playerTurn,
era = battle.era,
year = battle.year,
white = battle.white,
black = battle.black,
site = battle.site,
result = battle.result,
eco = battle.eco,
opening = battle.opening,
sourceBook = battle.sourceBook
)
}
}
}
/**
* Lightweight PGN replay engine that converts a PGN move-text string into a FEN.
*
* Replays the full game on an 8×8 board array, then rewinds a few plies
* to produce a puzzle position where the winning side has a forced continuation.
*/
object PgnToFen {
private val MOVE_REGEX = Regex(
"""(?:\d+\.+\s*)?([KQRBN]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])(=[QRBN])?[+#]?|O-O-O[+#]?|O-O[+#]?"""
)
private val COMMENT_REGEX = Regex("\\{[^\\}]*\\}")
private val VARIATION_REGEX = Regex("\\([^\\)]*\\)")
private val RESULT_REGEX = Regex("(1-0|0-1|1/2-1/2|\\*)$")
/**
* Produces a puzzle FEN by replaying the PGN and rewinding [rewindPlies] half-moves
* from the end, so the player is presented with a winning continuation.
*
* Uses a circular buffer of size [rewindPlies]+1 to avoid storing every
* intermediate FEN — only the most recent positions are kept in memory.
*
* @return a valid FEN string or `null` if the PGN cannot be parsed.
*/
fun puzzleFenFromPgn(pgn: String, rewindPlies: Int = 4): String? {
val board = initialBoard()
var whiteToMove = true
val castleRights = booleanArrayOf(true, true, true, true) // KQkq
val cleanPgn = pgn
.replace(COMMENT_REGEX, "")
.replace(VARIATION_REGEX, "")
.replace(RESULT_REGEX, "")
.trim()
val tokens = MOVE_REGEX.findAll(cleanPgn).map { it.value.trim() }.toList()
if (tokens.isEmpty()) return null
val bufSize = rewindPlies + 1
val ring = arrayOfNulls<String>(bufSize)
var totalMoves = 0
for (token in tokens) {
val success = applyMoveToken(board, token, whiteToMove, castleRights)
if (!success) break
whiteToMove = !whiteToMove
ring[totalMoves % bufSize] = toFen(board, whiteToMove, castleRights)
totalMoves++
}
if (totalMoves == 0) return null
val targetPly = (totalMoves - rewindPlies).coerceIn(0, totalMoves - 1)
return ring[targetPly % bufSize]
}
fun sideToMoveFromFen(fen: String): PieceColor {
val parts = fen.split(" ")
return if (parts.size > 1 && parts[1] == "b") PieceColor.BLACK else PieceColor.WHITE
}
// ---- internal replay helpers ----
private fun initialBoard(): CharArray {
val b = CharArray(64) { '.' }
val backRank = "rnbqkbnr"
for (c in 0..7) {
b[c] = backRank[c] // row 0 = rank 8 (black)
b[8 + c] = 'p' // row 1 = rank 7
b[48 + c] = 'P' // row 6 = rank 2
b[56 + c] = backRank[c].uppercaseChar() // row 7 = rank 1 (white)
}
return b
}
/**
* Applies a single SAN token (e.g. "Nf3", "exd5", "O-O") to the board.
*
* Resolves disambiguation by scanning all candidate source squares for
* pieces of the matching type that can legally reach the target square.
*
* @return `true` if the move was applied successfully.
*/
private fun applyMoveToken(
board: CharArray,
token: String,
whiteToMove: Boolean,
castleRights: BooleanArray
): Boolean {
val clean = token.replace("+", "").replace("#", "")
// Castling
if (clean == "O-O-O" || clean == "O-O") {
val row = if (whiteToMove) 7 else 0
val kingCol = 4
val isQueenside = clean == "O-O-O"
val rookCol = if (isQueenside) 0 else 7
val kingDest = if (isQueenside) 2 else 6
val rookDest = if (isQueenside) 3 else 5
board[row * 8 + kingDest] = board[row * 8 + kingCol]
board[row * 8 + kingCol] = '.'
board[row * 8 + rookDest] = board[row * 8 + rookCol]
board[row * 8 + rookCol] = '.'
updateCastleRights(castleRights, row, kingCol)
return true
}
val m = MOVE_REGEX.matchEntire(clean) ?: return false
val pieceChar = m.groupValues[1].ifEmpty { "" }
val disambigFile = m.groupValues[2].firstOrNull()
val disambigRank = m.groupValues[3].firstOrNull()
val destFile = m.groupValues[5].firstOrNull() ?: return false
val destRank = m.groupValues[6].firstOrNull() ?: return false
val promotion = m.groupValues[7].removePrefix("=").firstOrNull()
val destCol = destFile - 'a'
val destRow = 8 - (destRank - '0')
val searchPiece = when {
pieceChar.isEmpty() -> if (whiteToMove) 'P' else 'p'
whiteToMove -> pieceChar[0]
else -> pieceChar[0].lowercaseChar()
}
// Find the source square
var srcRow = -1
var srcCol = -1
for (r in 0..7) {
for (c in 0..7) {
if (board[r * 8 + c] != searchPiece) continue
if (disambigFile != null && c != disambigFile - 'a') continue
if (disambigRank != null && r != 8 - (disambigRank - '0')) continue
if (canReach(board, searchPiece, r, c, destRow, destCol)) {
srcRow = r; srcCol = c
}
}
}
if (srcRow == -1) return false
board[destRow * 8 + destCol] = if (promotion != null) {
if (whiteToMove) promotion.uppercaseChar() else promotion.lowercaseChar()
} else {
searchPiece
}
board[srcRow * 8 + srcCol] = '.'
// En-passant capture
if (searchPiece.lowercaseChar() == 'p' && srcCol != destCol && board[destRow * 8 + destCol] == searchPiece) {
board[srcRow * 8 + destCol] = '.'
}
updateCastleRights(castleRights, srcRow, srcCol)
updateCastleRights(castleRights, destRow, destCol)
return true
}
/**
* Tests whether [piece] at ([sr],[sc]) can reach ([dr],[dc]) by its movement rules.
* Does not verify check-legality — only geometric reachability and path clearance.
*/
private fun canReach(
board: CharArray, piece: Char,
sr: Int, sc: Int, dr: Int, dc: Int
): Boolean {
val dRow = dr - sr
val dCol = dc - sc
return when (piece.lowercaseChar()) {
'p' -> {
val dir = if (piece.isUpperCase()) -1 else 1
val startRow = if (piece.isUpperCase()) 6 else 1
when {
dCol == 0 && dRow == dir && board[dr * 8 + dc] == '.' -> true
dCol == 0 && dRow == dir * 2 && sr == startRow
&& board[(sr + dir) * 8 + sc] == '.'
&& board[dr * 8 + dc] == '.' -> true
abs(dCol) == 1 && dRow == dir && board[dr * 8 + dc] != '.' -> true
abs(dCol) == 1 && dRow == dir -> true // en-passant
else -> false
}
}
'n' -> (abs(dRow) == 2 && abs(dCol) == 1) || (abs(dRow) == 1 && abs(dCol) == 2)
'b' -> abs(dRow) == abs(dCol) && dRow != 0 && isPathClear(board, sr, sc, dr, dc)
'r' -> (dRow == 0 || dCol == 0) && (dRow != 0 || dCol != 0) && isPathClear(
board,
sr,
sc,
dr,
dc
)
'q' -> ((abs(dRow) == abs(dCol) && dRow != 0) || (dRow == 0 || dCol == 0) && (dRow != 0 || dCol != 0)) && isPathClear(
board,
sr,
sc,
dr,
dc
)
'k' -> abs(dRow) <= 1 && abs(dCol) <= 1 && (dRow != 0 || dCol != 0)
else -> false
}
}
private fun isPathClear(board: CharArray, sr: Int, sc: Int, dr: Int, dc: Int): Boolean {
val stepR = (dr - sr).coerceIn(-1, 1)
val stepC = (dc - sc).coerceIn(-1, 1)
var r = sr + stepR
var c = sc + stepC
while (r != dr || c != dc) {
if (board[r * 8 + c] != '.') return false
r += stepR
c += stepC
}
return true
}
private fun updateCastleRights(rights: BooleanArray, row: Int, col: Int) {
if (row == 7 && col == 4) {
rights[0] = false; rights[1] = false
} // White king
if (row == 0 && col == 4) {
rights[2] = false; rights[3] = false
} // Black king
if (row == 7 && col == 7) rights[0] = false // White K-side rook
if (row == 7 && col == 0) rights[1] = false // White Q-side rook
if (row == 0 && col == 7) rights[2] = false // Black K-side rook
if (row == 0 && col == 0) rights[3] = false // Black Q-side rook
}
private fun toFen(board: CharArray, whiteToMove: Boolean, castleRights: BooleanArray): String {
val sb = StringBuilder()
for (r in 0..7) {
var empty = 0
for (c in 0..7) {
val ch = board[r * 8 + c]
if (ch == '.') {
empty++
} else {
if (empty > 0) {
sb.append(empty); empty = 0
}
sb.append(ch)
}
}
if (empty > 0) sb.append(empty)
if (r < 7) sb.append('/')
}
sb.append(if (whiteToMove) " w " else " b ")
val castle = buildString {
if (castleRights[0]) append('K')
if (castleRights[1]) append('Q')
if (castleRights[2]) append('k')
if (castleRights[3]) append('q')
}
sb.append(castle.ifEmpty { "-" })
sb.append(" - 0 1")
return sb.toString()
}
}
// --- 3. MINIMAX AI ENGINE ---
/**
* Chess AI engine using minimax with alpha-beta pruning, piece-square tables
* for positional awareness, and quiescence search to avoid the horizon effect.
*
* ### Evaluation
* Each piece has a base material value (centipawns) plus a positional bonus
* read from piece-square tables (PST). The PST values encourage pieces to
* occupy strong squares (e.g. knights in the centre, pawns advancing).
*
* ### Search
* - **Alpha-beta pruning** with move ordering at every node (not just root).
* - **Quiescence search** at leaf nodes extends the search along capture
* chains so the engine does not mis-evaluate tactical positions.
* - **Mate-distance scoring** — checkmates closer to the root score higher,
* so the engine prefers the fastest mate and avoids delaying.
*/
class ChessAI(private val state: ChessBoardState) {
companion object {
private const val MATE_SCORE = 100_000
private const val PAWN_VALUE = 100
private const val KNIGHT_VALUE = 320
private const val BISHOP_VALUE = 330
private const val ROOK_VALUE = 500
private const val QUEEN_VALUE = 900
private const val KING_VALUE = 20_000
// Piece-square tables (from White's perspective, index 0 = a8)
// Values are centipawn bonuses; mirrored vertically for Black.
private val PAWN_PST = intArrayOf(
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 35, 35, 0, 0, 0,
5, -5, -10, 0, 0, -10, -5, 5,
5, 10, 10, -20, -20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0
)
private val KNIGHT_PST = intArrayOf(
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 0, 0, 0, -20, -40,
-30, 0, 10, 15, 15, 10, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 10, 15, 15, 10, 5, -30,
-40, -20, 0, 5, 5, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50
)
private val BISHOP_PST = intArrayOf(
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 5, 10, 10, 5, 0, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 5, 0, 0, 0, 0, 5, -10,
-20, -10, -10, -10, -10, -10, -10, -20
)
private val ROOK_PST = intArrayOf(
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0
)
private val QUEEN_PST = intArrayOf(
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 5, 5, 5, 0, -10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0, -10,
-10, 0, 5, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20
)
private val KING_MID_PST = intArrayOf(
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20
)
}
private val pieceValues = IntArray(6).apply {
this[PieceType.PAWN.ordinal] = PAWN_VALUE
this[PieceType.KNIGHT.ordinal] = KNIGHT_VALUE
this[PieceType.BISHOP.ordinal] = BISHOP_VALUE
this[PieceType.ROOK.ordinal] = ROOK_VALUE
this[PieceType.QUEEN.ordinal] = QUEEN_VALUE
this[PieceType.KING.ordinal] = KING_VALUE
}
private val pstByType = arrayOf(
PAWN_PST, KNIGHT_PST, BISHOP_PST, ROOK_PST, QUEEN_PST, KING_MID_PST
)
/**
* Evaluates the board using material balance and piece-square tables.
*
* For each piece the score is `materialValue + positionalBonus`.
* The PST index is mirrored for Black pieces so both sides benefit
* from the same positional heuristics.
*
* Additional bonuses:
* - **Bishop pair**: +30 cp for having both bishops (strong in open positions).
*
* Positive scores favour White, negative favour Black.
*/
private fun evaluateBoard(board: Array<Piece?>): Int {
var score = 0
var whiteBishops = 0
var blackBishops = 0
for (i in 0..63) {
val piece = board[i] ?: continue
val material = pieceValues[piece.type.ordinal]
val pst = pstByType[piece.type.ordinal]
// For White, index is as-is. For Black, mirror vertically.
val pstIndex =
if (piece.color == PieceColor.WHITE) i else (56 - (i / 8) * 8 + i % 8).let { (7 - i / 8) * 8 + i % 8 }
val positional = pst[pstIndex]
if (piece.type == PieceType.BISHOP) {
if (piece.color == PieceColor.WHITE) whiteBishops++ else blackBishops++
}
score += if (piece.color == PieceColor.WHITE) {
material + positional
} else {
-(material + positional)
}
}
// Bishop pair bonus
if (whiteBishops >= 2) score += 30
if (blackBishops >= 2) score -= 30
return score
}
/**
* Finds the best move for [aiColor] using minimax with alpha-beta pruning.
*
* Moves are ordered heuristically at the root: captures by MVV-LVA
* (Most Valuable Victim – Least Valuable Attacker), then non-captures
* that give check, then remaining moves.
*/
suspend fun calculateBestMove(board: Array<Piece?>, aiColor: PieceColor, depth: Int): Move? =
withContext(Dispatchers.Default) {
val pieceCount = board.count { it != null }
if (pieceCount == 32 && aiColor == PieceColor.WHITE) {
val isE4 = Random.nextBoolean()
return@withContext if (isE4) {
Move(getPos(6, 4), getPos(4, 4))
} else {
Move(getPos(6, 3), getPos(4, 3))
}
}
if (pieceCount == 32 && aiColor == PieceColor.BLACK) {
return@withContext Move(getPos(1, 4), getPos(3, 4))
}
val isMaximizing = aiColor == PieceColor.WHITE
var bestMove: Move? = null
var bestValue = if (isMaximizing) Int.MIN_VALUE else Int.MAX_VALUE
val orderedMoves = getOrderedLegalMoves(board, aiColor)
for (move in orderedMoves) {
val simulatedBoard = applyMove(board, move)
val boardValue = minimax(
simulatedBoard, depth - 1, Int.MIN_VALUE, Int.MAX_VALUE,
!isMaximizing, depth
)
if (isMaximizing) {
if (boardValue > bestValue) {
bestValue = boardValue; bestMove = move
}
} else {
if (boardValue < bestValue) {
bestValue = boardValue; bestMove = move
}
}
}
bestMove
}
/**
* Minimax search with alpha-beta pruning and quiescence extension.
*
* At every interior node moves are ordered heuristically so the most
* promising branches are searched first, dramatically improving pruning.
*
* When depth reaches 0 the search continues into [quiescence] — a
* capture-only search that resolves tactical exchanges before returning
* a static evaluation.
*
* Checkmate is scored relative to [maxDepth] so the engine prefers
* shorter mating sequences.
*
* @param alpha the best score the maximiser can guarantee so far
* @param beta the best score the minimiser can guarantee so far
* @param maxDepth the original search depth (for mate-distance scoring)
*/
private fun minimax(
board: Array<Piece?>,
depth: Int,
alpha: Int,
beta: Int,
isMaximizingPlayer: Boolean,
maxDepth: Int
): Int {
if (depth == 0) return quiescence(board, alpha, beta, isMaximizingPlayer)
val currentColor = if (isMaximizingPlayer) PieceColor.WHITE else PieceColor.BLACK
val moves = getOrderedLegalMoves(board, currentColor)
if (moves.isEmpty()) {
return if (state.isKingInCheck(currentColor, board)) {
// Mate-distance: prefer faster checkmates
val distFromRoot = maxDepth - depth
if (isMaximizingPlayer) -(MATE_SCORE - distFromRoot) else (MATE_SCORE - distFromRoot)
} else {
0 // stalemate
}
}
var a = alpha
var b = beta
return if (isMaximizingPlayer) {
var maxEval = Int.MIN_VALUE
for (move in moves) {
val eval = minimax(applyMove(board, move), depth - 1, a, b, false, maxDepth)
if (eval > maxEval) maxEval = eval
if (eval > a) a = eval
if (b <= a) break
}
maxEval
} else {
var minEval = Int.MAX_VALUE
for (move in moves) {
val eval = minimax(applyMove(board, move), depth - 1, a, b, true, maxDepth)
if (eval < minEval) minEval = eval
if (eval < b) b = eval
if (b <= a) break
}
minEval
}
}
/**
* Quiescence search — extends the search along **capture chains** so the
* engine does not stop evaluating in the middle of a piece exchange.
*
* At each node:
* 1. Compute the **stand-pat** (static evaluation). If it already causes a
* cutoff, return immediately.
* 2. Generate only capture moves and search them recursively.
*
* This eliminates the *horizon effect* where the engine pushes losing
* captures just beyond the search horizon, producing blunders.
*
* Capped at 6 extra plies to keep search time bounded.
*/
private fun quiescence(
board: Array<Piece?>,
alpha: Int,
beta: Int,
isMaximizing: Boolean,
qDepth: Int = 0
): Int {
val standPat = evaluateBoard(board)
if (qDepth >= 6) return standPat
if (isMaximizing) {
if (standPat >= beta) return beta
var a = if (standPat > alpha) standPat else alpha
val color = PieceColor.WHITE
val captures = getOrderedCaptures(board, color)
for (move in captures) {
val eval = quiescence(applyMove(board, move), a, beta, false, qDepth + 1)
if (eval > a) a = eval
if (a >= beta) return beta
}
return a
} else {
if (standPat <= alpha) return alpha
var b = if (standPat < beta) standPat else beta
val color = PieceColor.BLACK
val captures = getOrderedCaptures(board, color)
for (move in captures) {
val eval = quiescence(applyMove(board, move), alpha, b, true, qDepth + 1)
if (eval < b) b = eval
if (b <= alpha) return alpha
}
return b
}
}
/**
* Returns all legal moves ordered by heuristic priority:
* 1. Captures sorted by MVV-LVA (Most Valuable Victim – Least Valuable Attacker)
* 2. Non-captures that deliver check
* 3. All other quiet moves
*/
private fun getOrderedLegalMoves(board: Array<Piece?>, color: PieceColor): List<Move> {
val captures = ArrayList<Move>(16)
val checks = ArrayList<Move>(8)
val quiet = ArrayList<Move>(32)
for (i in 0..63) {
val piece = board[i] ?: continue
if (piece.color != color) continue
val pos = POSITIONS[i]
val targets = state.calculateStrictLegalMoves(piece, pos, board)
for (target in targets) {
val move = Move(pos, target)
if (board[target.index] != null) {
captures.add(move)
} else {
val sim = applyMove(board, move)
val enemy =
if (color == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE
if (state.isKingInCheck(enemy, sim)) checks.add(move) else quiet.add(move)
}
}
}
// MVV-LVA: sort captures by victim value desc, then attacker value asc
captures.sortWith(Comparator { m1, m2 ->
val v1 = pieceValues[board[m1.to.index]!!.type.ordinal] -
pieceValues[board[m1.from.index]!!.type.ordinal]
val v2 = pieceValues[board[m2.to.index]!!.type.ordinal] -
pieceValues[board[m2.from.index]!!.type.ordinal]
v2.compareTo(v1)
})
val result = ArrayList<Move>(captures.size + checks.size + quiet.size)
result.addAll(captures)
result.addAll(checks)
result.addAll(quiet)
return result
}
/**
* Returns only legal capture moves, ordered by MVV-LVA.
* Used by [quiescence] to extend the search along tactical lines.
*/
private fun getOrderedCaptures(board: Array<Piece?>, color: PieceColor): List<Move> {
val captures = ArrayList<Move>(16)
for (i in 0..63) {
val piece = board[i] ?: continue
if (piece.color != color) continue
val pos = POSITIONS[i]
val targets = state.calculateStrictLegalMoves(piece, pos, board)
for (target in targets) {
if (board[target.index] != null) captures.add(Move(pos, target))
}
}
captures.sortWith(Comparator { m1, m2 ->
val v1 = pieceValues[board[m1.to.index]!!.type.ordinal] -
pieceValues[board[m1.from.index]!!.type.ordinal]
val v2 = pieceValues[board[m2.to.index]!!.type.ordinal] -
pieceValues[board[m2.from.index]!!.type.ordinal]
v2.compareTo(v1)
})
return captures
}
private fun applyMove(board: Array<Piece?>, move: Move): Array<Piece?> {
val newBoard = board.copyOf()
val piece = newBoard[move.from.index] ?: return newBoard
if (piece.type == PieceType.KING && abs(move.from.col - move.to.col) == 2) {
val isKingside = move.to.col > move.from.col
val rookCol = if (isKingside) 7 else 0
val rookDestCol = if (isKingside) 5 else 3
val rFrom = getPos(move.from.row, rookCol).index
val rTo = getPos(move.from.row, rookDestCol).index
val rook = newBoard[rFrom]
if (rook != null) {
newBoard[rFrom] = null
newBoard[rTo] = rook.copy(hasMoved = true)
}
}
val isPromotion = piece.type == PieceType.PAWN && (move.to.row == 0 || move.to.row == 7)
newBoard[move.from.index] = null
newBoard[move.to.index] = if (isPromotion) {
piece.copy(type = PieceType.QUEEN, hasMoved = true)
} else {
piece.copy(hasMoved = true)
}
return newBoard
}
}
// --- 4. DUAL-STATE HOLDER ---
class ChessBoardState {
var internalBoard = createInitialBoard()
private set
var uiBoard by mutableStateOf(ImmutableBoard(internalBoard.toList()))
private set
var selectedPosition by mutableStateOf<Position?>(null)
private set
var validMoves by mutableStateOf<Set<Position>>(emptySet())
private set
var currentTurn by mutableStateOf<PieceColor?>(null)
private set
var checkedKingColor by mutableStateOf<PieceColor?>(null)
private set
var gameStatus by mutableStateOf(GameStatus.PLAYING)
private set
var lastMove by mutableStateOf<Move?>(null)
private set
var whiteTimeMillis by mutableLongStateOf(10 * 60 * 1000L)
private set
var blackTimeMillis by mutableLongStateOf(10 * 60 * 1000L)
private set
var activePuzzle: ChessPuzzle? = null
private set
var isDecidingTurn by mutableStateOf(false)
private set
fun startGame(timerMinutes: Int, puzzle: ChessPuzzle?) {
activePuzzle = puzzle
if (puzzle != null) {
loadFen(puzzle.fen)
currentTurn = puzzle.playerTurn
isDecidingTurn = false
} else {
internalBoard = createInitialBoard()
uiBoard = ImmutableBoard(internalBoard.toList())
currentTurn = null
isDecidingTurn = true
}
checkedKingColor = null
gameStatus = GameStatus.PLAYING
lastMove = null
val totalMillis = timerMinutes * 60 * 1000L
whiteTimeMillis = totalMillis
blackTimeMillis = totalMillis
clearSelection()
}
fun finishCoinToss(startingColor: PieceColor) {
currentTurn = startingColor
isDecidingTurn = false
}
fun prepareNewGame(timerMinutes: Int) {
startGame(timerMinutes, activePuzzle)
}
private fun loadFen(fen: String) {
val arr = arrayOfNulls<Piece>(64)
val rows = fen.split(" ")[0].split("/")
for (r in 0..7) {
var c = 0
for (char in rows[r]) {
if (char.isDigit()) {
c += char.digitToInt()
} else {
val color = if (char.isUpperCase()) PieceColor.WHITE else PieceColor.BLACK
val type = when (char.lowercaseChar()) {
'p' -> PieceType.PAWN
'n' -> PieceType.KNIGHT
'b' -> PieceType.BISHOP
'r' -> PieceType.ROOK
'q' -> PieceType.QUEEN
'k' -> PieceType.KING
else -> PieceType.PAWN
}
if (c < 8) {
arr[getPos(r, c).index] = Piece(type, color)
c++
}
}
}
}
internalBoard = arr
uiBoard = ImmutableBoard(arr.toList())
}
fun tickTime(amountMillis: Long) {
if (currentTurn == PieceColor.WHITE) {
whiteTimeMillis = max(0L, whiteTimeMillis - amountMillis)
if (whiteTimeMillis <= 0L) gameStatus = GameStatus.TIMEOUT_WHITE
} else if (currentTurn == PieceColor.BLACK) {
blackTimeMillis = max(0L, blackTimeMillis - amountMillis)
if (blackTimeMillis <= 0L) gameStatus = GameStatus.TIMEOUT_BLACK
}
}
fun executeMove(move: Move) {
val pieceToMove = internalBoard[move.from.index] ?: return
val newBoard = internalBoard.copyOf()
if (pieceToMove.type == PieceType.KING && abs(move.from.col - move.to.col) == 2) {
val isKingside = move.to.col > move.from.col
val rookCol = if (isKingside) 7 else 0
val rookDestCol = if (isKingside) 5 else 3
val rFrom = getPos(move.from.row, rookCol).index
val rTo = getPos(move.from.row, rookDestCol).index
val rook = newBoard[rFrom]
if (rook != null) {
newBoard[rFrom] = null
newBoard[rTo] = rook.copy(hasMoved = true)
}
}
val isPromotion =
pieceToMove.type == PieceType.PAWN && (move.to.row == 0 || move.to.row == 7)
newBoard[move.from.index] = null
newBoard[move.to.index] = if (isPromotion) {
pieceToMove.copy(type = PieceType.QUEEN, hasMoved = true)
} else {
pieceToMove.copy(hasMoved = true)
}
internalBoard = newBoard
uiBoard = ImmutableBoard(internalBoard.toList())
lastMove = move
finalizeTurn()
}
fun onSquareClicked(position: Position) {
if (currentTurn == null || gameStatus != GameStatus.PLAYING || isDecidingTurn) return
val currentlySelected = selectedPosition
val clickedPiece = internalBoard[position.index]
if (currentlySelected == null) {
if (clickedPiece != null && clickedPiece.color == currentTurn) {
selectedPosition = position
validMoves = calculateStrictLegalMoves(clickedPiece, position).toSet()
}
} else {
when {
currentlySelected == position -> clearSelection()
clickedPiece != null && clickedPiece.color == currentTurn -> {
selectedPosition = position
validMoves = calculateStrictLegalMoves(clickedPiece, position).toSet()
}
position in validMoves -> {
executeMove(Move(currentlySelected, position))
clearSelection()
}
else -> clearSelection()
}
}
}
private fun finalizeTurn() {
val nextTurn = if (currentTurn == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE
currentTurn = nextTurn
val isCheck = isKingInCheck(nextTurn, internalBoard)
checkedKingColor = if (isCheck) nextTurn else null
if (!playerHasAnyLegalMoves(nextTurn)) {
gameStatus = if (activePuzzle != null && isCheck) {
GameStatus.PUZZLE_SOLVED
} else if (isCheck) {
GameStatus.CHECKMATE
} else {
GameStatus.STALEMATE
}
}
}
private fun clearSelection() {
selectedPosition = null
validMoves = emptySet()
}
private fun playerHasAnyLegalMoves(color: PieceColor): Boolean {
for (i in 0..63) {
val piece = internalBoard[i] ?: continue
if (piece.color == color && calculateStrictLegalMoves(
piece,
POSITIONS[i]
).isNotEmpty()
) {
return true
}
}
return false
}
/**
* Filters pseudo-legal moves to only those that do not leave the king in check.
*
* For castling moves, additionally verifies that the king does not pass
* through an attacked square.
*/
fun calculateStrictLegalMoves(
piece: Piece,
pos: Position,
currentBoard: Array<Piece?> = internalBoard
): List<Position> {
val pseudoMoves = getPseudoLegalMoves(piece, pos, currentBoard)
val strictMoves = ArrayList<Position>(pseudoMoves.size)
for (targetPos in pseudoMoves) {
if (piece.type == PieceType.KING && abs(pos.col - targetPos.col) == 2) {
if (isKingInCheck(piece.color, currentBoard)) continue
val direction = if (targetPos.col > pos.col) 1 else -1
val passBoard = currentBoard.copyOf()
passBoard[pos.index] = null
passBoard[getPos(pos.row, pos.col + direction).index] = piece
if (isKingInCheck(piece.color, passBoard)) continue
}
val simulatedBoard = currentBoard.copyOf()
simulatedBoard[pos.index] = null
simulatedBoard[targetPos.index] = piece
if (!isKingInCheck(piece.color, simulatedBoard)) {
strictMoves.add(targetPos)
}
}
return strictMoves
}
/**
* Determines whether the king of [color] is under attack on [currentBoard].
*
* Scans outward from the king's position along all attack vectors (knight jumps,
* sliding rays, pawn captures) rather than iterating every enemy piece, giving
* O(1)-bounded work per direction instead of O(n) pseudo-legal generation.
*/
fun isKingInCheck(color: PieceColor, currentBoard: Array<Piece?>): Boolean {
var kingRow = -1
var kingCol = -1
for (i in 0..63) {
val p = currentBoard[i]
if (p != null && p.type == PieceType.KING && p.color == color) {
kingRow = i / 8
kingCol = i % 8
break
}
}
if (kingRow == -1) return false
val enemy = if (color == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE
// Knight attacks
val knightOffsets = intArrayOf(-2, -1, -2, 1, -1, -2, -1, 2, 1, -2, 1, 2, 2, -1, 2, 1)
for (k in knightOffsets.indices step 2) {
val r = kingRow + knightOffsets[k]
val c = kingCol + knightOffsets[k + 1]
if (r in 0..7 && c in 0..7) {
val p = currentBoard[r * 8 + c]
if (p != null && p.color == enemy && p.type == PieceType.KNIGHT) return true
}
}
// Pawn attacks
val pawnDir = if (color == PieceColor.WHITE) -1 else 1
for (dc in intArrayOf(-1, 1)) {
val r = kingRow + pawnDir
val c = kingCol + dc
if (r in 0..7 && c in 0..7) {
val p = currentBoard[r * 8 + c]
if (p != null && p.color == enemy && p.type == PieceType.PAWN) return true
}
}
// King adjacency
for (dr in -1..1) {
for (dc in -1..1) {
if (dr == 0 && dc == 0) continue
val r = kingRow + dr
val c = kingCol + dc
if (r in 0..7 && c in 0..7) {
val p = currentBoard[r * 8 + c]
if (p != null && p.color == enemy && p.type == PieceType.KING) return true
}
}
}
// Sliding attacks: diagonals (bishop/queen) and straights (rook/queen)
val slideDirs = intArrayOf(-1, -1, -1, 1, 1, -1, 1, 1, -1, 0, 1, 0, 0, -1, 0, 1)
for (d in slideDirs.indices step 2) {
val dr = slideDirs[d]
val dc = slideDirs[d + 1]
val isDiagonal = dr != 0 && dc != 0
var r = kingRow + dr
var c = kingCol + dc
while (r in 0..7 && c in 0..7) {
val p = currentBoard[r * 8 + c]
if (p != null) {
if (p.color == enemy) {
if (p.type == PieceType.QUEEN) return true
if (isDiagonal && p.type == PieceType.BISHOP) return true
if (!isDiagonal && p.type == PieceType.ROOK) return true
}
break
}
r += dr
c += dc
}
}
return false
}
/**
* Generates pseudo-legal moves for [piece] at [pos] without considering
* whether they leave the king in check. Sliding pieces (bishop, rook, queen)
* ray-cast in each direction until blocked.
*/
private fun getPseudoLegalMoves(
piece: Piece,
pos: Position,
currentBoard: Array<Piece?>
): List<Position> {
val moves = ArrayList<Position>()
fun addIfValid(r: Int, c: Int): Boolean {
if (r !in 0..7 || c !in 0..7) return false
val targetPos = getPos(r, c)
val targetPiece = currentBoard[targetPos.index]
return when {
targetPiece == null -> {
moves.add(targetPos)
true
}
targetPiece.color != piece.color -> {
moves.add(targetPos)
false
}
else -> false
}
}
fun slide(dr: Int, dc: Int) {
var r = pos.row + dr
var c = pos.col + dc
while (addIfValid(r, c)) {
r += dr
c += dc
}
}
when (piece.type) {
PieceType.PAWN -> {
val dir = if (piece.color == PieceColor.WHITE) -1 else 1
val startRow = if (piece.color == PieceColor.WHITE) 6 else 1
if (pos.row + dir in 0..7) {
val forward1 = getPos(pos.row + dir, pos.col)
if (currentBoard[forward1.index] == null) {
moves.add(forward1)
if (pos.row == startRow) {
val forward2 = getPos(pos.row + dir * 2, pos.col)
if (currentBoard[forward2.index] == null) {
moves.add(forward2)
}
}
}
for (dc in listOf(-1, 1)) {
if (pos.col + dc in 0..7) {
val target = getPos(pos.row + dir, pos.col + dc)
val targetPiece = currentBoard[target.index]
if (targetPiece != null && targetPiece.color != piece.color) {
moves.add(target)
}
}
}
}
}
PieceType.KNIGHT -> {
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
).forEach { (dr, dc) ->
addIfValid(pos.row + dr, pos.col + dc)
}
}
PieceType.BISHOP -> {
slide(-1, -1)
slide(-1, 1)
slide(1, -1)
slide(1, 1)
}
PieceType.ROOK -> {
slide(-1, 0)
slide(1, 0)
slide(0, -1)
slide(0, 1)
}
PieceType.QUEEN -> {
slide(-1, -1)
slide(-1, 1)
slide(1, -1)
slide(1, 1)
slide(-1, 0)
slide(1, 0)
slide(0, -1)
slide(0, 1)
}
PieceType.KING -> {
for (dr in -1..1) {
for (dc in -1..1) {
if (dr != 0 || dc != 0) {
addIfValid(pos.row + dr, pos.col + dc)
}
}
}
if (!piece.hasMoved) {
val kRook = currentBoard[getPos(pos.row, 7).index]
if (kRook?.type == PieceType.ROOK &&
kRook.color == piece.color &&
!kRook.hasMoved &&
currentBoard[getPos(pos.row, 5).index] == null &&
currentBoard[getPos(pos.row, 6).index] == null
) {
moves.add(getPos(pos.row, 6))
}
val qRook = currentBoard[getPos(pos.row, 0).index]
if (qRook?.type == PieceType.ROOK &&
qRook.color == piece.color &&
!qRook.hasMoved &&
currentBoard[getPos(pos.row, 1).index] == null &&
currentBoard[getPos(pos.row, 2).index] == null &&
currentBoard[getPos(pos.row, 3).index] == null
) {
moves.add(getPos(pos.row, 2))
}
}
}
}
return moves
}
private fun createInitialBoard(): Array<Piece?> {
val arr = arrayOfNulls<Piece>(64)
for (col in 0..7) {
arr[getPos(1, col).index] = Piece(PieceType.PAWN, PieceColor.BLACK)
arr[getPos(6, col).index] = Piece(PieceType.PAWN, PieceColor.WHITE)
}
val order = arrayOf(
PieceType.ROOK,
PieceType.KNIGHT,
PieceType.BISHOP,
PieceType.QUEEN,
PieceType.KING,
PieceType.BISHOP,
PieceType.KNIGHT,
PieceType.ROOK
)
for (col in 0..7) {
arr[getPos(0, col).index] = Piece(order[col], PieceColor.BLACK)
arr[getPos(7, col).index] = Piece(order[col], PieceColor.WHITE)
}
return arr
}
}
// --- 5. NAVIGATION & UI COMPONENTS ---
@Composable
fun ChessApp() {
val view = LocalView.current
val context = LocalContext.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = false
}
}
val historicalBattles = remember(context) {
HistoricalBattleLoader.load(context)
}
var currentScreen by remember { mutableStateOf(ChessScreen.SETTINGS) }
var appSettings by remember { mutableStateOf(GameSettings()) }
val gameState = remember { ChessBoardState() }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF121212))
) {
AnimatedContent(
targetState = currentScreen,
modifier = Modifier.fillMaxSize(),
transitionSpec = {
if (targetState == ChessScreen.GAME) {
slideInHorizontally(tween(500, easing = EaseOutCubic)) { it } +
fadeIn(tween(300)) togetherWith
slideOutHorizontally(tween(500, easing = EaseOutCubic)) { -it } +
fadeOut(tween(300))
} else {
slideInHorizontally(tween(500, easing = EaseOutCubic)) { -it } +
fadeIn(tween(300)) togetherWith
slideOutHorizontally(tween(500, easing = EaseOutCubic)) { it } +
fadeOut(tween(300))
}
},
label = "ScreenTransition"
) { screen ->
when (screen) {
ChessScreen.SETTINGS -> {
SettingsScreen(
settings = appSettings,
historicalBattles = historicalBattles,
onSettingsChanged = { appSettings = it },
onStartGame = { puzzle ->
gameState.startGame(appSettings.timerMinutes, puzzle)
currentScreen = ChessScreen.GAME
}
)
}
ChessScreen.GAME -> {
ChessGameScreen(
gameState = gameState,
settings = appSettings,
onNavigateBack = { currentScreen = ChessScreen.SETTINGS }
)
}
}
}
}
}
@Composable
fun SettingsScreen(
settings: GameSettings,
historicalBattles: List<ChessPuzzle>,
onSettingsChanged: (GameSettings) -> Unit,
onStartGame: (ChessPuzzle?) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
var isGeneratingPuzzle by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF121212))
.statusBarsPadding()
.padding(top = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Chess Setup",
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 32.dp)
)
Column(modifier = Modifier.padding(horizontal = 24.dp)) {
Text(
text = "AI Engine Difficulty",
color = TextMuted,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 12.dp, start = 4.dp)
)
val difficulties = listOf(2 to "Casual", 3 to "Standard", 4 to "Expert")
val selectedIndex =
max(0, difficulties.indexOfFirst { it.first == settings.difficultyDepth })
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clip(RoundedCornerShape(50))
.background(SurfaceDark)
.padding(6.dp)
) {
val segmentWidth = maxWidth / difficulties.size
val indicatorOffset by animateDpAsState(
targetValue = segmentWidth * selectedIndex,
animationSpec = spring(
dampingRatio = 0.75f,
stiffness = Spring.StiffnessLow
),
label = "IndicatorOffset"
)
Box(
modifier = Modifier
.offset(x = indicatorOffset)
.width(segmentWidth)
.fillMaxHeight()
.clip(RoundedCornerShape(50))
.background(CyberMagentaGradient)
)
Row(modifier = Modifier.fillMaxSize()) {
difficulties.forEach { (depth, label) ->
val isSelected = settings.difficultyDepth == depth
val textColor by animateColorAsState(
targetValue = if (isSelected) Color.White else TextMuted,
label = "DiffText"
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSettingsChanged(settings.copy(difficultyDepth = depth))
},
contentAlignment = Alignment.Center
) {
Text(
text = label,
color = textColor,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
}
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.clip(RoundedCornerShape(24.dp))
.background(SurfaceDark)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSettingsChanged(settings.copy(timersEnabled = !settings.timersEnabled))
}
.padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Blitz Timers",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (settings.timersEnabled) {
"${settings.timerMinutes} minutes per player"
} else {
"Off"
},
color = MagentaAccent.copy(alpha = 0.8f),
fontSize = 14.sp
)
}
Switch(
checked = settings.timersEnabled,
onCheckedChange = {
onSettingsChanged(settings.copy(timersEnabled = it))
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = MagentaAccent,
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = SurfaceVariant,
uncheckedBorderColor = Color.Transparent
)
)
}
AnimatedVisibility(
visible = settings.timersEnabled,
enter = expandVertically(
expandFrom = Alignment.Top,
animationSpec = spring(
dampingRatio = 0.8f,
stiffness = Spring.StiffnessLow
)
) + fadeIn(tween(300)) + slideInVertically(initialOffsetY = { -it / 6 }),
exit = shrinkVertically(
shrinkTowards = Alignment.Top,
animationSpec = tween(250, easing = FastOutSlowInEasing)
) + fadeOut(tween(200))
) {
Column {
Text(
text = "Time Control",
color = TextMuted,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(bottom = 12.dp)
)
LazyRow(
contentPadding = PaddingValues(
start = 24.dp,
end = 24.dp,
bottom = 24.dp
),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(listOf(1, 3, 5, 10, 15, 30)) { mins ->
val isSelected = settings.timerMinutes == mins
val textColor by animateColorAsState(
targetValue = if (isSelected) Color.White else TextMuted,
label = "ChipText"
)
val gradientAlpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
label = "ChipGradientAlpha"
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(SurfaceVariant)
.clickable {
onSettingsChanged(settings.copy(timerMinutes = mins))
}
) {
Box(
modifier = Modifier
.matchParentSize()
.alpha(gradientAlpha)
.background(CyberMagentaGradient)
)
Text(
text = "$mins min",
color = textColor,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
modifier = Modifier.padding(
horizontal = 20.dp,
vertical = 12.dp
)
)
}
}
}
}
}
HorizontalDivider(
color = SurfaceVariant,
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 24.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSettingsChanged(settings.copy(puzzleModeEnabled = !settings.puzzleModeEnabled))
}
.padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Historical Battles",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (settings.puzzleModeEnabled) {
"Play famous positions from chess history"
} else {
"Off"
},
color = MagentaAccent.copy(alpha = 0.8f),
fontSize = 14.sp
)
}
Switch(
checked = settings.puzzleModeEnabled,
onCheckedChange = {
onSettingsChanged(settings.copy(puzzleModeEnabled = it))
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = MagentaAccent,
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = SurfaceVariant,
uncheckedBorderColor = Color.Transparent
)
)
}
HorizontalDivider(
color = SurfaceVariant,
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 24.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSettingsChanged(settings.copy(highlightLastMove = !settings.highlightLastMove))
}
.padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Highlight Moves",
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Subtly outline previous action",
color = TextMuted,
fontSize = 14.sp
)
}
Switch(
checked = settings.highlightLastMove,
onCheckedChange = {
onSettingsChanged(settings.copy(highlightLastMove = it))
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = MagentaAccent,
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = SurfaceVariant,
uncheckedBorderColor = Color.Transparent
)
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Board Theme",
color = TextMuted,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(bottom = 12.dp, start = 4.dp)
)
LazyRow(
contentPadding = PaddingValues(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(AppThemes) { theme ->
val isSelected = settings.theme == theme
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.05f else 1f,
label = "ThemeScale"
)
val gradientAlpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
label = "ThemeGradientAlpha"
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.scale(scale)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSettingsChanged(settings.copy(theme = theme))
}
) {
Box(
modifier = Modifier
.size(72.dp)
.clip(RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.matchParentSize()
.alpha(gradientAlpha)
.background(CyberMagentaGradient)
)
Box(
modifier = Modifier
.fillMaxSize(if (isSelected) 0.9f else 1f)
.clip(RoundedCornerShape(12.dp))
) {
Row(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(theme.light)
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(theme.dark)
)
}
}
}
Text(
text = theme.name,
color = if (isSelected) MagentaAccent else TextMuted,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.height(64.dp)
.navigationBarsPadding()
.clip(RoundedCornerShape(20.dp))
.background(if (isGeneratingPuzzle) SurfaceVariant else Color.Transparent)
.background(
if (!isGeneratingPuzzle) CyberMagentaGradient else SolidColor(Color.Transparent)
)
.clickable(enabled = !isGeneratingPuzzle) {
if (settings.puzzleModeEnabled && historicalBattles.isNotEmpty()) {
isGeneratingPuzzle = true
coroutineScope.launch {
val finalPuzzle = historicalBattles.random()
onStartGame(finalPuzzle)
isGeneratingPuzzle = false
}
} else if (!settings.puzzleModeEnabled) {
onStartGame(null)
}
},
contentAlignment = Alignment.Center
) {
if (isGeneratingPuzzle) {
CircularProgressIndicator(
color = MagentaAccent,
modifier = Modifier.size(24.dp)
)
} else {
Text(
text = if (settings.puzzleModeEnabled) {
"PLAY HISTORICAL BATTLE"
} else {
"START MATCH"
},
fontSize = 16.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.White,
letterSpacing = 1.5.sp
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
fun ChessGameScreen(
gameState: ChessBoardState,
settings: GameSettings,
onNavigateBack: () -> Unit
) {
val chessAI = remember { ChessAI(gameState) }
var isThinking by remember { mutableStateOf(false) }
val userColor = gameState.activePuzzle?.playerTurn ?: PieceColor.WHITE
val aiColor = if (userColor == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE
LaunchedEffect(gameState.currentTurn, gameState.gameStatus, gameState.isDecidingTurn) {
if (settings.timersEnabled &&
gameState.gameStatus == GameStatus.PLAYING &&
gameState.currentTurn != null &&
!gameState.isDecidingTurn
) {
while (true) {
delay(100)
gameState.tickTime(100)
}
}
}
LaunchedEffect(gameState.currentTurn, gameState.gameStatus) {
if (gameState.gameStatus == GameStatus.PLAYING &&
gameState.currentTurn == aiColor &&
!gameState.isDecidingTurn
) {
try {
isThinking = true
val move = chessAI.calculateBestMove(
gameState.internalBoard,
aiColor,
settings.difficultyDepth
)
if (move != null) {
delay(600)
gameState.executeMove(move)
}
} finally {
isThinking = false
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF121212))
.statusBarsPadding()
.padding(horizontal = 16.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 24.dp)
) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier
.align(Alignment.CenterStart)
.background(SurfaceDark, CircleShape)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = Color.White
)
}
if (gameState.activePuzzle != null) {
Column(
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 52.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val puzzle = gameState.activePuzzle!!
Text(
text = puzzle.white + " vs " + puzzle.black,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 1
)
val subtitle = buildString {
if (puzzle.era.isNotEmpty()) append(puzzle.era)
if (puzzle.year > 0) {
if (isNotEmpty()) append(", ")
append(puzzle.year)
}
}
if (subtitle.isNotEmpty()) {
Text(
text = subtitle,
color = TealAccent,
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
}
}
} else {
val turnText = when (gameState.gameStatus) {
GameStatus.CHECKMATE -> "CHECKMATE!"
GameStatus.STALEMATE -> "STALEMATE"
GameStatus.PUZZLE_SOLVED -> "PUZZLE SOLVED!"
GameStatus.PLAYING -> when {
gameState.checkedKingColor != null -> "CHECK!"
isThinking -> "AI calculating..."
gameState.currentTurn == userColor -> "Your Turn"
else -> "Deciding..."
}
else -> ""
}
Text(
text = turnText,
color = if (
gameState.checkedKingColor != null ||
gameState.gameStatus == GameStatus.CHECKMATE
) {
Color(0xFFEF5350)
} else {
Color.White
},
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Center)
)
}
}
if (gameState.activePuzzle != null) {
val puzzle = gameState.activePuzzle!!
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(SurfaceDark)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = puzzle.context,
color = Color(0xFFE0E0E0),
fontSize = 14.sp
)
HorizontalDivider(
color = SurfaceVariant,
modifier = Modifier.padding(vertical = 4.dp)
)
val details = buildList {
if (puzzle.site.isNotBlank()) add("📍" to puzzle.site)
if (puzzle.result.isNotBlank()) add("🏆" to puzzle.result)
if (puzzle.eco.isNotBlank()) add("📖" to puzzle.eco)
if (puzzle.opening.isNotBlank()) add("♟" to puzzle.opening)
if (puzzle.sourceBook.isNotBlank()) add("📚" to puzzle.sourceBook)
}
for ((icon, value) in details) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = icon, fontSize = 13.sp)
Text(
text = value,
color = TextMuted,
fontSize = 13.sp
)
}
}
}
}
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
if (settings.timersEnabled) {
PlayerTimer(
timeMillis = gameState.blackTimeMillis,
isActive = gameState.currentTurn == aiColor && !gameState.isDecidingTurn,
playerName = "AI (${aiColor.name})"
)
Spacer(modifier = Modifier.height(16.dp))
}
ChessBoard(
boardState = gameState.uiBoard,
selectedPosition = gameState.selectedPosition,
validMoves = gameState.validMoves,
checkedKingColor = gameState.checkedKingColor,
lastMove = if (settings.highlightLastMove) gameState.lastMove else null,
lightSquareColor = settings.theme.light,
darkSquareColor = settings.theme.dark,
modifier = Modifier.alpha(
if (gameState.isDecidingTurn || isThinking) 0.5f else 1f
),
onSquareClick = { pos ->
if (!gameState.isDecidingTurn &&
!isThinking &&
gameState.currentTurn == userColor
) {
gameState.onSquareClicked(pos)
}
}
)
if (settings.timersEnabled) {
Spacer(modifier = Modifier.height(16.dp))
PlayerTimer(
timeMillis = gameState.whiteTimeMillis,
isActive = gameState.currentTurn == userColor && !gameState.isDecidingTurn,
playerName = "You (${userColor.name})"
)
}
}
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
if (gameState.isDecidingTurn) {
TurnDeciderAnimation(onDecided = { gameState.finishCoinToss(it) })
}
if (gameState.gameStatus != GameStatus.PLAYING) {
GameOverDialog(
status = gameState.gameStatus,
winnerColor = when (gameState.gameStatus) {
GameStatus.CHECKMATE ->
if (gameState.currentTurn == PieceColor.WHITE) PieceColor.BLACK else PieceColor.WHITE
GameStatus.TIMEOUT_WHITE -> PieceColor.BLACK
GameStatus.TIMEOUT_BLACK -> PieceColor.WHITE
GameStatus.PUZZLE_SOLVED -> userColor
else -> null
},
onPlayAgain = { gameState.prepareNewGame(settings.timerMinutes) }
)
}
}
}
@Composable
fun PlayerTimer(timeMillis: Long, isActive: Boolean, playerName: String) {
val totalSeconds = timeMillis / 1000
val timeString = "${(totalSeconds / 60).toString().padStart(2, '0')}:${
(totalSeconds % 60).toString().padStart(2, '0')
}"
val textColor by animateColorAsState(
targetValue = if (isActive) Color.White else TextMuted,
label = "TimerText"
)
val gradientAlpha by animateFloatAsState(
targetValue = if (isActive) 1f else 0f,
animationSpec = tween(400),
label = "TimerBg"
)
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(SurfaceDark)
) {
Box(
modifier = Modifier
.matchParentSize()
.alpha(gradientAlpha)
.background(CyberMagentaGradient)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = playerName,
color = textColor,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
Text(
text = timeString,
color = textColor,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
}
}
@Composable
fun GameOverDialog(
status: GameStatus,
winnerColor: PieceColor?,
onPlayAgain: () -> Unit
) {
val titleText = when (status) {
GameStatus.STALEMATE -> "Draw"
GameStatus.TIMEOUT_WHITE -> "White out of time!"
GameStatus.TIMEOUT_BLACK -> "Black out of time!"
GameStatus.CHECKMATE ->
"${winnerColor?.name?.lowercase()?.replaceFirstChar { it.uppercase() }} Wins!"
GameStatus.PUZZLE_SOLVED -> "Brilliant!"
else -> ""
}
AlertDialog(
onDismissRequest = {},
icon = {
Text(
text = when {
status == GameStatus.STALEMATE -> "🤝"
status.name.contains("TIMEOUT") -> "⏰"
else -> "👑"
},
fontSize = 48.sp
)
},
title = {
Text(
text = titleText,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
},
text = {
Text(
text = "Would you like to play another game?",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(50))
.background(CyberMagentaGradient)
.clickable { onPlayAgain() }
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "PLAY AGAIN",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
},
containerColor = SurfaceDark,
titleContentColor = Color.White,
textContentColor = Color(0xFFE0E0E0),
shape = RoundedCornerShape(24.dp)
)
}
@Composable
fun TurnDeciderAnimation(onDecided: (PieceColor) -> Unit) {
val rotationY = remember { Animatable(0f) }
val scale = remember { Animatable(0.5f) }
val alpha = remember { Animatable(1f) }
var chosenColor by remember { mutableStateOf<PieceColor?>(null) }
LaunchedEffect(Unit) {
val isWhiteStart = Random.nextBoolean()
chosenColor = if (isWhiteStart) PieceColor.WHITE else PieceColor.BLACK
launch {
scale.animateTo(1.5f, tween(800, easing = EaseOutBack))
}
rotationY.animateTo(
targetValue = (360f * 15f) + if (isWhiteStart) 0f else 180f,
animationSpec = tween(3500, easing = FastOutSlowInEasing)
)
scale.animateTo(1.8f, tween(200, easing = EaseOut))
scale.animateTo(1.5f, tween(300, easing = EaseIn))
delay(800)
launch {
scale.animateTo(3f, tween(400))
}
alpha.animateTo(0f, tween(400))
onDecided(chosenColor!!)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.6f * alpha.value)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Who plays first?",
color = Color.White.copy(alpha = alpha.value),
fontSize = 24.sp,
fontWeight = FontWeight.Light,
modifier = Modifier.padding(bottom = 32.dp)
)
Box(
modifier = Modifier
.graphicsLayer {
this.rotationY = rotationY.value
scaleX = scale.value
scaleY = scale.value
this.alpha = alpha.value
cameraDistance = 12f * density
}
.size(100.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = "♚",
fontSize = 64.sp,
color = if ((rotationY.value % 360) < 90f || (rotationY.value % 360) > 270f) {
Color.White
} else {
Color.Black
},
textAlign = TextAlign.Center
)
}
}
}
}
@Composable
fun ChessBoard(
boardState: ImmutableBoard,
selectedPosition: Position?,
validMoves: Set<Position>,
checkedKingColor: PieceColor?,
lastMove: Move?,
lightSquareColor: Color,
darkSquareColor: Color,
modifier: Modifier = Modifier,
onSquareClick: (Position) -> Unit
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.padding(4.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
for (row in 0..7) {
Row(modifier = Modifier.weight(1f)) {
for (col in 0..7) {
key(row, col) {
val pos = getPos(row, col)
val piece = boardState.squares[pos.index]
val clickAction = remember(pos) { { onSquareClick(pos) } }
val isLastMove = pos == lastMove?.from || pos == lastMove?.to
ChessSquare(
modifier = Modifier.weight(1f),
piece = piece,
isLightSquare = (row + col) % 2 == 0,
isSelected = pos == selectedPosition,
isValidMove = pos in validMoves,
isLastMove = isLastMove,
isKingInDanger = piece?.type == PieceType.KING && piece.color == checkedKingColor,
lightColor = lightSquareColor,
darkColor = darkSquareColor,
onClick = clickAction
)
}
}
}
}
}
}
}
@Composable
fun ChessSquare(
modifier: Modifier = Modifier,
piece: Piece?,
isLightSquare: Boolean,
isSelected: Boolean,
isValidMove: Boolean,
isLastMove: Boolean,
isKingInDanger: Boolean,
lightColor: Color,
darkColor: Color,
onClick: () -> Unit
) {
val targetBackgroundColor = when {
isKingInDanger -> Color(0xFFE53935)
isSelected -> MagentaAccent.copy(alpha = 0.4f)
isLightSquare -> lightColor
else -> darkColor
}
val animatedBackgroundColor by animateColorAsState(
targetValue = targetBackgroundColor,
animationSpec = tween(300),
label = "SquareColor"
)
val baseScale = remember { Animatable(1f) }
val infiniteTransition = rememberInfiniteTransition(label = "SelectPulse")
val selectedScale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.15f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = androidx.compose.animation.core.EaseInOut),
repeatMode = RepeatMode.Reverse
),
label = "SelectedScale"
)
LaunchedEffect(isKingInDanger) {
if (isKingInDanger) {
baseScale.animateTo(1.4f, tween(100, easing = FastOutLinearInEasing))
baseScale.animateTo(1f, tween(300, easing = LinearOutSlowInEasing))
} else {
baseScale.snapTo(1f)
}
}
val validMoveAlpha by animateFloatAsState(
targetValue = if (isValidMove) 1f else 0f,
animationSpec = tween(200),
label = "ValidMoveAlpha"
)
Box(
modifier = modifier
.fillMaxHeight()
.background(animatedBackgroundColor)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
if (isLastMove) {
Box(
modifier = Modifier
.matchParentSize()
.padding(4.dp),
contentAlignment = Alignment.TopEnd
) {
Box(
modifier = Modifier
.size(5.dp)
.clip(CircleShape)
.background(TealAccent.copy(alpha = 0.7f))
)
}
}
if (piece != null) {
Text(
text = piece.symbol,
fontSize = 36.sp,
color = if (piece.color == PieceColor.WHITE) Color.White else Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier.scale(if (isSelected) selectedScale else baseScale.value)
)
}
if (isValidMove) {
Box(
modifier = Modifier
.size(if (piece != null) 36.dp else 12.dp)
.alpha(validMoveAlpha)
.clip(CircleShape)
.background(
if (piece != null) {
Color.Transparent
} else {
MagentaAccent.copy(alpha = 0.8f)
}
)
.border(
width = if (piece != null) 3.dp else 0.dp,
color = MagentaAccent.copy(alpha = 0.8f),
shape = CircleShape
)
)
}
}
}
@Kyriakos-Georgiopoulos
Copy link
Copy Markdown
Author

[
  {
    "id": "FIS-01",
    "name": "Robert James Fischer vs James T Sherwin",
    "white": "Robert James Fischer",
    "black": "James T Sherwin",
    "event": "New Jersey Open",
    "site": "USA",
    "date": "1957.09.02",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B40",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 { coment 1234 } 1...c5 2.Nf3 e6 3.d3 Nc6 4.g3 Nf6 5.Bg2 Be7 6.O-O O-O 7.Nbd2 Rb8 8.Re1 d6 9.c3 b6 10.d4 Qc7 11.e5 Nd5 12.exd6 Bxd6 13.Ne4 c4 14.Nxd6 Qxd6 15.Ng5 Nce7 16.Qc2 Ng6 17.h4 Nf6 18.Nxh7 Nxh7 19.h5 Nh4 20.Bf4 Qd8 21.gxh4 Rb7 22.h6 Qxh4 23.hxg7 Kxg7 24.Re4 Qh5 25.Re3 f5 26.Rh3 Qe8 27.Be5+ Nf6 28.Qd2 Kf7 29.Qg5 Qe7 30.Bxf6 Qxf6 31.Rh7+ Ke8 32.Qxf6 Rxh7 33.Bc6+ 1-0"
  },
  {
    "id": "FIS-02",
    "name": "Robert James Fischer vs Bent Larsen",
    "white": "Robert James Fischer",
    "black": "Bent Larsen",
    "event": "Portoroz Interzonal",
    "site": "Portoroz SLO",
    "date": "1958.08.16",
    "year": 1958,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B76",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 g6 6.Be3 Bg7 7.f3 O-O 8.Qd2 Nc6 9.Bc4 Nxd4 10.Bxd4 Be6 11.Bb3 Qa5 12.O-O-O b5 13.Kb1 b4 14.Nd5 Bxd5 15.Bxd5 Rac8 16.Bb3 Rc7 17.h4 Qb5 18.h5 Rfc8 19.hxg6 hxg6 20.g4 a5 21.g5 Nh5 22.Rxh5 gxh5 23.g6 e5 24.gxf7+ Kf8 25.Be3 d5 26.exd5 Rxf7 27.d6 Rf6 28.Bg5 Qb7 29.Bxf6 Bxf6 30.d7 Rd8 31.Qd6+ 1-0"
  },
  {
    "id": "FIS-03",
    "name": "Tigran Vartanovich Petrosian vs Robert James Fischer",
    "white": "Tigran Vartanovich Petrosian",
    "black": "Robert James Fischer",
    "event": "Portoroz Interzonal",
    "site": "Portoroz SLO",
    "date": "1958.08.27",
    "year": 1958,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "A15",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.c4 Nf6 2.Nc3 g6 3.g3 Bg7 4.Bg2 O-O 5.Nf3 d6 6.O-O Nc6 7.d3 Nh5 8.d4 e5 9.d5 Ne7 10.e4 f5 11.exf5 gxf5 12.Nxe5 Nxg3 13.hxg3 Bxe5 14.f4 Bg7 15.Be3 Bd7 16.Bd4 Ng6 17.Re1 Rf7 18.Bf3 Qf8 19.Kf2 Re8 20.Rxe8 Qxe8 21.Bxg7 Rxg7 22.Qd4 b6 23.Rh1 a5 24.Nd1 Qf8 25.Ne3 Rf7 26.b3 Qg7 27.Qxg7+ Kxg7 28.a3 Rf8 29.Be2 Ne7 30.Bd3 h6 31.Rh5 Be8 32.Rh2 Bd7 33.Rh1 Rh8 34.Nc2 Kf6 35.Nd4 Kg7 36.Be2 Ng8 37.b4 Nf6 38.Bd3 axb4 39.axb4 Kg6 40.Ra1 Ng4+ 41.Ke2 Re8+ 42.Kd2 Nf6 43.Ra6 Rb8 44.Ra7 Rc8 45.c5 bxc5 46.bxc5 dxc5 47.Nf3 Kf7 48.Ne5+ Ke7 49.Nxd7 Nxd7 50.Bxf5 Rf8 51.g4 Kd6 52.Bxd7 Kxd7 53.Ke3 Re8+ 54.Kf3 Kd6 55.Ra6+ Kxd5 56.Rxh6 c4 57.Rh1 c3 58.g5 c5 59.Rd1+ Kc4 60.g6 c2 61.Rc1 Kd3 62.f5 Rg8 63.Kf4 Kd2 64.Rxc2+ Kxc2 65.Kg5 c4 66.f6 c3 67.f7 1/2-1/2"
  },
  {
    "id": "FIS-04",
    "name": "Herman Pilnik vs Robert James Fischer",
    "white": "Herman Pilnik",
    "black": "Robert James Fischer",
    "event": "Mar del Plata",
    "site": "Mar del Plata ARG",
    "date": "1959.04.03",
    "year": 1959,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B92",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Be2 e5 7.Nb3 Be7 8.O-O O-O 9.Be3 Be6 10.f3 Qc7 11.Qe1 Nbd7 12.Rd1 b5 13.Rd2 Nb6 14.Qf2 Rab8 15.Bxb6 Rxb6 16.Nd5 Nxd5 17.exd5 Bd7 18.f4 Bf6 19.c3 Rbb8 20.fxe5 Bxe5 21.Nd4 g6 22.a3 a5 23.Kh1 b4 24.cxb4 axb4 25.Rc2 Qb6 26.Nc6 bxa3 27.Qxb6 Rxb6 28.bxa3 Ra8 29.Nxe5 dxe5 30.Rc3 Rb2 31.Rc7 Bf5 32.g4 Be4+ 33.Bf3 Bd3 34.d6 Rd8 35.Re1 Rxd6 36.Rxe5 Rf6 37.Re3 Rxf3 38.Rxf3 Be4 39.Rxf7 Rf2 40.Rf8+ Kg7 0-1"
  },
  {
    "id": "FIS-05",
    "name": "Robert James Fischer vs Hector Decio Rossetto",
    "white": "Robert James Fischer",
    "black": "Hector Decio Rossetto",
    "event": "Mar del Plata",
    "site": "Mar del Plata ARG",
    "date": "1959.04.05",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B41",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 e6 3.d4 cxd4 4.Nxd4 a6 5.c4 Qc7 6.Nc3 Nf6 7.Bd3 Nc6 8.Be3 Nxd4 9.Bxd4 Bc5 10.Bc2 d6 11.O-O Bd7 12.Na4 Bxd4 13.Qxd4 Rd8 14.Rfd1 O-O 15.Rac1 Qa5 16.Qb6 Qxb6 17.Nxb6 Bc6 18.f3 Nd7 19.Nd5 Bxd5 20.exd5 e5 21.b4 g6 22.Ba4 b6 23.Rd3 f5 24.Ra3 Nb8 25.c5 bxc5 26.bxc5 dxc5 27.Rxc5 Kg7 28.Rb3 Rf7 29.d6 Nd7 30.Rc7 Nf8 31.Rbb7 Rxc7 32.dxc7 Rc8 33.Bb3 a5 34.a4 h6 35.h3 g5 36.g4 fxg4 37.hxg4 1-0"
  },
  {
    "id": "FIS-06",
    "name": "Robert James Fischer vs Ruben Shocron",
    "white": "Robert James Fischer",
    "black": "Ruben Shocron",
    "event": "Mar del Plata",
    "site": "Mar del Plata ARG",
    "date": "1959.03.30",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C97",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 d6 8.c3 O-O 9.h3 Na5 10.Bc2 c5 11.d4 Qc7 12.Nbd2 Bd7 13.Nf1 Rfe8 14.Ne3 g6 15.dxe5 dxe5 16.Nh2 Rad8 17.Qf3 Be6 18.Nhg4 Nxg4 19.hxg4 Qc6 20.g5 Nc4 21.Ng4 Bxg4 22.Qxg4 Nb6 23.g3 c4 24.Kg2 Nd7 25.Rh1 Nf8 26.b4 Qe6 27.Qe2 a5 28.bxa5 Qa6 29.Be3 Qxa5 30.a4 Ra8 31.axb5 Qxb5 32.Rhb1 Qc6 33.Rb6 Qc7 34.Rba6 Rxa6 35.Rxa6 Rc8 36.Qg4 Ne6 37.Ba4 Rb8 38.Rc6 Qd8 39.Rxe6 Qc8 40.Bd7 1-0"
  },
  {
    "id": "FIS-07",
    "name": "Fridrik Olafsson vs Robert James Fischer",
    "white": "Fridrik Olafsson",
    "black": "Robert James Fischer",
    "event": "Zurich",
    "site": "Zurich SUI",
    "date": "1959.05.21",
    "year": 1959,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "E93",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.c4 Nf6 2.Nc3 g6 3.d4 Bg7 4.e4 d6 5.Nf3 O-O 6.Be2 e5 7.d5 Nbd7 8.Bg5 h6 9.Bh4 a6 10.Nd2 Qe8 11.g4 Nh7 12.Qc2 Ng5 13.h3 Nc5 14.O-O-O Bd7 15.f3 Na4 16.Nxa4 Bxa4 17.b3 Bd7 18.Bf2 c5 19.h4 Nh7 20.Be3 b5 21.Nb1 f5 22.gxf5 gxf5 23.exf5 Bxf5 24.Qd2 e4 25.Rdg1 exf3 26.Bxh6 Ra7 27.Bxg7 Rxg7 28.Rxg7+ Kxg7 29.Bd3 bxc4 30.Rg1+ Kh8 31.Qc3+ Qe5 32.Qxe5+ dxe5 33.Bxf5 Rxf5 34.bxc4 Nf6 35.Nd2 f2 36.Rh1 e4 37.Kd1 e3 38.Nf1 Re5 39.Ke2 Nh5 40.Kf3 e2 0-1"
  },
  {
    "id": "FIS-08",
    "name": "Robert James Fischer vs Paul Keres",
    "white": "Robert James Fischer",
    "black": "Paul Keres",
    "event": "Zurich",
    "site": "Zurich SUI",
    "date": "1959.06.03",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C99",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 d6 8.c3 O-O 9.h3 Na5 10.Bc2 c5 11.d4 Qc7 12.Nbd2 cxd4 13.cxd4 Bb7 14.Nf1 Rac8 15.Bd3 Nc6 16.Ne3 Rfe8 17.Nf5 Bf8 18.Bg5 Nd7 19.Rc1 Qb8 20.Bb1 Nxd4 21.N3xd4 Rxc1 22.Bxc1 exd4 23.Nh6+ gxh6 24.Qg4+ Kh8 25.Qxd7 Bd5 26.Qf5 Re5 27.Qf3 f5 28.Bf4 Re8 29.Qh5 Bxe4 30.f3 Bc6 31.Rc1 Bd7 32.Bxh6 Re6 33.Bxf8 Qxf8 34.Qh4 Qf6 35.Qxf6+ Rxf6 36.Kf2 Kg7 37.Rc7 Rf7 38.Ke2 f4 39.Ra7 Kf6 40.Rxa6 Re7+ 41.Kf2 Be6 42.Rxd6 Ke5 43.Rc6 Bd5 44.Rh6 Rc7 45.Rh5+ Kd6 46.Rh6+ Ke5 47.Rh5+ Kd6 48.Rf5 Rc1 49.Bd3 Rd1 50.Ke2 Rg1 51.Kf2 Rd1 52.Ke2 Rg1 53.Rg5 Bxa2 54.Bxb5 Rb1 55.Kd3 h6 56.Rh5 Rxb2 57.Kxd4 Rxg2 58.Rxh6+ Ke7 59.Ke4 Rg5 60.Ba6 Bf7 61.Bc8 Rg6 62.Rh7 Kf8 63.Bg4 Rg7 64.Rh6 Rg6 65.Rxg6 Bxg6+ 66.Kxf4 Kg7 67.Kg5 Bd3 68.f4 Be4 69.h4 Bd3 70.h5 Be4 71.h6+ Kh8 72.Bf5 Bd5 73.Bg6 Be6 74.Kf6 Bc4 75.Kg5 Be6 76.Bh5 Kh7 77.Bg4 Bc4 78.f5 Bf7 79.Bh5 Bc4 80.Bg6+ Kg8 81.f6 1-0"
  },
  {
    "id": "FIS-09",
    "name": "Edgar Walther vs Robert James Fischer",
    "white": "Edgar Walther",
    "black": "Robert James Fischer",
    "event": "Zurich",
    "site": "Zurich SUI",
    "date": "1959.05.19",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "B99",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bg5 e6 7.f4 Be7 8.Qf3 Nbd7 9.O-O-O Qc7 10.Bd3 b5 11.Bxf6 Nxf6 12.Rhe1 Bb7 13.Kb1 Rc8 14.g4 Nd7 15.g5 Nb6 16.f5 e5 17.f6 gxf6 18.gxf6 Bf8 19.Nd5 Nxd5 20.exd5 Kd8 21.Nc6+ Bxc6 22.dxc6 Qxc6 23.Be4 Qb6 24.Qh5 Kc7 25.Bf5 Rd8 26.Qxf7+ Kb8 27.Qe6 Qc7 28.Re3 Bh6 29.Rc3 Qb7 30.f7 Bg7 31.Rcd3 Bf8 32.Qxe5 dxe5 33.Rxd8+ Ka7 34.R1d7 h5 35.Rxb7+ Kxb7 36.c3 Kc7 37.Ra8 Kd6 38.Rxa6+ Ke7 39.Re6+ Kxf7 40.Rxe5 b4 41.cxb4 Bxb4 42.h3 Kf6 43.Rb5 Bd6 44.Be4 Re8 45.Rf5+ Kg7 46.Bf3 Re1+ 47.Kc2 Rf1 48.Rd5 Rf2+ 49.Rd2 Rxd2+ 50.Kxd2 h4 51.Kd3 Kf6 52.Kc4 Ke7 53.Kb5 Kd7 54.a4 Kc7 55.b4 Kb8 56.a5 Ka7 57.Kc4 Bg3 58.b5 Bf2 59.Be2 Be3 60.Kb3 Bd2 61.b6+ Kb7 62.Ka4 Kc6 63.Bb5+ Kc5 1/2-1/2"
  },
  {
    "id": "FIS-10",
    "name": "Robert James Fischer vs Wolfgang Unzicker",
    "white": "Robert James Fischer",
    "black": "Wolfgang Unzicker",
    "event": "Zurich",
    "site": "Zurich SUI",
    "date": "1959.05.28",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C97",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 d6 8.c3 O-O 9.h3 Na5 10.Bc2 c5 11.d4 Qc7 12.Nbd2 Bd7 13.Nf1 Rfe8 14.Ne3 g6 15.dxe5 dxe5 16.Nh2 Rad8 17.Qf3 Be6 18.Nhg4 Nxg4 19.hxg4 Qc6 20.g5 Nc4 21.Ng4 Bxg4 22.Qxg4 f6 23.gxf6 Bxf6 24.a4 Nb6 25.axb5 axb5 26.Be3 Ra8 27.Red1 Kh8 28.b3 Bg7 29.Qh4 Bf6 30.Bg5 Bxg5 31.Qxg5 Rxa1 32.Rxa1 Nd7 33.Bd1 Nf6 34.Ra7 Qd6 35.Be2 Re7 36.Rxe7 Qxe7 37.Bxb5 Kg7 38.Be2 Qc7 39.Qe3 Qa5 40.g3 Qa3 41.Kg2 Qa5 42.Qd3 Qb6 43.Qc4 Qc6 44.Bd3 Qb6 45.b4 cxb4 46.cxb4 Ng4 47.Qc5 Qxc5 48.bxc5 Kf7 49.f4 Ke7 50.Kf3 Nf6 51.Bb5 Ke6 52.Bc4+ Ke7 53.c6 Ne8 54.fxe5 h6 55.Ke3 Nc7 56.Kd4 h5 57.Ke3 g5 58.Be2 h4 59.gxh4 gxh4 60.Bc4 Ne8 61.Kf4 Kd8 62.Kg4 Kc7 63.Bf7 Ng7 64.Kxh4 Kxc6 65.Kg5 1-0"
  },
  {
    "id": "FIS-11",
    "name": "Robert James Fischer vs Pal Benko",
    "white": "Robert James Fischer",
    "black": "Pal Benko",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.09.22",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B57",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 Nc6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 d6 6.Bc4 Qb6 7.Nde2 e6 8.O-O Be7 9.Bb3 O-O 10.Kh1 Na5 11.Bg5 Qc5 12.f4 b5 13.Ng3 b4 14.e5 dxe5 15.Bxf6 gxf6 16.Nce4 Qd4 17.Qh5 Nxb3 18.Qh6 exf4 19.Nh5 f5 20.Rad1 Qe5 21.Nef6+ Bxf6 22.Nxf6+ Qxf6 23.Qxf6 Nc5 24.Qg5+ Kh8 25.Qe7 Ba6 26.Qxc5 Bxf1 27.Rxf1 1-0"
  },
  {
    "id": "FIS-12",
    "name": "Svetozar Gligoric vs Robert James Fischer",
    "white": "Svetozar Gligoric",
    "black": "Robert James Fischer",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.10.22",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "B99",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bg5 e6 7.f4 Be7 8.Qf3 Qc7 9.O-O-O Nbd7 10.g4 b5 11.Bxf6 gxf6 12.f5 Ne5 13.Qh3 O-O 14.Nce2 Kh8 15.Nf4 Rg8 16.Rg1 d5 17.fxe6 dxe4 18.Nd5 Qc5 19.Nxe7 Qxe7 20.Nf5 Qxe6 21.Qh6 Bd7 22.Rd6 Nxg4 23.Rxg4 Qxf5 24.Rxg8+ Rxg8 25.Rxf6 Qd5 26.Rd6 Qf5 27.Rf6 Qg5+ 28.Qxg5 Rxg5 29.Rxf7 Bg4 30.Kd2 Bf3 31.Ke3 Rg1 32.Bh3 Re1+ 33.Kf4 Bd1 34.Ke5 e3 35.Bf5 Rg1 36.Rxh7+ Kg8 37.Rc7 Bg4 38.Bxg4 Rxg4 39.Rc3 e2 40.Re3 Rg2 41.Kd4 e1=Q 42.Rxe1 Rxc2 43.Rb1 Kf7 44.a3 Ke6 45.b3 Rxh2 46.Kc5 Kd7 47.Kb6 Ra2 48.Kxa6 Rxa3+ 49.Kb7 Kd6 50.Kb6 Kd7 51.b4 Rh3 52.Rc1 Rh8 53.Kxb5 Rb8+ 54.Ka4 Ra8+ 55.Kb3 Rc8 56.Rxc8 Kxc8 57.Kc4 Kb8 1/2-1/2"
  },
  {
    "id": "FIS-13",
    "name": "Robert James Fischer vs Svetozar Gligoric",
    "white": "Robert James Fischer",
    "black": "Svetozar Gligoric",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.10.07",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "C74",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 d6 5.c3 Nf6 6.O-O Be7 7.d4 Bd7 8.Nbd2 O-O 9.Re1 Re8 10.a3 Bf8 11.b4 d5 12.Bb3 Bg4 13.h3 Bh5 14.dxe5 Nxe5 15.g4 Nxf3+ 16.Nxf3 dxe4 17.gxh5 exf3 18.Rxe8 Qxe8 19.Qxf3 Qe1+ 20.Kg2 Re8 21.h6 c6 22.Bc2 Qe2 23.Qxe2 Rxe2 24.Bd1 Re8 25.Be3 Nd5 26.Bd2 gxh6 27.c4 Bg7 28.Rc1 Nc7 29.Be3 Ne6 30.c5 Nd4 31.Bg4 f5 32.Bh5 Re4 33.Rd1 Kf8 34.Rd3 Ke7 35.Bd1 Ne6 36.Kf3 Nd4+ 37.Kg3 Ne6 38.Kf3 Nd4+ 39.Kg2 Ne6 40.Kf3 1/2-1/2"
  },
  {
    "id": "FIS-14",
    "name": "Paul Keres vs Robert James Fischer",
    "white": "Paul Keres",
    "black": "Robert James Fischer",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.09.07",
    "year": 1959,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B99",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bg5 e6 7.f4 Be7 8.Qf3 Qc7 9.O-O-O Nbd7 10.Be2 b5 11.Bxf6 Nxf6 12.e5 Bb7 13.exf6 Bxf3 14.Bxf3 Bxf6 15.Bxa8 d5 16.Bxd5 Bxd4 17.Rxd4 exd5 18.Nxd5 Qc5 19.Re1+ Kf8 20.c3 h5 21.f5 Rh6 22.f6 gxf6 23.Nf4 h4 24.Rd8+ Kg7 25.Ree8 Qg1+ 26.Kd2 Qf2+ 27.Ne2 Rg6 28.g3 f5 29.Rg8+ Kf6 30.Rxg6+ fxg6 31.gxh4 Qxh2 32.Rd4 Qh1 33.Kc2 Ke5 34.a4 Qf1 35.Nc1 Qg2+ 36.Kb3 bxa4+ 37.Ka3 Qc2 38.Nd3+ Kf6 39.Nc5 Qc1 40.Rxa4 Qe3 41.Nxa6 f4 42.Rd4 Kf5 43.Nb4 Qe7 44.Kb3 Qxh4 45.Nd3 g5 46.c4 Qg3 47.c5 f3 48.Kc4 f2 49.Nxf2 Qxf2 50.c6 Qxb2 51.Kc5 Qc3+ 52.Kd5 g4 53.Rc4 Qe5# 0-1"
  },
  {
    "id": "FIS-15",
    "name": "Vasily Smyslov vs Robert James Fischer",
    "white": "Vasily Smyslov",
    "black": "Robert James Fischer",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.10.29",
    "year": 1959,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B99",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bg5 e6 7.f4 Be7 8.Qf3 Qc7 9.O-O-O Nbd7 10.g4 b5 11.Bxf6 Nxf6 12.g5 Nd7 13.Bh3 b4 14.Nce2 Bb7 15.Kb1 Nc5 16.Ng3 d5 17.f5 dxe4 18.Qg4 exf5 19.Ndxf5 g6 20.Nxe7 Qxe7 21.Qf4 O-O 22.Rd6 Rad8 23.Rf6 Rd5 24.Bg4 Nd7 25.Rf1 e3 26.b3 Rd2 27.Bxd7 Rxd7 28.Re1 Re8 29.h4 Qc5 30.Qc4 Qxc4 31.bxc4 Rd4 32.c5 Rxh4 33.c6 Bc8 34.Rd6 Rc4 35.Kb2 Kg7 36.Kb3 Rg4 37.Ne2 Re6 38.Red1 Rg2 39.Nf4 Rxd6 40.Rxd6 Rd2 41.Rd3 Rf2 42.Rd4 e2 43.Nd3 Bf5 44.c7 Rf3 45.c8=Q Bxc8 46.Re4 Bf5 47.Rxe2 Bxd3 48.cxd3 Rxd3+ 49.Kxb4 Rd5 50.Rg2 h6 51.gxh6+ Kxh6 52.a4 g5 53.Rc2 Rd6 54.Kc5 Re6 0-1"
  },
  {
    "id": "FIS-16",
    "name": "Robert James Fischer vs Tigran Vartanovich Petrosian",
    "white": "Robert James Fischer",
    "black": "Tigran Vartanovich Petrosian",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.10.04",
    "year": 1959,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "B11",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c6 2.Nc3 d5 3.Nf3 Bg4 4.h3 Bxf3 5.Qxf3 Nf6 6.d3 e6 7.g3 Bb4 8.Bd2 d4 9.Nb1 Bxd2+ 10.Nxd2 e5 11.Bg2 c5 12.O-O Nc6 13.Qe2 Qe7 14.f4 O-O-O 15.a3 Ne8 16.b4 cxb4 17.Nc4 f6 18.fxe5 fxe5 19.axb4 Nc7 20.Na5 Nb5 21.Nxc6 bxc6 22.Rf2 g6 23.h4 Kb7 24.h5 Qxb4 25.Rf7+ Kb6 26.Qf2 a5 27.c4 Nc3 28.Rf1 a4 29.Qf6 Qc5 30.Rxh7 Rdf8 31.Qxg6 Rxh7 32.Qxh7 Rxf1+ 33.Bxf1 a3 34.h6 a2 35.Qg8 a1=Q 36.h7 Qd6 37.h8=Q Qa7 38.g4 Kc5 39.Qf8 Qae7 40.Qa8 Kb4 41.Qh2 Kb3 42.Qa1 Qa3 43.Qxa3+ Kxa3 44.Qh6 Qf7 45.Kg2 Kb3 46.Qd2 Qh7 47.Kg3 Qxe4 48.Qf2 Qh1 1/2-1/2"
  },
  {
    "id": "FIS-17",
    "name": "Robert James Fischer vs Mikhail Tal",
    "white": "Robert James Fischer",
    "black": "Mikhail Tal",
    "event": "Bled-Zagreb-Belgrade Candidates",
    "site": "Bled, Zagreb & Belgrade YUG",
    "date": "1959.10.26",
    "year": 1959,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B87",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bc4 e6 7.Bb3 b5 8.f4 b4 9.Na4 Nxe4 10.O-O g6 11.f5 gxf5 12.Nxf5 Rg8 13.Bd5 Ra7 14.Bxe4 exf5 15.Bxf5 Re7 16.Bxc8 Qxc8 17.Bf4 Qc6 18.Qf3 Qxa4 19.Bxd6 Qc6 20.Bxb8 Qb6+ 21.Kh1 Qxb8 22.Qc6+ Rd7 23.Rae1+ Be7 24.Rxf7 Kxf7 25.Qe6+ Kf8 26.Qxd7 Qd6 27.Qb7 Rg6 28.c3 a5 29.Qc8+ Kg7 30.Qc4 Bd8 31.cxb4 axb4 32.g3 Qc6+ 33.Re4 Qxc4 34.Rxc4 Rb6 35.Kg2 Kf6 36.Kf3 Ke5 37.Ke3 Bg5+ 38.Ke2 Kd5 39.Kd3 Bf6 40.Rc2 Be5 41.Re2 Rf6 42.Rc2 Rf3+ 43.Ke2 Rf7 44.Kd3 Bd4 45.a3 b3 46.Rc8 Bxb2 47.Rd8+ Kc6 48.Rb8 Rf3+ 49.Kc4 Rc3+ 50.Kb4 Kc7 51.Rb5 Ba1 52.a4 b2 0-1"
  },
  {
    "id": "FIS-18",
    "name": "Boris Spassky vs Robert James Fischer",
    "white": "Boris Spassky",
    "black": "Robert James Fischer",
    "event": "Mar del Plata",
    "site": "Mar del Plata ARG",
    "date": "1960.03.30",
    "year": 1960,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C39",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e5 2.f4 exf4 3.Nf3 g5 4.h4 g4 5.Ne5 Nf6 6.d4 d6 7.Nd3 Nxe4 8.Bxf4 Bg7 9.Nc3 $6 Nxc3 10.bxc3 c5 11.Be2 cxd4 12.O-O Nc6 13.Bxg4 O-O 14.Bxc8 Rxc8 15.Qg4 f5 16.Qg3 dxc3 17.Rae1 Kh8 18.Kh1 Rg8 19.Bxd6 Bf8 20.Be5+ Nxe5 21.Qxe5+ Rg7 22.Rxf5 Qxh4+ 23.Kg1 Qg4 24.Rf2 Be7 25.Re4 Qg5 26.Qd4 Rf8 27.Re5 Rd8 28.Qe4 Qh4 29.Rf4 1-0"
  },
  {
    "id": "FIS-19",
    "name": "Arinbjorn Gudmundsson vs Robert James Fischer",
    "white": "Arinbjorn Gudmundsson",
    "black": "Robert James Fischer",
    "event": "Reykjavik",
    "site": "Reykjavik ISL",
    "date": "1960.10.06",
    "year": 1960,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "D95",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.d4 Nf6 2.Nf3 d5 3.e3 g6 4.c4 Bg7 5.Nc3 O-O 6.Qb3 e6 7.Be2 Nc6 8.Qc2 dxc4 9.Bxc4 e5 10.dxe5 Ng4 11.O-O Ncxe5 12.Nxe5 Nxe5 13.Be2 c6 14.f4 Ng4 15.h3 Bf5 16.e4 Qd4+ 17.Kh1 Nf2+ 18.Rxf2 Qxf2 19.exf5 Bxc3 20.bxc3 Rae8 21.Bd3 Re1+ 22.Kh2 Qg1+ 23.Kg3 Rfe8 24.Rb1 gxf5 25.Bd2 Rxb1 26.Qxb1 Qxb1 27.Bxb1 Re2 0-1"
  },
  {
    "id": "FIS-20",
    "name": "Robert James Fischer vs Max Euwe",
    "white": "Robert James Fischer",
    "black": "Max Euwe",
    "event": "Leipzig ol (Men) fin-A",
    "site": "Leipzig GDR",
    "date": "1960.11.03",
    "year": 1960,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B13",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c6 2.d4 d5 3.exd5 cxd5 4.c4 Nf6 5.Nc3 Nc6 6.Nf3 Bg4 7.cxd5 Nxd5 8.Qb3 Bxf3 9.gxf3 e6 10.Qxb7 Nxd4 11.Bb5+ Nxb5 12.Qc6+ Ke7 13.Qxb5 Nxc3 14.bxc3 Qd7 15.Rb1 Rd8 16.Be3 Qxb5 17.Rxb5 Rd7 18.Ke2 f6 19.Rd1 Rxd1 20.Kxd1 Kd7 21.Rb8 Kc6 22.Bxa7 g5 23.a4 Bg7 24.Rb6+ Kd5 25.Rb7 Bf8 26.Rb8 Bg7 27.Rb5+ Kc6 28.Rb6+ Kd5 29.a5 f5 30.Bb8 Rc8 31.a6 Rxc3 32.Rb5+ Kc4 33.Rb7 Bd4 34.Rc7+ Kd3 35.Rxc3+ Kxc3 36.Be5 1-0"
  },
  {
    "id": "FIS-21",
    "name": "Rene Letelier Martner vs Robert James Fischer",
    "white": "Rene Letelier Martner",
    "black": "Robert James Fischer",
    "event": "Leipzig ol (Men) qual-D",
    "site": "Leipzig GDR",
    "date": "1960.10.24",
    "year": 1960,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "E70",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 O-O 5.e5 Ne8 6.f4 d6 7.Be3 c5 8.dxc5 Nc6 9.cxd6 exd6 10.Ne4 Bf5 11.Ng3 Be6 12.Nf3 Qc7 13.Qb1 dxe5 14.f5 e4 15.fxe6 exf3 16.gxf3 f5 17.f4 Nf6 18.Be2 Rfe8 19.Kf2 Rxe6 20.Re1 Rae8 21.Bf3 Rxe3 22.Rxe3 Rxe3 23.Kxe3 Qxf4+ 0-1"
  },
  {
    "id": "FIS-22",
    "name": "Laszlo Szabo vs Robert James Fischer",
    "white": "Laszlo Szabo",
    "black": "Robert James Fischer",
    "event": "Leipzig ol (Men) fin-A",
    "site": "Leipzig GDR",
    "date": "1960.11.02",
    "year": 1960,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "E70",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 O-O 5.Bg5 d6 6.Qd2 c5 7.d5 e6 8.Bd3 exd5 9.Nxd5 Be6 10.Ne2 Bxd5 11.exd5 Nbd7 12.O-O Ne5 13.f4 Nxd3 14.Qxd3 h6 15.Bh4 Re8 16.Rae1 Qb6 17.Bxf6 Bxf6 18.f5 g5 19.b3 Qa5 20.Rc1 Qxa2 21.Rc2 Re3 22.Qxe3 Qxc2 23.Kh1 a5 24.h4 a4 0-1"
  },
  {
    "id": "FIS-23",
    "name": "Robert James Fischer vs Mikhail Tal",
    "white": "Robert James Fischer",
    "black": "Mikhail Tal",
    "event": "Leipzig ol (Men) fin-A",
    "site": "Leipzig GDR",
    "date": "1960.11.01",
    "year": 1960,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "C17",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e6 2.d4 d5 3.Nc3 Bb4 4.e5 c5 5.a3 Ba5 6.b4 cxd4 7.Qg4 Ne7 8.bxa5 dxc3 9.Qxg7 Rg8 10.Qxh7 Nbc6 11.Nf3 Qc7 12.Bb5 Bd7 13.O-O O-O-O 14.Bg5 Nxe5 15.Nxe5 Bxb5 16.Nxf7 Bxf1 17.Nxd8 Rxg5 18.Nxe6 Rxg2+ 19.Kh1 Qe5 20.Rxf1 Qxe6 21.Kxg2 Qg4+ 1/2-1/2"
  },
  {
    "id": "FIS-24",
    "name": "Robert James Fischer vs Klaus Viktor Darga",
    "white": "Robert James Fischer",
    "black": "Klaus Viktor Darga",
    "event": "FRG-USA",
    "site": "Berlin (W) FRG",
    "date": "1960.??.??",
    "year": 1960,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C19",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 e6 2.d4 d5 3.Nc3 Bb4 4.e5 c5 5.a3 Bxc3+ 6.bxc3 Ne7 7.a4 Qc7 8.Nf3 b6 9.Bb5+ Bd7 10.Bd3 Nbc6 11.O-O c4 12.Be2 f6 13.Ba3 fxe5 14.dxe5 Nxe5 15.Re1 N7c6 16.Nxe5 Nxe5 17.f4 Nc6 18.Bg4 O-O-O 19.Bxe6 Bxe6 20.Rxe6 Rd7 21.f5 Nd8 22.Re3 Qf4 23.Rf3 Qe4 24.a5 Nc6 25.axb6 axb6 26.Qb1 Kc7 27.Bc1 Qe1+ 28.Rf1 Qxc3 29.Bf4+ Kb7 30.Qb5 1-0"
  },
  {
    "id": "FIS-25",
    "name": "William James Lombardy vs Robert James Fischer",
    "white": "William James Lombardy",
    "black": "Robert James Fischer",
    "event": "USA-ch",
    "site": "New York, NY USA",
    "date": "1960.12.19",
    "year": 1960,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B55",
    "opening": "",
    "variation": "",
    "sourceBook": "My 60 Memorable Games",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.f3 Nc6 6.c4 e6 7.Nc3 Be7 8.Be3 O-O 9.Nc2 d5 10.cxd5 exd5 11.Nxd5 Nxd5 12.Qxd5 Qc7 13.Qb5 Bd7 14.Rc1 Nb4 15.Nxb4 Qxc1+ 16.Bxc1 Bxb5 17.Nd5 Bh4+ 18.g3 Bxf1 19.Kxf1 Bd8 20.Bd2 Rc8 21.Bc3 f5 22.e5 Rc5 23.Nb4 Ba5 24.a3 Bxb4 25.axb4 Rd5 26.Ke2 Kf7 27.h4 Ke6 28.Ke3 Rc8 29.Rg1 Rc4 30.Re1 Rxc3+ 31.bxc3 Rxe5+ 32.Kd2 Rxe1 33.Kxe1 Kd5 34.Kd2 Kc4 35.h5 b6 36.Kc2 g5 37.h6 f4 38.g4 a5 39.bxa5 bxa5 40.Kb2 a4 41.Ka3 Kxc3 42.Kxa4 Kd4 43.Kb4 Ke3 0-1"
  },
  {
    "id": "TAL-01",
    "name": "Ratmir Kholmov vs Mikhail Tal",
    "white": "Ratmir Kholmov",
    "black": "Mikhail Tal",
    "event": "",
    "site": "",
    "date": "1949.??.??",
    "year": 1949,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "D44",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.c4 e6 2.Nc3 d5 3.d4 c6 4.Nf3 Nf6 5.Bg5 dxc4 6.e4 b5 7.e5 h6 8.Bxf6 gxf6 9.exf6 Bb4 10.Be2 Qxf6 11.O-O Bxc3 12.bxc3 Nd7 13.a4 Bb7 14.Ne5 Nxe5 15.dxe5 Qxe5 16.Bf3 Rd8 17.Qc2 Rd3 18.axb5 Rxf3 19.Rxa7 Qxb5 20.gxf3 Qg5+ 21.Kh1 Rg8 0-1"
  },
  {
    "id": "TAL-02",
    "name": "Mikhail Tal vs Leonov",
    "white": "Mikhail Tal",
    "black": "Leonov",
    "event": "Riga",
    "site": "Riga URS",
    "date": "1949.??.??",
    "year": 1949,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B13",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c6 2.d4 d5 3.exd5 cxd5 4.Bd3 Nf6 5.h3 h6 6.Bf4 e6 7.Nf3 Bd6 8.Bxd6 Qxd6 9.c3 Nc6 10.O-O O-O 11.Qe2 Re8 12.Ne5 Qc7 13.f4 Nxe5 14.fxe5 Nh7 15.Qh5 Re7 16.Na3 a6 17.Nc2 Qd7 18.Ne3 Qe8 19.Rf6 Qf8 20.Rf4 Bd7 21.Ng4 Be8 22.Nf6+ Nxf6 23.exf6 Rc7 24.fxg7 Kxg7 25.Qe5+ 1-0"
  },
  {
    "id": "TAL-03",
    "name": "Mikhail Tal vs Shlomo Giterman",
    "white": "Mikhail Tal",
    "black": "Shlomo Giterman",
    "event": "Leningrad",
    "site": "Leningrad",
    "date": "1951.??.??",
    "year": 1951,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "D15",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 d5 2.c4 c6 3.Nc3 Nf6 4.Nf3 g6 5.e3 Bg7 6.Bd3 O-O 7.O-O c5 8.dxc5 dxc4 9.Bxc4 Qxd1 10.Rxd1 Nbd7 11.e4 Nxc5 12.e5 Nfd7 13.Nd5 Nxe5 14.Nxe7+ Kh8 15.Nxe5 Bxe5 16.Rd5 Bf6 17.Rxc5 Bxe7 18.Rc7 Bd6 19.Rxf7 Rxf7 20.Bxf7 Be5 21.Bh6 Bf5 22.Bd5 Bxb2 23.Re1 Bd7 24.Bxb7 Re8 25.Rxe8+ Bxe8 26.Bd5 Bb5 27.g3 a6 28.Kg2 Be2 29.Be6 Bd4 30.g4 Be5 31.f4 Bd4 32.Kg3 Be3 33.Kh4 Bf2+ 34.Kg5 Bd4 35.Bf8 Bg7 36.Bxg7+ Kxg7 37.f5 h6 38.Kf4 Kf6 39.h4 g5 40.hxg5+ hxg5 41.Kg3 Ke5 42.Bc8 Bc4 43.a3 a5 44.Kf3 Bf7 45.Bb7 Bg8 46.Ke3 Bf7 47.Bf3 Bg8 48.Kd3 Ba2 49.Be2 Bd5 50.Bd1 Bg8 51.Kc3 Bf7 52.Bb3 Be8 53.Bd1 Bf7 54.Bf3 Ba2 55.Bc6 Bg8 56.Bb5 Bd5 57.Be2 Bf7 58.Bc4 Be8 59.Kb3 Bc6 60.Be2 a4 61.Kb4 Kf6 62.Kc5 Be8 63.Bb5 1-0"
  },
  {
    "id": "TAL-04",
    "name": "Vladimir Sergeevich Saigin vs Mikhail Tal",
    "white": "Vladimir Sergeevich Saigin",
    "black": "Mikhail Tal",
    "event": "Match for USSR Master title",
    "site": "Riga",
    "date": "1954.??.??",
    "year": 1954,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "A61",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 Nf6 2.c4 c5 3.d5 e6 4.Nc3 exd5 5.cxd5 d6 6.Nf3 g6 7.Bg5 Bg7 8.e3 O-O 9.Nd2 a6 10.a4 Qc7 11.Be2 Nbd7 12.O-O Rb8 13.Qc2 Re8 14.e4 Nb6 15.Rae1 Bd7 16.Bf4 Nc8 17.h3 b5 18.axb5 axb5 19.Bg3 b4 20.Nd1 b3 21.Qb1 Rb4 22.Nc3 Nb6 23.Bd1 Bc8 24.Be2 c4 25.Qa1 Qc5 26.Bf4 Nfd7 27.Be3 Qc7 28.Qa5 Bxc3 29.bxc3 Ra4 30.Qb5 Re7 31.Ra1 Ba6 32.Qc6 Qxc6 33.dxc6 Nc5 34.Rxa4 Nbxa4 35.Bxc4 Bxc4 36.Nxc4 Nxc3 37.Ra1 N5a4 38.Bd4 Ne2+ 39.Kf1 Nxd4 40.Rxa4 Nxc6 41.Ra6 Rc7 42.Nxd6 b2 43.Rb6 Nb4 44.Rxb4 Rc1+ 45.Ke2 b1=Q 46.Rxb1 Rxb1 47.Nc4 1/2-1/2"
  },
  {
    "id": "TAL-05",
    "name": "Mikhail Tal vs Janis Visockis",
    "white": "Mikhail Tal",
    "black": "Janis Visockis",
    "event": "URS-chT Juniors",
    "site": "Leningrad URS",
    "date": "1954.08.??",
    "year": 1954,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "E87",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 d6 5.f3 O-O 6.Be3 e5 7.d5 Ne8 8.Qd2 f5 9.O-O-O f4 10.Bf2 Nd7 11.Nge2 Nb6 12.Qd3 g5 13.Kb1 Bd7 14.Nc1 c5 15.dxc6 bxc6 16.c5 Nc8 17.cxd6 Nexd6 18.Bc5 Rf6 19.Be2 Qc7 20.Nb3 Be8 21.Nd5 cxd5 22.Qxd5+ Nf7 23.Qxa8 Bc6 24.Bb6 axb6 25.Rc1 Bxa8 26.Rxc7 Rc6 27.Rc1 Rxc7 28.Rxc7 Ncd6 29.Nd2 Bf8 30.Bc4 b5 31.Be6 Kg7 32.a4 Kf6 33.Bxf7 Nxf7 34.axb5 Bb4 35.Nc4 g4 36.Ra7 gxf3 37.gxf3 Bxe4+ 38.fxe4 Ng5 39.b6 Bc5 40.Ra6 Ne6 41.b7 1-0"
  },
  {
    "id": "TAL-06",
    "name": "Isaac Lipnitsky vs Mikhail Tal",
    "white": "Isaac Lipnitsky",
    "black": "Mikhail Tal",
    "event": "4th Soviet Team-ch prelim",
    "site": "Voroshilovgrad URS",
    "date": "1955.09.??",
    "year": 1955,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "E65",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.Nf3 Nf6 2.d4 g6 3.c4 Bg7 4.g3 O-O 5.Bg2 d6 6.O-O c5 7.Nc3 Nc6 8.h3 cxd4 9.Nxd4 Nxd4 10.Qxd4 Be6 11.Qh4 Qc8 12.b3 Ng4 13.hxg4 Bxc3 14.Rb1 Bxg4 15.Qxe7 Qf5 16.Qe3 Bg7 17.Be4 Qh5 18.Bf3 Rfe8 19.Bxg4 Qxg4 20.Qf3 Qxf3 21.exf3 Re2 22.Rd1 Rxa2 23.Rxd6 Bf8 24.Rd5 a5 25.Be3 Rc8 26.Rbd1 Rb2 27.Rd8 Rxd8 28.Rxd8 Rb1+ 29.Kg2 Kg7 30.Bd4+ f6 31.Rd7+ Kg8 32.Rxb7 a4 33.c5 Rxb3 34.c6 Rxb7 35.cxb7 Bd6 36.Bxf6 Kf7 37.Bb2 g5 38.f4 gxf4 39.Bc1 fxg3 40.fxg3 Bb8 41.Bf4 Ba7 42.Be3 Bb8 43.Bf4 Ba7 44.Bc1 Ke6 45.Kf3 Kd7 46.Bf4 a3 47.b8=Q Bxb8 48.Bxb8 Ke6 49.Kf4 Kf6 50.Be5+ Kg6 51.Ba1 a2 52.Bb2 h5 53.Ba1 Kh6 1/2-1/2"
  },
  {
    "id": "TAL-07",
    "name": "Aivars Gipslis vs Mikhail Tal",
    "white": "Aivars Gipslis",
    "black": "Mikhail Tal",
    "event": "URS-ch qf",
    "site": "Vilnius URS",
    "date": "1955.05.??",
    "year": 1955,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B30",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c5 2.Nf3 Nc6 3.c3 d5 4.exd5 Qxd5 5.d4 Bg4 6.Be2 e6 7.O-O Nf6 8.h3 Bh5 9.Be3 cxd4 10.Nxd4 Bxe2 11.Qxe2 Nxd4 12.Bxd4 Be7 13.Nd2 O-O 14.Rfd1 Qf5 15.Bxf6 Bxf6 16.Ne4 Rab8 17.Rd7 a5 18.a4 Qe5 19.Nxf6+ gxf6 20.Qxe5 fxe5 21.Re1 f6 22.c4 Rf7 23.Red1 Rc8 24.b3 Rc7 25.Rxc7 Rxc7 26.Rd6 Kf7 27.Kf1 f5 28.g3 Kf6 29.Rb6 f4 30.gxf4 exf4 31.Ke2 Kf5 32.f3 Ke5 33.Kd3 Rd7+ 34.Kc3 Kf5 35.Rb5+ e5 36.Rxa5 Rd1 37.Rd5 Rh1 38.a5 Rxh3 39.Rd3 Rg3 40.Kd2 Rg2+ 41.Ke1 Rb2 42.c5 h5 43.Kf1 h4 44.Rc3 h3 45.Kg1 e4 46.a6 e3 47.axb7 Rb1+ 48.Kh2 e2 49.Re3 fxe3 50.b8=Q Rh1+ 51.Kxh1 e1=Q+ 52.Kh2 Qf2+ 53.Kxh3 Qxf3+ 54.Kh2 e2 55.Qf8+ Ke4 56.Qe8+ Kd3 57.Qb5+ Kc3 0-1"
  },
  {
    "id": "TAL-08",
    "name": "Mikhail Tal vs Vladimir Alexandrovich Soloviev",
    "white": "Mikhail Tal",
    "black": "Vladimir Alexandrovich Soloviev",
    "event": "URS-ch sf",
    "site": "Riga URS",
    "date": "1955.??.??",
    "year": 1955,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "E86",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 O-O 5.Be3 d6 6.f3 e5 7.Nge2 c6 8.Qb3 Nbd7 9.O-O-O Qe7 10.Kb1 Re8 11.g4 a6 12.Ng3 Nf8 13.d5 N6d7 14.h4 c5 15.Be2 Rb8 16.Rdg1 b5 17.h5 b4 18.Na4 Nb6 19.Nxb6 Rxb6 20.Qd3 Rb7 21.Qd2 f6 22.hxg6 Nxg6 23.Nf5 Bxf5 24.gxf5 Nf4 25.Bd1 Kh8 26.Qh2 h6 27.Bxf4 exf4 28.Rg6 Qf8 29.Qxf4 Kh7 30.Ba4 Rd8 31.Rhg1 Ra7 32.Qh4 Rc7 33.Bd1 Re8 34.Be2 a5 35.Bf1 a4 36.Bh3 a3 37.b3 Rf7 38.Bg4 Ra7 39.Bh5 Rd8 40.Rxg7+ Rxg7 41.Bg6+ Kg8 42.Qxh6 Qe7 43.Bh7+ Kh8 44.Bg6+ Kg8 45.Kc1 Rd7 46.Rh1 Kf8 47.f4 Rc7 48.Kd2 Rd7 49.Kd3 Rc7 50.Re1 Kg8 51.e5 dxe5 52.fxe5 fxe5 53.Rh1 1-0"
  },
  {
    "id": "TAL-09",
    "name": "Mikhail Tal vs Josif Israel Zilber",
    "white": "Mikhail Tal",
    "black": "Josif Israel Zilber",
    "event": "Riga Pioneer Palace Championship",
    "site": "Riga URS",
    "date": "1949.??.??",
    "year": 1949,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C07",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 e6 2.d4 d5 3.Nd2 c5 4.exd5 Qxd5 5.Ngf3 Nc6 6.Bc4 Qh5 7.dxc5 Bxc5 8.Ne4 Nge7 9.Bg5 Qg4 10.Qd3 b6 11.O-O-O O-O 12.Bf6 Qf4+ 13.Kb1 gxf6 14.g3 Qh6 15.g4 Qf4 16.g5 fxg5 17.Nfxg5 Ng6 18.h4 Nb4 19.Qh3 e5 20.Qg2 Bf5 21.h5 Kg7 22.hxg6 h6 23.Bxf7 Rxf7 24.gxf7 hxg5 25.Nxg5 Qxf2 26.Ne6+ Kxf7 27.Qg7+ Kxe6 28.Rh6+ Bg6 29.Qxg6+ Ke7 30.Rh7+ Kf8 31.Qg7+ Ke8 32.Qd7+ Kf8 33.Rh8# 1-0"
  },
  {
    "id": "TAL-10",
    "name": "Mikhail Tal vs Karlis Klasup",
    "white": "Mikhail Tal",
    "black": "Karlis Klasup",
    "event": "Riga",
    "site": "Riga",
    "date": "1952.??.??",
    "year": 1952,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "A80",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 f5 2.e4 fxe4 3.Nc3 Nf6 4.f3 d5 5.fxe4 dxe4 6.Bc4 Bf5 7.Nge2 Nc6 8.O-O e6 9.Bb5 a6 10.Ba4 Qd7 11.Bg5 O-O-O 12.Kh1 Be7 13.Bxf6 Bxf6 14.d5 exd5 15.Bxc6 bxc6 16.Nd4 Bg4 17.Qd2 Qd6 18.Nb3 c5 19.h3 h5 20.Na5 e3 21.Qxe3 d4 22.Qe4 dxc3 23.Qb7+ Kd7 24.Nc4 Qd4 25.Nb6+ Ke8 26.bxc3 Qd6 27.Nc4 Qg3 28.Rae1+ Kf8 29.Re3 Qh4 30.Qxc7 Kg8 31.Kg1 Bc8 32.Rf4 Qg5 33.Ref3 Kh7 34.h4 Rd1+ 35.Kh2 Qd5 36.Rg3 Bg4 37.Rxf6 Qxc4 38.Rf5 Kh6 39.Rxh5+ Kxh5 40.Qxg7 Qf4 41.Qxh8+ Kg6 42.Qg8+ Kf5 43.Qc8+ Ke5 44.Qxg4 Qxg4 45.Rxg4 Rd2 46.Rg5+ Kf4 47.Rxc5 Rxc2 48.Rc6 Kg4 49.Rc4+ Kh5 50.a4 Rd2 51.Kh3 Rd3+ 52.g3 Rd6 53.a5 Rd5 54.g4 Kg6 55.Rc6+ Kf7 56.Rxa6 Rd3+ 57.Kg2 Rxc3 58.h5 Kg7 59.Rb6 Ra3 60.a6 Kf7 61.Kh2 Kg7 62.g5 Ra5 63.Rb7+ Kg8 64.a7 1-0"
  },
  {
    "id": "TAL-11",
    "name": "Mikhail Tal vs Mark Pasman",
    "white": "Mikhail Tal",
    "black": "Mark Pasman",
    "event": "Latvian championship",
    "site": "Riga URS",
    "date": "1953.??.??",
    "year": 1953,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B93",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.f4 e5 7.Nf3 Nbd7 8.Bd3 Be7 9.O-O O-O 10.Kh1 b5 11.a3 Qc7 12.fxe5 dxe5 13.Nh4 Nc5 14.Bg5 Qd8 15.Nf5 Bxf5 16.Rxf5 Nfd7 17.Bxe7 Qxe7 18.Nd5 Qd6 19.Qg4 g6 20.Raf1 f6 21.h4 Kh8 22.R5f3 f5 23.exf5 Qxd5 24.fxg6 Rxf3 25.g7 Kg8 26.Bxh7+ Kxh7 27.Rxf3 Ne4 28.h5 Ndf6 29.Qg6+ Kg8 30.h6 Ra7 31.Kh2 Re7 32.Rh3 Nh7 33.Rd3 Qa8 34.Qxe4 Qxe4 35.Rd8+ Kf7 36.g8=Q+ Kf6 37.Rd6+ Kf5 38.Qg6+ Kf4 39.g3+ Ke3 40.Rd3+ Qxd3 1-0"
  },
  {
    "id": "TAL-12",
    "name": "Isaak Birbrager vs Mikhail Tal",
    "white": "Isaak Birbrager",
    "black": "Mikhail Tal",
    "event": "Team Championship of USSR, juniors",
    "site": "Kharkov",
    "date": "1953.??.??",
    "year": 1953,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "A70",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 Nf6 2.c4 c5 3.d5 e6 4.Nc3 exd5 5.cxd5 d6 6.e4 g6 7.Nf3 Bg7 8.Bd3 O-O 9.O-O Na6 10.Nd2 Nb4 11.Be2 Re8 12.a3 Na6 13.Re1 Nc7 14.Qc2 Rb8 15.a4 b6 16.Nb5 a6 17.Nxc7 Qxc7 18.Ra2 Qe7 19.f3 Nh5 20.Nf1 f5 21.Bd3 f4 22.g4 Bd4+ 23.Kh1 Qh4 24.Re2 Qh3 25.Rg2 Qxf3 26.Nd2 Qe3 27.Nf1 Qf3 28.Nd2 Bxg4 29.Nxf3 Bxf3 30.h4 Rf8 31.Be2 Ng3+ 32.Kh2 Bxg2 33.Kxg2 Nxe2 34.Qxe2 f3 35.Qxf3 Rxf3 36.Kxf3 Rf8+ 37.Kg3 Be5+ 38.Kg2 Bf4 0-1"
  },
  {
    "id": "TAL-13",
    "name": "Mikhail Tal vs Straume",
    "white": "Mikhail Tal",
    "black": "Straume",
    "event": "Riga",
    "site": "Riga",
    "date": "1953.??.??",
    "year": 1953,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C84",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.d4 exd4 7.e5 Nd5 8.Nxd4 Nxd4 9.Qxd4 Nb6 10.Qg4 Nxa4 11.Qxg7 Rf8 12.Bh6 d5 13.Qxh7 Bd7 14.Nd2 Bb5 15.c4 dxc4 16.Ne4 Nxb2 17.Bxf8 Bxf8 18.Nf6+ Ke7 19.Rfe1 Qd4 20.Re4 Qc5 21.e6 Kd6 22.e7+ Kc6 23.Qxf7 Bxe7 24.Rxe7 Kb6 25.Rxc7 Qd4 26.Rxb7+ Ka5 27.Qd5 Rd8 28.Qxd4 Rxd4 29.h3 c3 30.Rc1 Kb4 31.Re7 Bc4 32.Ne4 Rd1+ 33.Rxd1 Nxd1 34.Nxc3 Nxc3 35.h4 Bxa2 36.h5 a5 37.h6 Bb1 38.h7 Bxh7 39.Rxh7 a4 40.g4 a3 41.Ra7 Na4 42.Rb7+ Kc3 43.Rb1 Kc2 44.Re1 Nc3 45.g5 a2 46.g6 Nb1 47.g7 a1=Q 48.g8=Q Kd2 49.Qe6 Qg7+ 50.Kf1 Nc3 51.Qe3+ Kc2 52.Rc1+ Kb3 53.Qxc3+ Qxc3 54.Rxc3+ Kxc3 55.Ke2 Kd4 56.Kf3 Ke5 57.Kg4 Kf6 58.Kf4 1-0"
  },
  {
    "id": "TAL-14",
    "name": "Vladimir Sergeevich Saigin vs Mikhail Tal",
    "white": "Vladimir Sergeevich Saigin",
    "black": "Mikhail Tal",
    "event": "Match for USSR Master title",
    "site": "Riga",
    "date": "1954.??.??",
    "year": 1954,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "A31",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 Nf6 2.c4 c5 3.Nf3 e6 4.g3 cxd4 5.Nxd4 d5 6.Bg2 e5 7.Nf3 d4 8.O-O Nc6 9.e3 Be7 10.exd4 exd4 11.Nbd2 Be6 12.Re1 O-O 13.b3 Qd7 14.Bb2 Rad8 15.a3 a5 16.Ne5 Nxe5 17.Rxe5 b6 18.Nf3 Bc5 19.Qd2 Ng4 20.Ree1 d3 21.Rf1 Qd6 22.Qc3 f6 23.Rad1 Rfe8 24.Rd2 Bf5 25.Ng5 Ne3 26.fxe3 Bxe3+ 27.Kh1 Bxd2 28.Qxd2 Re2 29.Qc3 Rxg2 0-1"
  },
  {
    "id": "TAL-15",
    "name": "Mikhail Tal vs Yuri Averbakh",
    "white": "Mikhail Tal",
    "black": "Yuri Averbakh",
    "event": "2nd Soviet Team Cup",
    "site": "Riga URS",
    "date": "1954.09.04",
    "year": 1954,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C47",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Nc3 Nf6 4.d4 exd4 5.Nd5 Nb4 6.Nxd4 Nxe4 7.Nf5 c6 8.Nxb4 Bxb4+ 9.c3 Qf6 10.Qf3 Nxc3 11.a3 Ba5 12.Bd2 d5 13.Ng3 Qe6+ 14.Qe3 d4 15.Qxe6+ Bxe6 16.f3 O-O-O 17.Kf2 Bb6 18.bxc3 dxc3 19.Be3 Bxe3+ 20.Kxe3 Rhe8 21.Ne4 Bd5 22.g4 Bxe4 23.fxe4 Rd5 24.Rc1 g6 25.Bg2 f5 26.gxf5 gxf5 27.Rhf1 fxe4 28.Rxc3 Rh5 29.Rh1 Rh4 30.Rc4 Kc7 31.Rxe4 Rexe4+ 32.Bxe4 Rh3+ 33.Bf3 c5 34.Rg1 b5 35.Rg7+ Kb6 36.Rb7+ Ka6 37.Rb8 Rxh2 38.Be2 Rh3+ 39.Ke4 c4 40.a4 Rh4+ 1-0"
  },
  {
    "id": "TAL-16",
    "name": "Mikhail Tal vs Vladimir Simagin",
    "white": "Mikhail Tal",
    "black": "Vladimir Simagin",
    "event": "USSR Championship",
    "site": "Leningrad URS ",
    "date": "1956.01.14",
    "year": 1956,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B07",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c6 2.d4 d6 3.Nc3 Nf6 4.f4 Qb6 5.Nf3 Bg4 6.Be2 Nbd7 7.e5 Nd5 8.O-O Nxc3 9.bxc3 e6 10.Ng5 Bxe2 11.Qxe2 h6 12.Nxf7 Kxf7 13.f5 dxe5 14.fxe6+ Kxe6 15.Rb1 Qxb1 16.Qc4+ Kd6 17.Ba3+ Kc7 18.Rxb1 Bxa3 19.Qb3 Be7 20.Qxb7+ Kd6 21.dxe5+ Nxe5 22.Rd1+ Ke6 23.Qb3+ Kf5 24.Rf1+ Ke4 25.Re1+ Kf5 26.g4+ Kf6 27.Rf1+ Kg6 28.Qe6+ Kh7 29.Qxe5 Rhe8 30.Rf7 Bf8 31.Qf5+ Kg8 32.Kf2 Bc5+ 33.Kg3 Re3+ 34.Kh4 Rae8 35.Rxg7+ Kxg7 36.Qxc5 R8e6 37.Qxa7+ Kg6 38.Qa8 Kf6 39.a4 Ke5 40.a5 Kd5 41.Qd8+ Ke4 42.a6 Kf3 43.a7 Re2 44.Qd3+ R6e3 45.Qxe3+ 1-0"
  },
  {
    "id": "TAL-17",
    "name": "Mikhail Tal vs Alexander Kazimirovich Tolush",
    "white": "Mikhail Tal",
    "black": "Alexander Kazimirovich Tolush",
    "event": "USSR Championship",
    "site": "Leningrad URS ",
    "date": "1956.02.06",
    "year": 1956,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B97",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 a6 6.Bg5 e6 7.f4 Qb6 8.Qd2 Qxb2 9.Rb1 Qa3 10.e5 dxe5 11.fxe5 Nfd7 12.Ne4 Qxa2 13.Rb3 Qa1+ 14.Kf2 Qa4 15.Bb5 axb5 16.Nxb5 f6 17.exf6 gxf6 18.Re1 Ra6 19.Bxf6 Nxf6 20.Nxf6+ Kf7 21.Rf3 Qh4+ 22.Kf1 e5 23.Qd5+ Be6 24.Nd7 Kg6 25.Nxe5+ Kg7 26.Rg3+ Qxg3 27.Qxb7+ Nd7 28.hxg3 Rb6 29.Qc7 Bc5 30.Nxd7 Bc4+ 31.Re2 1-0"
  },
  {
    "id": "TAL-18",
    "name": "Mikhail Tal vs Vladimir Antoshin",
    "white": "Mikhail Tal",
    "black": "Vladimir Antoshin",
    "event": "USSR Championship",
    "site": "Moscow URS",
    "date": "1957.01.30",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "C92",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 d6 8.c3 O-O 9.h3 Nd7 10.d4 Nb6 11.Be3 exd4 12.cxd4 d5 13.Nc3 dxe4 14.Nxe4 Bf5 15.d5 Na5 16.d6 cxd6 17.Bxb6 Qxb6 18.Ng3 Be6 19.Bxe6 fxe6 20.Rxe6 Bf6 21.Rxd6 Qb8 22.Nh5 Bxb2 23.Rb1 Nc4 24.Rd7 Ra7 25.Rxb2 Nxb2 26.Qd5+ Kh8 27.Qd4 Rxd7 28.Qxd7 Rg8 29.Ng5 h6 30.Nf7+ Kh7 31.h4 Qc8 32.Nf6+ Kg6 33.Nxg8 Qxd7 34.Ne5+ Kh7 35.Nxd7 Kxg8 36.Nc5 a5 37.Kf1 Nd1 38.Ne4 b4 39.Ke2 Nb2 40.Kd2 a4 1/2-1/2"
  },
  {
    "id": "TAL-19",
    "name": "Mikhail Tal vs Lev Aronin",
    "white": "Mikhail Tal",
    "black": "Lev Aronin",
    "event": "USSR Championship",
    "site": "Moscow URS",
    "date": "1957.02.15",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "D32",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 d5 2.c4 e6 3.Nc3 c5 4.e3 Nf6 5.Nf3 Nc6 6.a3 Bd6 7.dxc5 Bxc5 8.b4 Bd6 9.Bb2 O-O 10.Qc2 Ne5 11.O-O-O Qe7 12.Nb5 Ned7 13.Nxd6 Qxd6 14.Qc3 Re8 15.g4 Qf8 16.Bd3 Nb6 17.g5 Na4 18.Qc2 Nxb2 19.Kxb2 dxc4 20.gxf6 cxd3 21.Qxd3 e5 22.Ng5 g6 23.h4 Bf5 24.e4 Bg4 25.h5 Rad8 26.hxg6 Rxd3 27.Rxd3 hxg6 28.Rh7 Rc8 29.f3 Rc6 30.Rxf7 Qxf7 31.Nxf7 Kxf7 32.fxg4 Kxf6 33.Rd7 Rb6 34.Kc3 Kg5 35.a4 a6 36.Kc4 Kxg4 1/2-1/2"
  },
  {
    "id": "TAL-20",
    "name": "Mikhail Tal vs Alexander Kazimirovich Tolush",
    "white": "Mikhail Tal",
    "black": "Alexander Kazimirovich Tolush",
    "event": "USSR Championship",
    "site": "Moscow URS",
    "date": "1957.02.21",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "E80",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.c4 Nf6 2.Nc3 g6 3.e4 d6 4.d4 Bg7 5.f3 e5 6.Nge2 Nbd7 7.Bg5 c6 8.Qd2 O-O 9.d5 c5 10.g4 a6 11.Ng3 Re8 12.h4 Qa5 13.Bh6 Nf8 14.h5 Qc7 15.Bd3 b5 16.O-O-O bxc4 17.Bb1 Bh8 18.Rdg1 Rb8 19.Nf5 N6d7 20.Bg5 Bg7 21.Nxg7 Kxg7 22.Bh6+ Kg8 23.f4 exf4 24.Qxf4 Qd8 25.hxg6 Nxg6 26.Qh2 Nde5 27.Bf4 Nf8 28.Qh6 Neg6 29.Bg5 f6 30.e5 Rxe5 31.Bxg6 Rb7 32.Ne4 fxg5 33.Rf1 Rxe4 34.Bxe4 Rg7 35.Rf6 Bxg4 36.Rhf1 Nd7 37.Rxd6 Qe7 38.Rxa6 Kh8 39.Bxh7 Nb8 40.Bf5 Kg8 41.Be6+ Bxe6 42.Rxe6 1-0"
  },
  {
    "id": "TAL-21",
    "name": "Abram Khasin vs Mikhail Tal",
    "white": "Abram Khasin",
    "black": "Mikhail Tal",
    "event": "USSR Championship",
    "site": "Leningrad URS ",
    "date": "1956.01.12",
    "year": 1956,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "B88",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c5 2.Nf3 Nc6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 d6 6.Bc4 e6 7.O-O a6 8.Be3 Qc7 9.Bb3 Be7 10.f4 b5 11.f5 Nxd4 12.Qxd4 O-O 13.fxe6 Bxe6 14.Rad1 Rac8 15.Kh1 Rfd8 16.Nd5 Bxd5 17.exd5 Nd7 18.Qf4 Bf6 19.Bd4 Re8 20.c3 Re7 21.Bc2 Bxd4 22.Qh4 Nf8 23.Rxd4 Re2 24.Bf5 Rce8 25.Rb4 Rd2 26.Be4 Qe7 27.Qe1 Rxd5 28.Qf2 Re5 29.Bd3 Nd7 30.Rf4 Nc5 31.Rxf7 Nxd3 32.Qf3 Re1 33.Qd5 Qxf7 34.Qxf7+ Kh8 35.Kg1 Rxf1+ 36.Qxf1 Re1 0-1"
  },
  {
    "id": "TAL-22",
    "name": "Mikhail Tal vs Borislav Ivkov",
    "white": "Mikhail Tal",
    "black": "Borislav Ivkov",
    "event": "Uppsala",
    "site": "Uppsala SWE",
    "date": "1956.04.??",
    "year": 1956,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "C97",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3 O-O 8.c3 d6 9.h3 Na5 10.Bc2 c5 11.d4 Qc7 12.Nbd2 Bd7 13.Nf1 Rfe8 14.Ne3 g6 15.b4 cxb4 16.cxb4 Nc4 17.Nxc4 bxc4 18.Re3 Bf8 19.Bb2 Bh6 20.Ra3 Qb7 21.dxe5 Qxb4 22.Qd4 Nh5 23.exd6 Bg7 24.e5 Bc6 25.Re3 Bxf3 26.Bc3 Qb5 27.gxf3 Rad8 28.f4 Re6 29.Rb1 Qc6 30.Rb6 Qc8 31.Bd1 Bh6 32.Bxh5 gxh5 33.f5 1-0"
  },
  {
    "id": "TAL-23",
    "name": "Alexander Koblents vs Mikhail Tal",
    "white": "Alexander Koblents",
    "black": "Mikhail Tal",
    "event": "Riga",
    "site": "Riga",
    "date": "1957.??.??",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1/2-1/2",
    "eco": "A97",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 e6 2.c4 f5 3.Nf3 Nf6 4.g3 Be7 5.Bg2 O-O 6.O-O d6 7.Nc3 Qe8 8.Re1 Qg6 9.e4 fxe4 10.Nxe4 Nxe4 11.Rxe4 Nc6 12.Re3 Bf6 13.d5 exd5 14.cxd5 Ne5 15.Nxe5 Bxe5 16.Rb3 Bf5 17.Rxb7 Bc2 18.Qd2 Rae8 19.Rxc7 Bd3 20.Qb4 a5 21.Qa4 Bxg3 22.hxg3 Re1+ 23.Kh2 Be4 24.Be3 Qh5+ 25.Bh3 Rxe3 26.Rxg7+ Kxg7 27.Qd4+ Kg8 28.Qxe3 Bf5 29.g4 Bxg4 30.Rg1 Rxf2+ 31.Kh1 Qxd5+ 32.Bg2 Qh5+ 33.Bh3 Qd5+ 34.Bg2 Qd2 35.Qxd2 Rxd2 36.Bf3 h5 37.Bxg4 hxg4 38.Rxg4+ Kf7 39.Ra4 1/2-1/2"
  },
  {
    "id": "TAL-24",
    "name": "Lev Aronson vs Mikhail Tal",
    "white": "Lev Aronson",
    "black": "Mikhail Tal",
    "event": "USSR Championship",
    "site": "Moscow URS",
    "date": "1957.01.21",
    "year": 1957,
    "era": "Soviet Era",
    "result": "0-1",
    "eco": "A97",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.d4 e6 2.c4 f5 3.Nf3 Nf6 4.Nc3 Be7 5.g3 O-O 6.Bg2 d6 7.O-O Qe8 8.Re1 Qg6 9.e4 fxe4 10.Nxe4 Nxe4 11.Rxe4 Nc6 12.Qe2 Bf6 13.Bd2 e5 14.dxe5 dxe5 15.Bc3 Bf5 16.Nh4 Bxh4 17.Rxh4 Rae8 18.Qe3 h6 19.b4 Qf6 20.b5 Nd8 21.Bd5+ Kh8 22.f4 exf4 23.Qd2 Qb6+ 24.Bd4 Qg6 25.Qxf4 Kh7 26.Qxc7 Bb1 27.Be5 Ne6 28.Qd6 Qf5 29.Bf4 Ng5 30.Qb4 Be4 31.Bxe4 Rxe4 32.Rf1 Re2 33.Qd6 Rxa2 34.Qd5 Qc2 35.c5 Rd8 36.Bd6 Re8 0-1"
  },
  {
    "id": "TAL-25",
    "name": "Mikhail Tal vs Konstantin Klaman",
    "white": "Mikhail Tal",
    "black": "Konstantin Klaman",
    "event": "USSR Championship",
    "site": "Moscow URS",
    "date": "1957.02.11",
    "year": 1957,
    "era": "Soviet Era",
    "result": "1-0",
    "eco": "B61",
    "opening": "",
    "variation": "",
    "sourceBook": "The Life and Games of Mikhail Tal",
    "pgn": "1.e4 c5 2.Nf3 Nc6 3.d4 cxd4 4.Nxd4 Nf6 5.Nc3 d6 6.Bg5 Bd7 7.Qd2 Nxd4 8.Qxd4 Qa5 9.Bxf6 gxf6 10.O-O-O Rc8 11.f4 Rg8 12.g3 e6 13.Bh3 Qc5 14.Qd2 b5 15.Rhe1 b4 16.Ne2 Qc4 17.Kb1 Qxe4 18.Nd4 Qb7 19.Qd3 Be7 20.Qxh7 Rf8 21.Bg4 Qc7 22.Ka1 f5 23.Bxf5 exf5 24.Rxe7+ Kxe7 25.Re1+ Kd8 26.Qh4+ f6 27.Qh6 Qa5 28.Nb3 {not 28.Qxf8+? Kc7 29.Qxf6 b3! -+} Qd5 29.Qxf8+ Kc7 30.Qxf6 Re8 31.Rc1 Ba4 32.Qd4 Qb7 33.Rd1 Re6 34.Qc4+ 1-0"
  }
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment