ChessBuilder/Systems/Game/ChessGame.gd

876 lines
28 KiB
GDScript

class_name ChessGame extends Control
# Constants
const PieceContainer = preload("res://Systems/PieceContainer.gd")
const WHITE = "white"
const BLACK = "black"
const StockfishController = preload("res://Systems/FairyStockfish/StockfishClient.gd")
# Signals
signal tile_pressed(location: String)
signal send_location(location: String)
signal turn_changed
signal game_initialized
# Game state variables
var currentPlayer: String = WHITE
var board: Array
var isWhiteToMove: bool = true
var castlingRights: String = "KQkq"
var enPassantTarget: String = "-"
var halfMoveClock: int = 0
var moveCount: int = 1
var currentHand: Array
var selectedNode: String = ""
var locationX: String = ""
var locationY: String = ""
var areas: PackedStringArray
var specialArea: PackedStringArray
var gamecheckMate: bool = false
var gamedraw: bool = false
var hasMoved: bool = false
var currentlyMovingPiece = null
var p1Points: int = 0
var p2Points: int = 0
var Turn: int = 0
var stockfishController: StockfishController
var stockfishPath = "res://Assets/ChessEngines/stockfish/stockfish.exe"
var currentFen = ""
var lightStyle = null
var darkStyle = null
var highlightStyle = null
var cpuElo = 1500
var is_initialized: bool = false
# Node references
@onready var turnIndicator: ColorRect = $TurnIndicator
@onready var p1String: RichTextLabel = $Player1Points
@onready var p2String: RichTextLabel = $Player2Points
@onready var gold: int = 1000
@onready var deckManager: DeckManager
@onready var tileManager: TileManager
@onready var cameraController: CameraController
@onready var cardDisplay: CardDisplay
@onready var cardPreview: CardPreview
@onready var boardContainer: FlowContainer = $Flow
@onready var stateMachine: StateMachine = $StateMachine
@onready var menuContainer = get_node_or_null("/root/Board/MenuContainer")
# Export parameters
@export var boardXSize = 12
@export var boardYSize = 12
@export var tileXSize: int = 50
@export var tileYSize: int = 50
@export var windowXSize: int = 1280
@export var windowYSize: int = 720
@export var FEN: String = "2rnbqkbnr2/2pppppppp2/6u5/12/2********U1/12/2u9/12/7U4/12/2PPPPPPPP2/2RNBQKBNR2 w KQkq - 0 1"
# Standard chess FEN: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
# Modified FEN: "2rnbqkbnr2/2pppppppp2/12/12/2********2/12/12/12/12/12/2PPPPPPPP2/2RNBQKBNR2 w KQkq - 0 1"
# ===========================================================================
# INITIALIZATION FUNCTIONS
# ===========================================================================
func _ready() -> void:
print("ChessGame _ready() called")
# Only set up paths and window size initially
if OS.get_name() == "Windows":
stockfishPath = "res://Assets/ChessEngines/stockfish/stockfish.exe"
else:
stockfishPath = ProjectSettings.globalize_path("res://Assets/ChessEngines/Fairy-Stockfish/src/stockfish")
add_to_group("ChessGame")
currentFen = FEN
DisplayServer.window_set_size(Vector2i(windowXSize, windowYSize))
# Set up the menu signal connection
if menuContainer:
print("Found MenuContainer, connecting signal")
if !menuContainer.is_connected("new_game_requested", Callable(self, "_on_new_game_requested")):
print("Signal Connected")
menuContainer.connect("new_game_requested", Callable(self, "_on_new_game_requested"))
else:
print("MenuContainer not found, will initialize game now")
call_deferred("initialize_game_system")
turnIndicator.visible = false
deckManager = DeckManager.new()
# 2rnbqkbnr1R/2ppp1pppp2/5p6/75/66/66/66/66/66/66/2PPPPPPPP2/2RNBQKBN3 b KQkq - 0 3
func _on_new_game_requested(options = {}):
print("ChessGame received new_game_requested signal ", is_initialized)
turnIndicator.visible = true
if is_initialized:
resetBoard()
else:
initialize_game_system()
func initialize_game_system():
print("Initializing game system")
# Set up basic styles first
setupStyles()
# Initialize the game components
initializeGame()
initializeTiles()
# Initialize Stockfish controller
stockfishController = StockfishController.new()
add_child(stockfishController)
stockfishController.connect_to_engine(stockfishPath, self)
# Start the state machine
if stateMachine:
stateMachine.start()
stateMachine.transitionToNextState(Constants.WHITE_TURN)
# Mark as initialized
is_initialized = true
# Emit signal that game is initialized
emit_signal("game_initialized")
func _exit_tree():
# Clean up the Stockfish controller when exiting
if stockfishController:
stockfishController.disconnect_engine()
func initializeGame() -> void:
# Initialize all game components in the correct order
setupStyles()
initializeDeckSystem()
initializeCardPreview()
initializeBoard()
setupCameraController()
setupUI()
func initializeTiles() -> void:
# Initialize tile manager and setup tiles
tileManager = TileManager.new($Flow, self)
add_child(tileManager)
await get_tree().process_frame
tileManager.initialize(boardContainer)
setupTilesFromFEN()
# tileManager.place_random_game_tiles()
func initializeBoard() -> void:
# Parse FEN to get board dimensions
var fen_parts = FEN.split(" ")
var rows = fen_parts[0].split("/")
boardYSize = rows.size()
boardXSize = 0
# Calculate width from first row by counting both pieces and numbers
for c in rows[0]:
if c.is_valid_int():
boardXSize += int(c) # Add the number of empty squares
else:
boardXSize += 1
# Initialize board array
board = []
for i in range(boardYSize):
var row = []
for j in range(boardXSize):
row.append(null)
board.append(row)
# Create the visual board and setup pieces
createBoard()
setupPiecesFromFEN()
# Set up signals for board interaction
if !boardContainer.has_user_signal("tile_pressed"):
boardContainer.add_user_signal("tile_pressed")
if !boardContainer.has_user_signal("send_location"):
boardContainer.add_user_signal("send_location")
func initializeCardPreview() -> void:
# Initialize the card preview component
cardPreview = CardPreview.new()
add_child(cardPreview)
func initializeDeckSystem() -> void:
deckManager.shuffleDeck()
deckManager.drawStartingHand()
# Initialize the deck manager and card display
cardDisplay = CardDisplay.new()
add_child(deckManager)
add_child(cardDisplay)
# Set up signals for card interaction
if !deckManager.has_user_signal("card_pressed"):
deckManager.add_user_signal("card_pressed")
if !deckManager.has_user_signal("hand_updated"):
deckManager.add_user_signal("hand_updated")
# Update card display with initial hand
cardDisplay.update_hand(deckManager.hand)
deckManager.connect("hand_updated", func(hand): cardDisplay.update_hand(hand))
func setupCameraController() -> void:
# Set up the camera controller for board navigation
cameraController = CameraController.new(boardContainer)
cameraController.name = "CameraController"
add_child(cameraController)
cameraController.zoom_changed.connect(_on_zoom_changed)
func _on_zoom_changed(zoom_level: float) -> void:
# manuall adjustements try to avoid
pass
func setupStyles() -> void:
lightStyle = StyleBoxFlat.new()
lightStyle.bg_color = Utils.LIGHT_CELL
darkStyle = StyleBoxFlat.new()
darkStyle.bg_color = Utils.DARK_CELL
highlightStyle = StyleBoxFlat.new()
highlightStyle.bg_color = Color(0, 0.3, 0, 1)
func setupUI() -> void:
p1String.text = "0"
p2String.text = "0"
updateTurnIndicator()
# Create control buttons
var zoom_in_button = Button.new()
zoom_in_button.text = "+"
zoom_in_button.size = Vector2(30, 30)
zoom_in_button.pressed.connect(func(): cameraController.zoom_in(zoom_in_button.global_position))
var zoom_out_button = Button.new()
zoom_out_button.text = "-"
zoom_out_button.size = Vector2(30, 30)
zoom_out_button.pressed.connect(func(): cameraController.zoom_out(zoom_out_button.global_position))
var reset_button = Button.new()
reset_button.text = "Reset View"
reset_button.size = Vector2(0, 30)
reset_button.pressed.connect(func(): cameraController.reset_view())
var control_container = VBoxContainer.new()
control_container.name = "ZoomControls"
control_container.position = Vector2(windowXSize - 100, windowYSize - 80)
var h_container = HBoxContainer.new()
h_container.add_child(zoom_out_button)
h_container.add_child(zoom_in_button)
control_container.add_child(h_container)
control_container.add_child(reset_button)
# Add the container to the scene
add_child(control_container)
func get_base_style(is_white: bool) -> StyleBoxFlat:
return lightStyle if is_white else darkStyle
# ===========================================================================
# BOARD CREATION AND SETUP
# ===========================================================================
func createBoard() -> void:
boardContainer.add_to_group("Flow")
var numberX = 0
var numberY = 0
var isWhite = true
print("CREATING BOARD X " + str(boardXSize) + " Y " + str(boardYSize))
while numberY != boardYSize:
boardContainer.size.y += tileYSize + 5
boardContainer.size.x += tileXSize + 5
while numberX != boardXSize:
createTile(numberX, numberY, isWhite)
isWhite = !isWhite
numberX += 1
isWhite = !isWhite
numberY += 1
numberX = 0
func createTile(x: int, y: int, isWhite: bool) -> void:
# Create a single tile for the chess board
var tile = PieceContainer.new(str(x) + "-" + str(y))
tile.set_custom_minimum_size(Vector2(tileXSize, tileYSize))
tile.add_theme_stylebox_override("normal", lightStyle if isWhite else darkStyle)
print(" Create Tile " + str(x) + "-" + str(y) )
tile.set_name(str(x) + "-" + str(y))
# tile.pressed.connect(func(): handleTileSelection(tile.name))
tile.pressed.connect(func():
# tile_pressed.emit(tile.name)
boardContainer.emit_signal("tile_pressed", tile.name)
)
boardContainer.add_child(tile)
func setupPiecesFromFEN() -> void:
# Set up chess pieces from the FEN string
var fen_parts = FEN.split(" ")
var rows = fen_parts[0].split("/")
# Iterate through rows in reverse to place black pieces at top
for y in range(rows.size()):
var x = 0
# Convert y coordinate to flip the board (boardY-y puts white at bottom)
var board_y = (boardYSize - 1) - y
for c in rows[y]:
if c.is_valid_int():
# Skip empty squares
x += int(c)
else:
var piece_info = getFENPieceInfo(c)
if piece_info.name != "":
placePiece(str(x) + "-" + str(board_y), piece_info.name, piece_info.color)
x += 1
func setupTilesFromFEN() -> void:
# Set up special tiles from the FEN string
var fen_parts = FEN.split(" ")
var rows = fen_parts[0].split("/")
var matchedPortals = {}
var portalCnt = 0;
# Iterate through rows in reverse to place black pieces at top
for y in range(rows.size()):
var x = 0
# Convert y coordinate to flip the board (7-y puts white at bottom)
var board_y = (boardYSize - 1) - y # For an 8x8 board
for c in rows[y]:
if c.is_valid_int():
# Skip empty squares
x += int(c)
else:
var loc = str(x) + "-" + str(board_y);
if tileManager.portalString.find(c) > -1:
var char = c
# shjould we lowercase?
if char in matchedPortals:
if !("p2" in matchedPortals[char]):
matchedPortals[char].p2 = loc
tileManager.place_portal_pair(matchedPortals[char].p1, matchedPortals[char].p2, portalCnt)
portalCnt += 1;
else:
matchedPortals[char] = { "p1": loc }
x += 1
else:
var tile = getFENTile(c, loc)
if tile != null:
tileManager.add_tile(loc, tile)
x += 1
func placePiece(position: String, pieceName: String, color: int) -> void:
print("Placing Piece")
var piece = summonPiece(pieceName, color)
var container = boardContainer.get_node(position) as PieceContainer
await container.set_piece(piece, false)
container.remove_piece(true)
container.set_piece(piece, false)
var coords = position.split("-")
board[int(coords[1])][int(coords[0])] = piece
func summonPiece(pieceName: String, color: int) -> Node:
# Create a chess piece based on its name and color
var piece
match pieceName:
"Pawn":
piece = Pawn.new(self)
piece.name = "Pawn"
"King":
piece = King.new(self)
piece.name = "King"
"Queen":
piece = Queen.new(self)
piece.name = "Queen"
"Knight":
piece = Knight.new(self)
piece.name = "Knight"
"Rook":
piece = Rook.new(self)
piece.name = "Rook"
"Bishop":
piece = Bishop.new(self)
piece.name = "Bishop"
piece.Item_Color = color
piece.position = Vector2(tileXSize / 2, tileYSize / 2)
return piece
# ===========================================================================
# GAME FLOW AND STATE MANAGEMENT
# ===========================================================================
func isPlayerTurn() -> bool:
return currentPlayer == WHITE
func _process(delta: float) -> void:
# Process the state machine
stateMachine.process(delta)
func _unhandled_input(event: InputEvent) -> void:
# Handle input events not handled by other nodes
stateMachine.unhandledInput(event)
if event is InputEventKey:
if event.pressed:
if event.keycode == KEY_EQUAL or event.keycode == KEY_PLUS: # Zoom in with + key
cameraController.zoom_in(get_viewport().get_mouse_position())
elif event.keycode == KEY_MINUS: # Zoom out with - key
cameraController.zoom_out(get_viewport().get_mouse_position())
elif event.keycode == KEY_R: # Reset view with R key
cameraController.reset_view()
func resetBoard() -> void:
clearSelection()
clearBoard()
setupPiecesFromFEN()
Turn = 0
currentPlayer = WHITE
p1Points = 0
p1String.text = str(p1Points)
p2Points = 0
p2String.text = str(p2Points)
gamecheckMate = false;
gamedraw = false;
deckManager.initializeStartingDeck()
areas.clear()
specialArea.clear()
updateTurnIndicator()
func clearBoard() -> void:
# Clear all pieces from the board
for child in boardContainer.get_children():
if child is PieceContainer:
child.remove_piece()
func updateTurnIndicator():
if Turn == 0: # White's turn
turnIndicator.color = Color(1, 1, 1, 1) # White
else: # Black's turn
turnIndicator.color = Color(0, 0, 0, 1) # Black
func cleanupPhase() -> void:
pass
func updateEffectDurations() -> void:
# Update the duration of active effects
deckManager.updateCardDurations()
tileManager.update_tile_durations()
func applyTileEffects() -> void:
# Apply effects from active tiles
tileManager.apply_alltile_effects()
func applyCardEffects() -> void:
pass
func evaluatePosition() -> Dictionary:
var status = {
"checkmate": isCheckmate(),
"draw": isDraw(),
}
return status
func isCheckmate() -> bool:
return gamecheckMate
func isDraw() -> bool:
return gamedraw
func endGame(reason: String) -> void:
pass
# ===========================================================================
# MOVE EXECUTION AND VALIDATION
# ===========================================================================
func executeMove(targetLocation: String) -> void:
print("executeMove ", targetLocation)
var targetContainer = get_node("Flow/" + targetLocation) as PieceContainer
var sourceContainer = get_node("Flow/" + selectedNode) as PieceContainer
var piece = sourceContainer.get_piece()
var old_location = selectedNode
# Handle capture if there's a piece in target location
if targetContainer.has_piece():
var capturedPiece = targetContainer.get_piece()
if Turn == 0:
p1Points += capturedPiece.Points
p1String.text = str(p1Points)
else:
p2Points += capturedPiece.Points
p2String.text = str(p2Points)
targetContainer.remove_piece() # This handles freeing the captured piece
# Move piece to new location
sourceContainer.remove_piece(true)
targetContainer.set_piece(piece)
hasMoved = true
currentlyMovingPiece = piece
resetHighlights()
func resolveMoveEffects() -> void:
# Resolve effects after a move is made
print("resolveMoveEffects", currentlyMovingPiece)
togglePieceChessEffect()
selectedNode = ""
Turn = 1 if Turn == 0 else 0
updateTurnIndicator()
resetHighlights()
hasMoved = false
currentlyMovingPiece = null
emit_signal("turn_changed")
func togglePieceChessEffect() -> void:
# Update piece-specific effects after a move
var piece = currentlyMovingPiece
if piece.name == "Pawn":
if piece.Double_Start:
piece.En_Passant = true
piece.Double_Start = false
elif piece.name == "King":
piece.Castling = false
elif piece.name == "Rook":
piece.Castling = false
func isValidMove(location: String) -> bool:
# Check if a move to the given location is valid
var node = get_node("Flow/" + location) as PieceContainer
var piece = node.get_piece()
if piece == null || piece.Item_Color != Turn:
for area in areas:
if area == node.name:
return true
return false
func clearSelection() :
# Clear the current selection
resetHighlights()
selectedNode = ""
if cardPreview:
cardPreview.hide_preview()
return
func getMovableAreas() -> void:
# Get all valid moves for the selected piece
print("HIGHLIGHTING getMovableAreas 1", selectedNode)
resetHighlights()
areas.clear()
specialArea.clear()
var container = get_node("Flow/" + selectedNode) as PieceContainer
var piece = container.get_piece()
if piece == null:
return
var piece_id = piece.get_instance_id()
# print("HIGHLIGHTING getMovableAreas 2")
if deckManager.attached_cards.has(piece_id):
var card = deckManager.attached_cards[piece_id]
cardPreview.show_card_preview(card)
if stateMachine.state.name == Constants.MOVEMENT:
var movement_state = stateMachine.state
if piece_id in movement_state.moves_remaining:
var moves_left = movement_state.moves_remaining[piece_id] - 1
cardPreview.update_moves_remaining(moves_left)
else:
cardPreview.hide_preview()
# print("HIGHLIGHTING getMovableAreas 3")
var moves = piece.getValidMoves(boardContainer, selectedNode)
areas = moves.regular_moves
specialArea = moves.special_moves
# print("HIGHLIGHTING getMovableAreas 4")
highlightValidMoves()
func highlightValidMoves() -> void:
for move in areas:
var button = boardContainer.get_node(move)
# If there's an active tile effect, combine with its current style instead
if tileManager && tileManager.get_tile(move):
var current_style = button.get_theme_stylebox("normal")
var highlightedStyle = StyleBoxFlat.new()
highlightedStyle.bg_color = current_style.bg_color + highlightStyle.bg_color
button.add_theme_stylebox_override("normal", highlightedStyle)
else:
# Default chess pattern highlighting
var isWhiteSquare = (int(move.split("-")[0]) + int(move.split("-")[1])) % 2 == 0
var baseStyle = lightStyle if isWhiteSquare else darkStyle
var combinedStyle = StyleBoxFlat.new()
combinedStyle.bg_color = baseStyle.bg_color + highlightStyle.bg_color
button.add_theme_stylebox_override("normal", combinedStyle)
func resetHighlights():
# Reset all highlights on the board
for button in boardContainer.get_children():
if !button.name.contains("-"):
continue
# Skip if this tile has an active effect
if tileManager && tileManager.get_tile(button.name):
continue
var coord = button.name.split("-")
var isWhiteSquare = (int(coord[0]) + int(coord[1])) % 2 == 0
if isWhiteSquare:
button.add_theme_stylebox_override("normal", lightStyle)
else:
button.add_theme_stylebox_override("normal", darkStyle)
# ===========================================================================
# CAPTURING AND PIECE MANAGEMENT
# ===========================================================================
func isNull(location: String) -> bool:
return get_node_or_null("Flow/" + location) == null
func updatePointsAndCapture(capturedPiece: Pawn, animate: bool = true) -> void:
# Update points and handle piece capture
if Turn == 0:
p1Points += capturedPiece.Points
p1String.text = str(p1Points)
else:
p2Points += capturedPiece.Points
p2String.text = str(p2Points)
if animate:
animatePieceCapture(capturedPiece)
if capturedPiece.name == "King":
print("Game Over!")
gamecheckMate = true
func animatePieceCapture(capturedPiece: Pawn) -> void:
# Animate the capture of a piece
var container = capturedPiece.get_parent() as PieceContainer
await capturedPiece.animate_capture()
container.remove_piece()
# ===========================================================================
# FEN NOTATION AND POSITION TRACKING
# ===========================================================================
func parseLocation(location: String) -> void:
# Parse a location string into X and Y coordinates
var number = 0
locationX = ""
while location.substr(number, 1) != "-":
locationX += location.substr(number, 1)
number += 1
locationY = location.substr(number + 1)
func getCurrentFen() -> String:
# Generate FEN string from the current board position
var fen = ""
# For a standard chess board, we want to generate FEN from top (black side, rank 8)
# to bottom (white side, rank 1)
for y in range(boardYSize):
var emptySquares = 0
for x in range(boardXSize):
# print("CHECKING ", str(x) + "-" + str(y))
var container = boardContainer.get_node(str(x) + "-" + str(y)) as PieceContainer
if tileManager.active_tiles.has(str(x) + "-" + str(y)):
var tile = tileManager.active_tiles[str(x) + "-" + str(y)]
if tile is WallTile or tile is DoubleWallTile:
if emptySquares > 0:
fen += str(emptySquares)
emptySquares = 0
if tile.tile_name == "Double Wall":
fen += "*"
else:
fen += "*"
elif tile is PortalTile:
var piece = container.get_piece()
if piece == null:
emptySquares += 1
else:
# Convert piece to FEN notation
var fenChar = getPieceFenChar(piece)
fen += fenChar
else:
var piece = container.get_piece()
if piece == null:
emptySquares += 1
else:
if emptySquares > 0:
fen += str(emptySquares)
emptySquares = 0
# Convert piece to FEN notation
var fenChar = getPieceFenChar(piece)
fen += fenChar
# Add any remaining empty squares at the end of the rank
if emptySquares > 0:
fen += str(emptySquares)
# Add rank separator (except for the last rank)
if y < boardYSize - 1:
fen += "/"
# Add the rest of the FEN string components
fen += " %s %s %s %d %d" % [
"b" if currentPlayer == BLACK else "w",
castlingRights,
enPassantTarget,
halfMoveClock,
moveCount
]
# fen += specialChars;
currentFen = fen;
return fen
func getFENPieceInfo(fen_char: String) -> Dictionary:
# Get piece information from a FEN character
var piece_info = {
"name": "",
"color": 0 # Black
}
# If uppercase, it's white
if fen_char.to_upper() == fen_char:
piece_info.color = 1 # White
# Map FEN characters to piece names
match fen_char.to_upper():
"P": piece_info.name = "Pawn"
"R": piece_info.name = "Rook"
"N": piece_info.name = "Knight"
"B": piece_info.name = "Bishop"
"Q": piece_info.name = "Queen"
"K": piece_info.name = "King"
return piece_info
func getFENTile(fen_char: String, location: String) -> Tile:
# Get tile information from a FEN character
var tile = null
var container = boardContainer.get_node(location) as PieceContainer
var is_white = (int(location.split("-")[0]) + int(location.split("-")[1])) % 2 == 0
# Map FEN characters to piece names
match fen_char.to_upper():
"*": tile = WallTile.new(container, is_white, -1)
return tile
func getPieceFenChar(piece: Pawn) -> String:
# Get FEN character from a piece
if piece == null:
return ""
var fenChar = ""
match piece.name:
"Pawn": fenChar = "p"
"Knight": fenChar = "n"
"Bishop": fenChar = "b"
"Rook": fenChar = "r"
"Queen": fenChar = "q"
"King": fenChar = "k"
# In our system, Item_Color == 1 is white, 0 is black
return fenChar.to_upper() if piece.Item_Color == 0 else fenChar
# Unused
func updateStateFromMove(fromIdx: int, toIdx: int) -> void:
# Update game state based on the move
isWhiteToMove = !isWhiteToMove
if isWhiteToMove:
moveCount += 1
# Update castling rights if needed
updateCastlingRights(fromIdx, toIdx)
# Update en passant target
updateEnPassantTarget(fromIdx, toIdx)
# Update halfmove clock
updateHalfMoveClock(fromIdx, toIdx)
func updateCastlingRights(fromIdx: int, toIdx: int) -> void:
var piece = board[fromIdx]
if piece == null:
return
# Remove castling rights when king or rook moves
match piece.type:
"king":
if piece.isWhite:
castlingRights = castlingRights.replace("K", "").replace("Q", "")
else:
castlingRights = castlingRights.replace("k", "").replace("q", "")
"rook":
var startRank = 7 if piece.isWhite else 0
if fromIdx == startRank * 8: # Queen-side rook
castlingRights = castlingRights.replace("Q" if piece.isWhite else "q", "")
elif fromIdx == startRank * 8 + 7: # King-side rook
castlingRights = castlingRights.replace("K" if piece.isWhite else "k", "")
if castlingRights == "":
castlingRights = "-"
func updateEnPassantTarget(fromIdx: int, toIdx: int) -> void:
var piece = board[fromIdx]
if piece == null or piece.type != "pawn":
enPassantTarget = "-"
return
# Check for double pawn move
var fromRank = fromIdx / 8
var toRank = toIdx / 8
if abs(fromRank - toRank) == 2:
var file = fromIdx % 8
var targetRank = (fromRank + toRank) / 2
enPassantTarget = "%s%d" % [char(97 + file), 8 - targetRank]
else:
enPassantTarget = "-"
func updateHalfMoveClock(fromIdx: int, toIdx: int) -> void:
var piece = board[fromIdx]
if piece == null:
return
# Reset on pawn move or capture
if piece.type == "pawn" or board[toIdx] != null:
halfMoveClock = 0
else:
halfMoveClock += 1