Search code examples
pythonindexingerror-handlingartificial-intelligencecs50

How to turn set items into indexes for an array


I am currently trying to solve an exercise in CS50 AI, where I am supposed to create a tictactoe using a minimax algorithm. While doing this, I have to also create a function that generates possible options as well a function that generates a new state of the board as soon as one of the possible actions is chosen. However, I am having an error in my code.

This is the error:

File "c:\Users\Melisa\OneDrive\Desktop\tictactoe\tictactoe.py", line 40, in result
    kopja[i][j] = player(board)
TypeError: list indices must be integers or slices, not tuple`

This is my code for the whole problem:

import copy
import math

X = "X"
O = "O"
EMPTY = None

def initial_state():
    return [[EMPTY, EMPTY, EMPTY],
            [EMPTY, EMPTY, EMPTY],
            [EMPTY, EMPTY, EMPTY]]

def player(board):
    Xc= 0
    Oc= 0
    for i in board:
        for j in i:
            if j == X:
                Xc+=1
            if j == O:
                Oc+=1
    # since we start with x
    if Xc>Oc:
        return O
    else:
        return X

def actions(board):
    possible= set()
    for i in range(0,len(board)):
        for j in range(len(board[0])):
            if board[i][j]== EMPTY:
                possible.add((i,j))
    return possible

def result(board, action):
    kopja = copy.deepcopy(board)
    i,j = action[0],action[1]

    kopja[i][j] = player(board)
    return kopja

def winner(board):
    # present all of the winning outlays in a manual way:
    # horisontally; only i changes ,j is contantly 0,1,2
    for e in range(3):
        if board[e][0]==board[e][1]==board[e][2] and board[e][0] != EMPTY:
            winneri = board[0][e]
        # diagonals are purely fixed
        if (board[0][0]==board[1][1]==board[2][2] or board[0][2]==board[1][1]==board[2][0])and board[1][1]!=EMPTY:
            winneri= board[1][1]
        else:
            winneri= None

    return winneri

def terminal(board):  
    if winner(board) == X or winner(board) == O:
        return True 
    for i in range(3):
        for j in range(3):
            if board[i][j] == EMPTY:
                return False
        return True

def utility(board):
    if winner(board)== X:
        return 1
    if winner(board)== O:
        return -1
    else:
        return 0

def maxval(board):
    if terminal(board): 
        return utility(board)
    else:
        v = float('-inf')
        for action in actions(board):
            v = max(v,minval(result(board,action)))
        return v

def minval(board):
    if terminal(board):
        return utility(board)
    else:
        v = float('inf')
        for action in actions(board):
            v = min(v,maxval(result(board,action)))
        return v

def minimax(board):
    if terminal(board):
        return None 
    else:
        listx=[]
        if player(board) == X:
            for action in actions(board):
                listx.append((minval(result(board,action)),action))
                listx.reverse()
                listi=listx
            return listi[0]

        if player(board) == O:
            listo=[]
            for action in actions(board):
                listo.append((maxval(result(board,action)),action))
                listo.reverse()
                lista=listo
            return lista[0]

Here is the code that CS50 uses to run the program:

import pygame
import sys
import time

import tictactoe as ttt

pygame.init()
size = width, height = 600, 400


black = (0, 0, 0)
white = (255, 255, 255)

screen = pygame.display.set_mode(size)

mediumFont = pygame.font.Font("OpenSans-Regular.ttf", 28)
largeFont = pygame.font.Font("OpenSans-Regular.ttf", 40)
moveFont = pygame.font.Font("OpenSans-Regular.ttf", 60)

user = None
board = ttt.initial_state()
ai_turn = False

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    screen.fill(black)

    # Let user choose a player.
    if user is None:

        # Draw title
        title = largeFont.render("Play Tic-Tac-Toe", True, white)
        titleRect = title.get_rect()
        titleRect.center = ((width / 2), 50)
        screen.blit(title, titleRect)

        # Draw buttons
        playXButton = pygame.Rect((width / 8), (height / 2), width / 4, 50)
        playX = mediumFont.render("Play as X", True, black)
        playXRect = playX.get_rect()
        playXRect.center = playXButton.center
        pygame.draw.rect(screen, white, playXButton)
        screen.blit(playX, playXRect)

        playOButton = pygame.Rect(5 * (width / 8), (height / 2), width / 4, 50)
        playO = mediumFont.render("Play as O", True, black)
        playORect = playO.get_rect()
        playORect.center = playOButton.center
        pygame.draw.rect(screen, white, playOButton)
        screen.blit(playO, playORect)

        # Check if button is clicked
        click, _, _ = pygame.mouse.get_pressed()
        if click == 1:
            mouse = pygame.mouse.get_pos()
            if playXButton.collidepoint(mouse):
                time.sleep(0.2)
                user = ttt.X
            elif playOButton.collidepoint(mouse):
                time.sleep(0.2)
                user = ttt.O

    else:

        # Draw game board
        tile_size = 80
        tile_origin = (width / 2 - (1.5 * tile_size),
                       height / 2 - (1.5 * tile_size))
        tiles = []
        for i in range(3):
            row = []
            for j in range(3):
                rect = pygame.Rect(
                    tile_origin[0] + j * tile_size,
                    tile_origin[1] + i * tile_size,
                    tile_size, tile_size
                )
                pygame.draw.rect(screen, white, rect, 3)

                if board[i][j] != ttt.EMPTY:
                    move = moveFont.render(board[i][j], True, white)
                    moveRect = move.get_rect()
                    moveRect.center = rect.center
                    screen.blit(move, moveRect)
                row.append(rect)
            tiles.append(row)

        game_over = ttt.terminal(board)
        player = ttt.player(board)

        # Show title
        if game_over:
            winner = ttt.winner(board)
            if winner is None:
                title = f"Game Over: Tie."
            else:
                title = f"Game Over: {winner} wins."
        elif user == player:
            title = f"Play as {user}"
        else:
            title = f"Computer thinking..."
        title = largeFont.render(title, True, white)
        titleRect = title.get_rect()
        titleRect.center = ((width / 2), 30)
        screen.blit(title, titleRect)

        # Check for AI move
        if user != player and not game_over:
            if ai_turn:
                time.sleep(0.5)
                move = ttt.minimax(board)
                board = ttt.result(board, move)
                ai_turn = False
            else:
                ai_turn = True

        # Check for a user move
        click, _, _ = pygame.mouse.get_pressed()
        if click == 1 and user == player and not game_over:
            mouse = pygame.mouse.get_pos()
            for i in range(3):
                for j in range(3):
                    if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)):
                        board = ttt.result(board, (i, j))

        if game_over:
            againButton = pygame.Rect(width / 3, height - 65, width / 3, 50)
            again = mediumFont.render("Play Again", True, black)
            againRect = again.get_rect()
            againRect.center = againButton.center
            pygame.draw.rect(screen, white, againButton)
            screen.blit(again, againRect)
            click, _, _ = pygame.mouse.get_pressed()
            if click == 1:
                mouse = pygame.mouse.get_pos()
                if againButton.collidepoint(mouse):
                    time.sleep(0.2)
                    user = None
                    board = ttt.initial_state()
                    ai_turn = False

    pygame.display.flip()

I tried to use the tuples inside the set as indexes for the array(board) by assigning them :

i,j = action[0],action[1]

and expected this solution to work, but instead it generated an error.


Solution

  • After some digging thru your code, I think I figured it out. Did you check the value of action before the call to result()? If so, I think you will find it is not a what you think it is. (In other words, it does not look like (1,1)). The cause is complicated...it begins near the end of minimax() then propagates thru your code. The code segment of interest is repeated below for easy reference:

    if player(board) == X:
        for action in actions(board):
            listx.append((minval(result(board,action)),action))
            listx.reverse()
            listi=listx
        return listi[0]
    

    If I understand your code, inside this for loop you are creating listx as a list of tuples with (value, action) pairs. Then, when you exit the loop, you return listi[0] which is the 1st (value, action) tuple in the list. You want to return the action from the first tuple, which is listi[0][1].

    Once you get that fixed, review the logic in the loop. You are creating a list and reversing it each time thru the loop. Its not clear to me why are you reversing it. Do you want to sort the tuples based on the value? If so, you should do that after you exit the loop, AND use the value in the tuple as the sort key. Also, you really don't need 2 lists.