class_name ChessGame extends Control const PieceContainer = preload("res://Systems/PieceContainer.gd") const WHITE = "white" const BLACK = "black" signal tile_pressed(location: String) signal send_location(location: String) signal turn_changed 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 const StockfishController = preload("res://Systems/FairyStockfish/Stockfish.gd") var stockfishController: StockfishController var stockfishPath = "res://Assets/ChessEngines/stockfish/stockfish.exe" @onready var turnIndicator: ColorRect = $TurnIndicator @onready var p1String: RichTextLabel = $Player1Points @onready var p2String: RichTextLabel = $Player2Points @onready var deckManager: DeckManager @onready var tileManager: TileManager @onready var cardDisplay: CardDisplay @onready var cardPreview: CardPreview @export var boardXSize = 8 @export var boardYSize = 8 @export var tileXSize: int = 50 @export var tileYSize: int = 50 @export var windowXSize: int = 1280 @export var windowYSize: int = 720 @export var FEN: String = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" @onready var boardContainer: FlowContainer = $Flow @onready var stateMachine: StateMachine = $StateMachine var currentFen = "" var lightStyle = null var darkStyle = null var highlightStyle = null var cpuElo = 1500 func _ready() -> void: 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)) initializeGame() initializeTiles() stateMachine.transitionToNextState(Constants.WHITE_TURN) stockfishController = StockfishController.new(board) add_child(stockfishController) if stockfishController.connect_to_engine(stockfishPath): stockfishController.limit_strength_to(cpuElo) func _exit_tree(): stockfishController.disconnect_engine() func initializeTiles() -> void: tileManager = TileManager.new($Flow, self) add_child(tileManager) await get_tree().process_frame tileManager.initialize(boardContainer) tileManager.place_random_game_tiles() func get_base_style(is_white: bool) -> StyleBoxFlat: return lightStyle if is_white else darkStyle func _unhandled_input(event: InputEvent) -> void: stateMachine.unhandledInput(event) func _process(delta: float) -> void: stateMachine.process(delta) func initializeGame() -> void: setupStyles() setupUI() initializeDeckSystem() initializeCardPreview() initializeBoard() func initializeCardPreview() -> void: cardPreview = CardPreview.new() add_child(cardPreview) func getCurrentFen() -> String: 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 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" % [ "w" if isWhiteToMove else "b", castlingRights, enPassantTarget, halfMoveClock, moveCount ] return fen func initializeBoard() -> void: # Parse FEN to get board dimensions # rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 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 # Calculate board width by counting pieces and empty squares in first row # Initialize board array board = [] for i in range(boardYSize): var row = [] for j in range(boardXSize): row.append(null) board.append(row) createBoard() setupPiecesFromFEN() 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 setupUI() -> void: p1String.text = "0" p2String.text = "0" updateTurnIndicator() func initializeDeckSystem() -> void: deckManager = DeckManager.new() cardDisplay = CardDisplay.new() add_child(deckManager) add_child(cardDisplay) 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") cardDisplay.update_hand(deckManager.hand) deckManager.connect("hand_updated", func(hand): cardDisplay.update_hand(hand)) func createBoard() -> void: boardContainer.add_to_group("Flow") var numberX = 0 var numberY = 0 var isWhite = true 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: # print("CreateTile x ", x, " y ", y); var tile = PieceContainer.new() tile.set_custom_minimum_size(Vector2(tileXSize, tileYSize)) tile.add_theme_stylebox_override("normal", lightStyle if isWhite else darkStyle) 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 clearSelection() : resetHighlights() selectedNode = "" cardPreview.hide_preview() return func updatePointsAndCapture(capturedPiece: Pawn, animate: bool = true) -> void: 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: var container = capturedPiece.get_parent() as PieceContainer await capturedPiece.animate_capture() container.remove_piece() func parseLocation(location: String) -> void: var number = 0 locationX = "" while location.substr(number, 1) != "-": locationX += location.substr(number, 1) number += 1 locationY = location.substr(number + 1) 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 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 setupPiecesFromFEN() -> void: 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 (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 piece_info = getFENPieceInfo(c) placePiece(str(x) + "-" + str(board_y), piece_info.name, piece_info.color) x += 1 func getFENPieceInfo(fen_char: String) -> Dictionary: 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 placePiece(position: String, pieceName: String, color: int) -> void: 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: var piece match pieceName: "Pawn": piece = Pawn.new() piece.name = "Pawn" "King": piece = King.new() piece.name = "King" "Queen": piece = Queen.new() piece.name = "Queen" "Knight": piece = Knight.new() piece.name = "Knight" "Rook": piece = Rook.new() piece.name = "Rook" "Bishop": piece = Bishop.new() piece.name = "Bishop" piece.Item_Color = color piece.position = Vector2(tileXSize / 2, tileYSize / 2) return piece func clearBoard() -> void: for child in boardContainer.get_children(): if child is PieceContainer: child.remove_piece() # func prepareHand() -> void: # if deckManager.hand.size() < 5: # deckManager.drawCard() # pass func updateEffectDurations() -> void: deckManager.updateCardDurations() tileManager.update_tile_durations() func applyTileEffects() -> void: tileManager.apply_tile_effects() func applyCardEffects() -> void: pass func evaluatePosition() -> Dictionary: var status = { "checkmate": isCheckmate(), "draw": isDraw(), } return status func isValidMove(location: String) -> bool: 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 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 getMovableAreas() -> void: # print("HIGHLIGHTING getMovableAreas 1") resetHighlights() areas.clear() specialArea.clear() var container = get_node("Flow/" + selectedNode) as PieceContainer var piece = container.get_piece() 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 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 togglePieceChessEffect() -> void: 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 resolveMoveEffects() -> void: print("resolveMoveEffects", currentlyMovingPiece) togglePieceChessEffect() selectedNode = "" Turn = 1 if Turn == 0 else 0 updateTurnIndicator() resetHighlights() hasMoved = false currentlyMovingPiece = null emit_signal("turn_changed") func isPlayerTurn() -> bool: return currentPlayer == WHITE func resetHighlights(): 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) func isCheckmate() -> bool: return gamecheckMate func isDraw() -> bool: return gamedraw func endGame(reason: String) -> void: pass func cleanupPhase() -> void: pass func isNull(location: String) -> bool: return get_node_or_null("Flow/" + location) == null func getPieceFenChar(piece: Pawn) -> String: 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 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 func _on_ai_move_generated(move: String) -> void: if Turn == 1: # Only process AI moves during black's turn # The move will be automatically handled by the Movement state # which is listening for the moveGenerated signal pass func executeAiMove(fromLocation: String, toLocation: String) -> void: selectedNode = fromLocation executeMove(toLocation) func convertNotationToLocation(notation: String) -> String: var file = notation[0].unicode_at(0) - 'a'.unicode_at(0) var rank = 8 - int(notation[1]) return str(file) + "-" + str(rank)