Game keeps making me play already won Ultimate Tic Tac Toe columns

Godot Version

Godot 4.3

Question

So, as you all know, I have been working on a Ultimate Tic Tac Toe game... Heres how to play it https://en.wikipedia.org/wiki/Ultimate_tic-tac-toe but basically, whenever the Label states for me to play, sometimes it can land on a already won Column and I can't click anything else and I have to reset the game... Any ideas?

Here is a video to show whats happening

Here is the script:

miniboard.gd (The script with the game)

extends Control  # Change this to the appropriate base class, if needed


var current_player = "X"  # Keep track of the current player
var cells = []  # Store references to buttons
var small_board_wins = []  # Track wins for small boards
var overall_winner = ""  # Track overall winner
var is_initialized = false  # Track if the board is initialized
var stats: Dictionary = load_stats()  # Load or initialize stats at the start
var active_column = -1  # Initialize with -1 to allow any board on the first move

@onready var winner_label = $WinnerLabel  # General winner label
@onready var home_button = $HomeTownButton  # Reference to the HomeButton
@onready var rematch_button = $RematchYourselfButton  # Reference to the Rematch button
@onready var state_labels = [
	$State1, $State2, $State3, $State4, $State5, $State6, $State7, $State8, $State9
]


# Declare the variable for your AudioStreamPlayer
var button5_click_player: AudioStreamPlayer = null  # Reference to the AudioStreamPlayer for Button5

# Declare labels for each column
var x_win_labels = []
var o_win_labels = []
var tie_labels = []  # Array to hold tie labels for each column

# Function to save stats
func load_stats() -> Dictionary:
	var file_path = "user://Stats.cfg"
	if not FileAccess.file_exists(file_path):
		print("Stats file does not exist. Initializing with default values.")
		return {
			"Ultimate_Tic_Tac_Toe_Yourself_Won": 0,
			"Ultimate_Tic_Tac_Toe_Yourself_Lost": 0,
			"Ultimate_Tic_Tac_Toe_AI_Won": 0,
			"Ultimate_Tic_Tac_Toe_AI_Lost": 0,
			"Simple_Tic_Tac_Toe_AI_Won": 0,
			"Simple_Tic_Tac_Toe_Won": 0,
			"Simple_Tic_Tac_Toe_AI_Lost": 0,
			"Simple_Tic_Tac_Toe_Lost": 0
		}

	var file = FileAccess.open(file_path, FileAccess.READ)
	if file:
		var local_stats = {}  # Renamed the local variable
		while not file.eof_reached():
			var line = file.get_line().strip_edges()
			var parts = line.split("=")
			if parts.size() == 2:
				local_stats[parts[0]] = int(parts[1])  # Convert the value to an int
		file.close()
		return local_stats  # Return the renamed variable
	else:
		print("Failed to open file for reading.")
		return {}

func save_stats(new_stats: Dictionary) -> void:
	var file_path = "user://Stats.cfg"
	var file = FileAccess.open(file_path, FileAccess.WRITE)
	if file:
		for key in new_stats.keys():
			var line = key + "=" + str(new_stats[key])  # Create the line for each key-value pair
			file.store_line(line)  # Store the line in the file
		file.close()
	else:
		print("Failed to open file for writing.")

func _ready():
	for label in state_labels:
		label.visible = false

	if is_initialized:  # Prevent re-initialization
		return
	is_initialized = true  # Mark as initialized
	
	stats = load_stats()  # Load existing stats

# Initialize AudioStreamPlayer reference and set the sound stream
	button5_click_player = $Button5ClickPlayer  # Make sure this node exists in the scene
	button5_click_player.stream = load("res://sounds/click.ogg")  # Set the path to your sound file

	cells.clear()
	small_board_wins = [null, null, null, null, null, null, null, null, null]  # Initialize small board wins
	winner_label.visible = false
	home_button.visible = false  # Hide the HomeButton initially
	rematch_button.visible = false  # Hide the Rematch button initially

	# Initialize labels for each column
	for i in range(1, 10):
		# Use 'get_node_safe' or check if the node exists
		var x_label = get_node("XWinLabelColumn" + str(i))
		var o_label = get_node("OWinLabelColumn" + str(i))
		var tie_label = get_node("ItsATieLabelColumn" + str(i))

		if x_label and o_label and tie_label:
			x_win_labels.append(x_label)
			o_win_labels.append(o_label)
			tie_labels.append(tie_label)
			x_win_labels[i - 1].visible = false
			o_win_labels[i - 1].visible = false
			tie_labels[i - 1].visible = false
		else:
			print("One or more labels not found for column: ", i)

	# Initialize cells for columns and connect all buttons
	for column_index in range(1, 10):  # Columns 1 to 9
		var grid_container: GridContainer = get_node("GridContainerColumn" + str(column_index))
		for cell_index in range(9):  # Cells 1 to 9 in each column
			var cell: Button = grid_container.get_child(cell_index)
			cells.append(cell)
			cell.connect("pressed", Callable(self, "_on_Cell_pressed").bind(column_index - 1, cell_index))
			
			# Function to connect buttons safely
