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 map_open_requested(options) 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 = 75 @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") @onready var mapContainer = get_node_or_null("/root/Board/MapScreen") # 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 options and "fen" in options: currentFen = options.fen if "elo" in options: cpuElo = options.elo if cameraController: cameraController.reset_view() print("ChessGame FEN ", currentFen) print("ChessGame DIMENSIONS ", get_board_dimensions(currentFen)) if is_initialized: resetBoard() initializeDeckSystem() if cardDisplay: cardDisplay.visible = true if stateMachine: stateMachine.start() stateMachine.transitionToNextState(Constants.WHITE_TURN) else: initialize_game_system() if currentFen: stockfishController.start_board(cpuElo, get_board_dimensions(currentFen)) else: stockfishController.start_board(cpuElo, "8x8") 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 var fen_parts = currentFen.split(" ") var rows = fen_parts[0].split("/") boardYSize = rows.size() boardXSize = 0 # Calculate width from first row by counting both pieces numbers and tiles 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: 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 while numberX != boardXSize: if numberY == 0: boardContainer.size.x += tileXSize + 5 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)) # if x == 0: # var style = StyleBoxFlat.new() # style.bg_color = Utils.GREEN_CELL # tile.add_theme_stylebox_override("normal", style ) # else: 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 = currentFen.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 = currentFen.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 ", position) 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; 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 endRound() -> void: print("****************ENDROUND*************") deckManager.cleanup() cardDisplay.visible = false mapContainer.visible = true resetBoard() 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() updatePointsAndCapture(capturedPiece, true) # 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: gold += capturedPiece.Points; 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 func get_board_dimensions(fen_string: String) -> String: var board_part: String = fen_string.split(" ")[0] var ranks: Array = board_part.split("/") var height: int = ranks.size() var width: int = 0 var first_rank: String = ranks[0] for character in first_rank: if character.is_valid_int(): width += int(character) else: width += 1 return str(width) + "x" + str(height)