func _connect_button(button: Button, handler: String):
	if button and !button.is_connected("pressed", Callable(self, handler)):
		button.connect("pressed", Callable(self, handler))
	else:
		print(button.name + " not found or already connected.")
		

# Function to play button click sound
func _play_button_click_sound():
	print("Playing button click sound")  # Debugging statement
	button5_click_player.play()  # Play the button click sound

# Single handler for all Cells (1-81)
func _on_Cell_pressed(column_index: int, cell_index: int) -> void:
	if overall_winner != "" or (active_column != -1 and active_column != column_index): 
		return  # Ignore input if game is won or move is in the wrong column
	
	var cell: Button = get_node("GridContainerColumn" + str(column_index + 1)).get_child(cell_index)
	
	# Check if the cell is already occupied or the column has been won
	if cell.text != "" or small_board_wins[column_index] != null:
		# Show the state label for the specific column
		state_labels[column_index].visible = true
		return  # Do not allow the move if the cell is already filled or the column is won

	cell.text = current_player
	if check_winner(column_index):
		small_board_wins[column_index] = current_player
		update_column_win_label(column_index)
		lock_column(column_index)
		if check_overall_winner():
			display_winner(current_player)
			lock_game()
			return
	if is_column_full(column_index):
		tie_labels[column_index].visible = true
		lock_column(column_index)

	current_player = "O" if current_player == "X" else "X"
	active_column = cell_index  # Set active column based on the cell selected
	
	# Hide all state labels initially
	for label in state_labels:
		label.visible = false

	# Show the label for the next active column if there is one
	if overall_winner == "" and active_column != -1:
		# Only show the state label if the next column has not been won
		if small_board_wins[active_column] == null:
			state_labels[active_column].visible = true
		state_labels[column_index].visible = false  # Hide label after move in column


func update_stats(is_win: bool, player: String):
	print("Updating stats - is_win:", is_win, "player:", player)  # Debugging output
	# Ensure the necessary keys are present in the stats dictionary
	if not stats.has("Ultimate_Tic_Tac_Toe_Yourself_Won"):
		stats["Ultimate_Tic_Tac_Toe_Yourself_Won"] = 0
	if not stats.has("Ultimate_Tic_Tac_Toe_Yourself_Lost"):
		stats["Ultimate_Tic_Tac_Toe_Yourself_Lost"] = 0

	# Update stats based on win/loss and player
	if player == "X":
		if is_win:
			stats["Ultimate_Tic_Tac_Toe_Yourself_Won"] += 1
	elif player == "O":
		if is_win:
			stats["Ultimate_Tic_Tac_Toe_Yourself_Lost"] += 1

	save_stats(stats)

func display_winner(player: String) -> void:
	if player == "X":
		update_stats(true, "X")  # Player wins
		update_stats(false, "O")  # AI loses
	else:
		update_stats(false, "X")  # Player loses
		update_stats(true, "O")  # AI wins

	# Other display logic remains the same
	for label in x_win_labels:
		label.visible = false
	for label in o_win_labels:
		label.visible = false
	
	# Show the general winner label
	winner_label.text = player + " wins!"
	winner_label.visible = true
	home_button.visible = true
	rematch_button.visible = true

	# Call reset game state to prepare for the next game
	reset_game_state()

func end_game(is_player_win: bool):
	if is_player_win:  # If the player won
		update_stats(true, "X")  # Call with is_win true
	else:  # Player lost
		update_stats(false, "X")  # Call with is_win false 

func reset_game_state() -> void:
	current_player = "X"  # Reset current player
	small_board_wins = [null, null, null, null, null, null, null, null, null]  # Reset small board wins
	overall_winner = ""  # Reset overall winner


func is_column_full(column_index: int) -> bool:
	var grid_container: GridContainer = get_node("GridContainerColumn" + str(column_index + 1))
	for i in range(9):
		var cell: Button = grid_container.get_child(i)
		if cell.text == "":
			return false  # Return false if any cell is empty
	return true  # Return true if all cells are filled

func update_column_win_label(column_index: int) -> void:
	# Show the appropriate win label for the column
	if small_board_wins[column_index] == "X":
		x_win_labels[column_index].visible = true
	elif small_board_wins[column_index] == "O":
		o_win_labels[column_index].visible = true

func lock_column(column_index: int) -> void:
	var grid_container: GridContainer = get_node("GridContainerColumn" + str(column_index + 1))
	for i in range(9):
		var cell: Button = grid_container.get_child(i)
		cell.disabled = true

func lock_game() -> void:
	for column_index in range(9):
		lock_column(column_index)
	active_column = -1  # Reset active column to allow any board for rematch

func check_winner(column_index: int) -> bool:
	var grid_container: GridContainer = get_node("GridContainerColumn" + str(column_index + 1))

	# Check horizontal win
	for i in range(3):
		if grid_container.get_child(i * 3).text == current_player and \
		   grid_container.get_child(i * 3 + 1).text == current_player and \
		   grid_container.get_child(i * 3 + 2).text == current_player:
			return true

	# Check vertical win
	for i in range(3):
		if grid_container.get_child(i).text == current_player and \
		   grid_container.get_child(i + 3).text == current_player and \
		   grid_container.get_child(i + 6).text == current_player:
			return true

	# Check diagonal win
	if grid_container.get_child(0).text == current_player and \
	   grid_container.get_child(4).text == current_player and \
	   grid_container.get_child(8).text == current_player:
		return true

	if grid_container.get_child(2).text == current_player and \
	   grid_container.get_child(4).text == current_player and \
	   grid_container.get_child(6).text == current_player:
		return true

	return false


func check_overall_winner() -> bool:
	for i in range(3):
		if (small_board_wins[i * 3] == current_player and \
			small_board_wins[i * 3 + 1] == current_player and \
			small_board_wins[i * 3 + 2] == current_player) or \
		   (small_board_wins[i] == current_player and \
			small_board_wins[i + 3] == current_player and \
			small_board_wins[i + 6] == current_player):
			return true
	if (small_board_wins[0] == current_player and \
		small_board_wins[4] == current_player and \
		small_board_wins[8] == current_player) or \
	   (small_board_wins[2] == current_player and \
		small_board_wins[4] == current_player and \
		small_board_wins[6] == current_player):
		return true
	return false


func _on_hometown_button_pressed() -> void:
	button5_click_player.play()  # Play the button click sound
	print("HomeButton pressed")
	await get_tree().create_timer(0.1).timeout  # Add a delay
	get_tree().change_scene_to_file("res://UI.tscn")  # Update with your actual main menu scene path

func _on_minihomepage_pressed() -> void:
	button5_click_player.play()  # Play the button click sound
	print("MiniHomepage pressed")
	await get_tree().create_timer(0.1).timeout  # Add a delay
	get_tree().change_scene_to_file("res://UI.tscn")  # Update with your actual main menu scene path

func _on_rematchyourselfbutton_pressed() -> void:
	button5_click_player.play()  # Play the button click sound
	print("Rematch button pressed")
	await get_tree().create_timer(0.1).timeout  # Add a delay
	# Call functions to reset the game
	clear_board()
	reset_labels()
	reset_game_state()
	rematch_button.visible = false  # Hide the rematch button after pressing it
	home_button.visible = false # hide after pressed


func clear_board() -> void:
	for column_index in range(9):
		var grid_container: GridContainer = get_node("GridContainerColumn" + str(column_index + 1))
		for i in range(9):
			var cell: Button = grid_container.get_child(i)
			cell.text = ""
			cell.disabled = false  # Re-enable the cells

func reset_labels() -> void:
	for label in x_win_labels:
		label.visible = false
	for label in o_win_labels:
		label.visible = false
	for label in tie_labels:
		label.visible = false
	winner_label.visible = false  # Hide the winner label

miniboard SceneTree:







Any help would be very much appreciated!! Thank you so much and have a great night! :slight_smile:

There is a lot to take in.

I would suggest using the debugger and examine the game state using the remote tree view. Find values that are wrong and back trace how they got the way they did.

An alternative is to write unit tests. Write tests for individual functions providing input and evaluate expected outcomes. plugins like GDUnit, and GUT are good.

1 Like

Ok i will do that and update you if it still occurs.
Thank you! :slight_smile: