diff --git a/.idea/SGAI-DRHAT-Outbreak-Final.iml b/.idea/SGAI-DRHAT-Outbreak-Final.iml new file mode 100644 index 0000000..fa7a615 --- /dev/null +++ b/.idea/SGAI-DRHAT-Outbreak-Final.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..3dce9c6 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..dc9ea49 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..171a93e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SGAI_MK3/Assets/.DS_Store b/Assets/.DS_Store similarity index 100% rename from SGAI_MK3/Assets/.DS_Store rename to Assets/.DS_Store diff --git a/Assets/Kill sound.mp3 b/Assets/Kill sound.mp3 new file mode 100644 index 0000000..0b32778 Binary files /dev/null and b/Assets/Kill sound.mp3 differ diff --git a/Assets/Outbreak_title.png b/Assets/Outbreak_title.png new file mode 100644 index 0000000..e227e9b Binary files /dev/null and b/Assets/Outbreak_title.png differ diff --git a/Assets/RedCross.png b/Assets/RedCross.png new file mode 100644 index 0000000..d81533d Binary files /dev/null and b/Assets/RedCross.png differ diff --git a/Assets/bite.png b/Assets/bite.png new file mode 100644 index 0000000..495e05f Binary files /dev/null and b/Assets/bite.png differ diff --git a/Assets/cure.png b/Assets/cure.png new file mode 100644 index 0000000..754d796 Binary files /dev/null and b/Assets/cure.png differ diff --git a/SGAI_MK3/Assets/govt.png b/Assets/govt.png similarity index 100% rename from SGAI_MK3/Assets/govt.png rename to Assets/govt.png diff --git a/Assets/kill.png b/Assets/kill.png new file mode 100644 index 0000000..5b5fdef Binary files /dev/null and b/Assets/kill.png differ diff --git a/SGAI_MK3/Assets/map_background.jpeg b/Assets/map_background.jpeg similarity index 100% rename from SGAI_MK3/Assets/map_background.jpeg rename to Assets/map_background.jpeg diff --git a/Assets/person_normal.png b/Assets/person_normal.png new file mode 100644 index 0000000..09afb34 Binary files /dev/null and b/Assets/person_normal.png differ diff --git a/SGAI_MK3/Assets/person_vax.png b/Assets/person_vax.png similarity index 100% rename from SGAI_MK3/Assets/person_vax.png rename to Assets/person_vax.png diff --git a/Assets/person_zombie.png b/Assets/person_zombie.png new file mode 100644 index 0000000..30ddfdb Binary files /dev/null and b/Assets/person_zombie.png differ diff --git a/SGAI_MK3/Assets/self_play.png b/Assets/self_play.png similarity index 100% rename from SGAI_MK3/Assets/self_play.png rename to Assets/self_play.png diff --git a/SGAI_MK3/Assets/theirturn.png b/Assets/theirturn.png similarity index 100% rename from SGAI_MK3/Assets/theirturn.png rename to Assets/theirturn.png diff --git a/SGAI_MK3/Assets/yourturn.png b/Assets/yourturn.png similarity index 100% rename from SGAI_MK3/Assets/yourturn.png rename to Assets/yourturn.png diff --git a/SGAI_MK3/Assets/zom.png b/Assets/zom.png similarity index 100% rename from SGAI_MK3/Assets/zom.png rename to Assets/zom.png diff --git a/Board.py b/Board.py new file mode 100644 index 0000000..dd0b93b --- /dev/null +++ b/Board.py @@ -0,0 +1,499 @@ +from State import State +import random as rd +from Person import Person +from typing import List, Tuple +from constants import * +import pygame +pygame.mixer.init() + + +class Board: + #initializing variables + def __init__( + self, + dimensions: Tuple[int, int], + border: int, + cell_dimensions: Tuple[int, int], + player_role: Role, + ): + self.outrage = 0 + self.anxiety = 0 + + self.rows = dimensions[0] + self.columns = dimensions[1] + self.display_border = border + self.display_cell_dimensions = cell_dimensions + self.player_role = player_role + + if player_role == Role.government: + self.computer_role = Role.zombie + else: + self.computer_role = Role.government + + self.population = 0 # total number of people and zombies + self.States = [] + self.QTable = [] + for s in range(dimensions[0] * dimensions[1]): # creates a 1d array of the board + self.States.append(State(None, s)) + self.QTable.append([0] * 6) + + self.actionToFunction = { + Action.move: self.move, + Action.heal: self.heal, + Action.bite: self.bite, + Action.kill: self.kill + } + + def num_zombies(self) -> int: #number of zombies on the board, different than population + r = 0 + for state in self.States: + if state.person != None: + if state.person.isZombie: + r += 1 + return r + + def act(self, oldstate: Tuple[int, int], givenAction: str): # takes in the cell and action and performs that using the actiontofunction + cell = self.toCoord(oldstate) + f = self.actionToFunction[givenAction](cell) + reward = self.States[oldstate].evaluate(givenAction, self) + if f[0] == False: + reward = 0 + return [reward, f[1]] + + def containsPerson(self, isZombie: bool): #checks if person is a person + for state in self.States: + if state.person is not None and state.person.isZombie == isZombie: + return True + return False + + def get_possible_moves(self, action: Action, direction: Direction, role): + """ + Get the coordinates of people (or zombies) that are able + to make the specified move. + @param action - the action to return possibilities for (options are Action.bite, Action.move, Action.heal, and Action.kil) + @param direction - the direction this action is heading (options are Direction.up, Direction.down, Direction.left, Direction.right) + @param role - either 'Zombie' or 'Government'; helps decide whether an action + is valid and which people/zombies it applies to + """ + poss = [] + B = self.clone(self.States, role) + + if role == Role.zombie: + if not self.containsPerson(True): + return poss + + for state in self.States: + if state.person is not None: + changed_states = False + + if ( + state.person.isZombie + and bool(B.actionToFunction[action](B.toCoord(state.location), direction).value) + ): + poss.append(B.toCoord(state.location)) + changed_states = True + + if changed_states: + # reset the states because it had to check the possible moves by moving the states + B.States = [ + self.States[i].clone() + if self.States[i] != B.States[i] + else B.States[i] + for i in range(len(self.States)) + ] + + elif role == Role.government: + if not self.containsPerson(False): + + return poss + for state in self.States: + if state.person is not None: #checks if the boxes are empty + changed_states = False + if ( + not state.person.isZombie + and bool(B.actionToFunction[action](B.toCoord(state.location), direction).value) + ): + poss.append(B.toCoord(state.location)) + changed_states = True + + if changed_states: + # reset the states cuz it moved the board to check possibilities + B.States = [ + self.States[i].clone() + if self.States[i] != B.States[i] + else B.States[i] + for i in range(len(self.States)) + ] + return poss + + def toCoord(self, i: int): # converts coord from 1d to 2d + return (int(i % self.columns), int(i / self.rows)) + + def toIndex(self, coordinates: Tuple[int, int]): # converts coord from 2d to 1d + return int(coordinates[1] * self.columns) + int(coordinates[0]) + + def isValidCoordinate(self, coordinates: Tuple[int, int]): #checks if the box is in the grid + return ( + coordinates[1] < self.rows + and coordinates[1] >= 0 + and coordinates[0] < self.columns + and coordinates[0] >= 0 + ) + + def clone(self, L: List[State], role: Role): #creates a duplicate board + + NB = Board( + (self.rows, self.columns), + self.display_border, + self.display_cell_dimensions, + self.player_role, + ) + NB.States = [state.clone() for state in L] + NB.player_role = role + return NB + + def isAdjacentTo(self, coord: Tuple[int, int], is_zombie: bool) -> bool: # returns adjacent coordinates containing the same type (so person if person etc) + + ret = False + vals = [ + (coord[0], coord[1] + 1), + (coord[0], coord[1] - 1), + (coord[0] + 1, coord[1]), + (coord[0] - 1, coord[1]), + ] + for coord in vals: + if ( + self.isValidCoordinate(coord) + and self.States[self.toIndex(coord)].person is not None + and self.States[self.toIndex(coord)].person.isZombie == is_zombie + ): + ret = True + break + + return ret + + def getTargetCoords(self, coords: Tuple[int, int], direction: Direction) -> Tuple[int, int]: + if direction == Direction.up: + new_coords = (coords[0], coords[1] - 1) + print(f"going from {coords} to new coords {new_coords}") + elif direction == Direction.down: + new_coords = (coords[0], coords[1] + 1) + print(f"going from {coords} to new coords {new_coords}") + elif direction == Direction.left: + new_coords = (coords[0] - 1, coords[1]) + print(f"going from {coords} to new coords {new_coords}") + elif direction == Direction.right: + new_coords = (coords[0] + 1, coords[1]) + print(f"going from {coords} to new coords {new_coords}") + elif direction == Direction.self: + new_coords = coords + + self.States[self.toIndex(coords)].person.facing = Direction + + return new_coords + + def move(self, coords: Tuple[int, int], direction: Direction) -> Result: + new_coords = self.getTargetCoords(coords, direction) + if direction == Direction.self: return Result.invalid + if not self.isValidCoordinate(new_coords): return Result.invalid + + # Get the start and destination index (1D) + start_idx = self.toIndex(coords) + destination_idx = self.toIndex(new_coords) + + # Check if the new coordinates are valid + if not self.isValidCoordinate(new_coords): + return Result.invalid + if( + self.States[start_idx].person.isZombie + and self.States[destination_idx].safeSpace + ): + return Result.invalid + + # Check if the destination is currently occupied + if self.States[destination_idx].person is None: + #Execute Move + self.States[destination_idx].person = self.States[start_idx].person + self.States[start_idx].person = None + return Result.success + return Result.invalid + + def QGreedyat(self, state_id): + biggest = self.QTable[state_id][0] * self.player_role + ind = 0 + A = self.QTable[state_id] + i = 0 + for qval in A: + if (qval * self.player_role) > biggest: + biggest = qval + ind = i + i += 1 + return [ind, self.QTable[ind]] # action_index, qvalue + + # picks the action for the move in the qtable, including a probability that it is randomized based on the learning rate + def choose_action(self, state_id: int, lr: float): + L = lr * 100 + r = rd.randint(0, 100) + if r < L: + return self.QGreedyat(state_id) + else: + if self.player_role == Role.government: # Player is Govt + d = rd.randint(0, 4) + else: + d = rd.randint(0, 5) + while d != 4: + d = rd.randint(0, 4) + return d + + # picks the person that it wants to move or use, also including a learning rate based probability, returning the index of the state + def choose_state(self, lr: float): + L = lr * 100 + r = rd.randint(0, 100) + if r < L: + biggest = None + sid = None + for x in range(len(self.States)): + if self.States[x].person != None: + q = self.QGreedyat(x) + if biggest is None: + biggest = q[1] + sid = x + elif q[1] > biggest: + biggest = q[1] + sid = x + return self.QGreedyat(sid) + else: + if self.player_role == Role.government: # Player is Govt + d = rd.randint(0, len(self.States)) + while self.States[d].person is None or self.States[d].person.isZombie: + d = rd.randint(0, len(self.States)) + else: + d = rd.randint(0, len(self.States)) + while ( + self.States[d].person is None + or self.States[d].person.isZombie == False + ): + d = rd.randint(0, len(self.States)) + return d + + def bite(self, coords: Tuple[int, int], direction: Direction) -> Result: + target_coords = self.getTargetCoords(coords, direction) + if direction == Direction.self: return Result.invalid + if not self.isValidCoordinate(target_coords): return Result.invalid + + # Get the start and destination index (1D) + start_idx = self.toIndex(coords) + target_idx = self.toIndex(target_coords) + + #check if the orgin is valid + if ( + self.States[start_idx].person is None + or not self.States[start_idx].person.isZombie + ): + return Result.invalid + if( + self.States[start_idx].person.isZombie + and self.States[target_idx].safeSpace + ): + return Result.invalid + + + # Check if the destination is valid + if ( + self.States[target_idx].person is None + or self.States[target_idx].person.isZombie + or self.States[target_idx].safeSpace + ): + return Result.invalid + + #calculate probability + chance = 100 + target = self.States[target_idx].person + if target.isVaccinated: + chance = 15 + elif target.wasVaccinated != target.wasCured: + chance = 75 + elif target.wasVaccinated and target.wasCured: + chance = 50 + + # Execute Bite + r = rd.randint(0, 100) + if r < chance: + newTarget = target.clone() + newTarget.isZombie = True + newTarget.isVaccinated = False + self.States[target_idx].person = newTarget + return Result.success + return Result.failure + + def heal(self, coords: Tuple[int, int], direction: Direction) -> Result: + """ + the person at the stated coordinate heals the zombie to the person's stated direction + If no person is selected, then return [False, None] + if a person is vaccined, then return [True, index] + """ + target_coords = self.getTargetCoords(coords, direction) + if not self.isValidCoordinate(target_coords): return Result.invalid + + # Get the start and destination index (1D) + start_idx = self.toIndex(coords) + target_idx = self.toIndex(target_coords) + + #check if the orgin is valid + if ( + self.States[start_idx].person is None + or self.States[start_idx].person.hasMed == False + or self.States[start_idx].person.isZombie + or self.States[start_idx].safeSpace + ): + return Result.invalid + + + # Check if the destination is valid + if ( + self.States[target_idx].person is None + ): + return Result.invalid + + #probability of heal vs failed heal + if self.States[target_idx].person.isZombie: + chance = 50 + else: + chance = 100 + + r = rd.randint(0, 100) + self.States[start_idx].person.hasMed = False + if r < chance: + #implement heal + newTarget = self.States[target_idx].person.clone() + newTarget.isZombie = False + newTarget.wasCured = True + newTarget.isVaccinated = True + newTarget.turnsVaccinated = 1 + self.States[target_idx].person = newTarget + + if chance == 50: + self.anxiety -= 6 + else: + self.anxiety -= 1 + + else: + #implement failed heal + self.bite(target_coords, reverse_dir[direction]) + return Result.failure + return Result.success + + def kill(self, coords: Tuple[int, int], direction: Direction) -> Result: + target_coords = self.getTargetCoords(coords, direction) + if direction == Direction.self: return Result.invalid + if not self.isValidCoordinate(target_coords): return Result.invalid + + # Get the start and destination index (1D) + start_idx = self.toIndex(coords) + target_idx = self.toIndex(target_coords) + + #check if the orgin is valid + if ( + self.States[start_idx].person is None + or self.States[start_idx].person.isZombie + or self.States[start_idx].safeSpace + ): + return Result.invalid + + + # Check if the destination is valid + if ( + self.States[target_idx].person is None + or not self.States[target_idx].person.isZombie + ): + return Result.invalid + + # Execute Kill + self.States[target_idx].person = None + KILL_SOUND.play() + self.outrage += 0.5 * (100 - self.anxiety) + + return Result.success + + def med(self): + for idx in range(len(self.States)): + state = self.States[idx] + if( + state.safeSpace == True + and state.person is not None + ): + state.person.hasMed = True + self.States[idx] = state + + #gets all the locations of people or zombies on the board (this can be used to count them as well) + def get_possible_states(self, role_number: int): + indexes = [] + i = 0 + for state in self.States: + if state.person != None: + if role_number == 1 and state.person.isZombie == False: + indexes.append(i) + elif role_number == -1 and state.person.isZombie: + indexes.append(i) + i += 1 + return indexes + + # runs each choice for the qlearning algorithm + def step(self, role_number: int, learningRate: float): + P = self.get_possible_states(role_number) #gets all the relevent players + r = rd.uniform(0, 1) + if r < learningRate: # 50% chance of this happening + rs = rd.randrange(0, len(self.States) - 1) + if role_number == 1: + while ( + self.States[rs].person is not None + and self.States[rs].person.isZombie + ): #picks a relevent person, but idk why its not via all the possible people + rs = rd.randrange(0, len(self.States) - 1) #and instead searches all the states again + else: + while ( + self.States[rs].person is not None + and self.States[rs].person.isZombie == False #same thing but for zombie + ): + rs = rd.randrange(0, len(self.States) - 1) + + # random state and value + # old_value = QTable[state][acti] + # next_max = np.max(QTable[next_state]) + # new_value = (1 - alpha) * old_value + alpha * (reward + gamma * next_max) + # QTable[state][acti] = new_value + + #adds the people into the grid + def populate(self): + + self.anxiety = 0 + self.outrage = 0 + + #make between 7 and boardsize/3 people + allppl = rd.sample(range(len(self.States)), rd.randint(7, ((self.rows * self.columns) / 3))) + for state in range(len(self.States)): + self.States[state].person = None + if state in allppl: + self.States[state].person = Person(False) + self.population += 1 + + #turn half the humans into zombies + allzombs = rd.sample(range(len(allppl)), len(allppl)//4) + for person in allzombs: + self.States[allppl[person]].person.isZombie = True + + #add two safe spaces + noZombieInSafe = False + while not noZombieInSafe: + allsafes = rd.sample(range(len(self.States)), rd.randint(1, (self.rows*self.columns)//15)) + for state in range(len(self.States)): + if ( + self.States[state].person is not None + and self.States[state].person.isZombie + ): + continue + else: + noZombieInSafe = True + + if state in allsafes: + self.States[state].safeSpace = True diff --git a/Constants.py b/Constants.py new file mode 100644 index 0000000..aad47b0 --- /dev/null +++ b/Constants.py @@ -0,0 +1,41 @@ +import enum +import pygame +import os +pygame.mixer.init() + + +ROWS = 6 +COLUMNS = 6 +BORDER = 150 # Number of pixels to offset grid to the top-left side +CELL_DIMENSIONS = (100, 100) # Number of pixels (x,y) for each cell +SELF_PLAY = True # whether or not a human will be playing +KILL_SOUND = pygame.mixer.Sound(os.path.join('Assets', 'Kill sound.mp3')) + +class Action(enum.Enum): + move = 1 + bite = 2 + heal = 3 + kill = 4 + +class Direction(enum.Enum): + self = 0 + up = 1 + down = 2 + left = 3 + right = 4 + +class Role(enum.Enum): + government = 0 + zombie = 1 + +class Result(enum.Enum): + invalid = 0 + success = 1 + failure = 2 + +reverse_dir = { + Direction.up: Direction.down, + Direction.down: Direction.up, + Direction.left: Direction.right, + Direction.right: Direction.left +} diff --git a/SGAI_MK3/Person.py b/Person.py similarity index 95% rename from SGAI_MK3/Person.py rename to Person.py index 7c75b28..43adbc4 100644 --- a/SGAI_MK3/Person.py +++ b/Person.py @@ -1,13 +1,17 @@ import random as rd +from constants import Direction + class Person: def __init__(self, iz: bool): + self.facing = Direction.up self.isZombie = iz self.wasVaccinated = False self.turnsVaccinated = 0 self.isVaccinated = False self.wasCured = False + self.hasMed = False def clone(self): ret = Person(self.isZombie) diff --git a/PygameFunctions.py b/PygameFunctions.py new file mode 100644 index 0000000..9f5d5b5 --- /dev/null +++ b/PygameFunctions.py @@ -0,0 +1,352 @@ +from typing import Tuple +import pygame +from constants import * +from Board import Board +from math import tanh + + +# constants +BACKGROUND = "#DDC2A1" +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +CELL_COLOR = (233, 222, 188) +SAFE_COLOR = (93, 138, 168) +LINE_WIDTH = 5 +GAME_WINDOW_DIMENSIONS = (1200, 800) +RESET_MOVE_COORDS = (800, 600) +RESET_MOVE_DIMS = (200, 50) + +# Initialize pygame +screen = pygame.display.set_mode(GAME_WINDOW_DIMENSIONS) +pygame.display.set_caption("Outbreak!") +pygame.font.init() +font = pygame.font.SysFont("Impact", 30) +pygame.display.set_caption("Outbreak!") +screen.fill(BACKGROUND) + + +def get_action(GameBoard: Board, pixel_x: int, pixel_y: int): + """ + Get the action that the click represents. + If the click was on the heal or kill button, returns Action.heal or Action.kill respectively + Else, returns the board coordinates of the click (board_x, board_y) if valid + Return None otherwise + """ + # Check if the user clicked on the "heal" icon, return "heal" if so + + heal_bite_check = pixel_x >= 900 and pixel_x <= 1100 and pixel_y > 190 and pixel_y < 301 + kill_check = pixel_x >= 800 and pixel_x <= 900 and pixel_y > 199 and pixel_y < 301 + Med_check = pixel_x >= 800 and pixel_x <= 900 and pixel_y > 301 and pixel_y < 401 + reset_move_check = ( + pixel_x >= RESET_MOVE_COORDS[0] + and pixel_x <= RESET_MOVE_COORDS[0] + RESET_MOVE_DIMS[0] + and pixel_y >= RESET_MOVE_COORDS[1] + and pixel_y <= RESET_MOVE_COORDS[1] + RESET_MOVE_DIMS[1] + ) + board_x = int((pixel_x - 150) / 100) + board_y = int((pixel_y - 150) / 100) + move_check = ( + board_x >= 0 + and board_x < GameBoard.columns + and board_y >= 0 + and board_y < GameBoard.rows + ) + board_coords = (int((pixel_x - 150) / 100), int((pixel_y - 150) / 100)) + + if heal_bite_check: + if GameBoard.player_role == Role.government: + return Action.heal + else: + return Action.bite + elif Med_check: + return "Distrb Med" + elif kill_check: + return Action.kill + elif reset_move_check: + return "reset move" + elif move_check: + return board_coords + return None + + +def run(GameBoard: Board): + """ + Draw the screen and return any events. + """ + screen.fill(BACKGROUND) + build_grid(GameBoard) # Draw the grid + + # Draw the heal icon + if GameBoard.player_role == Role.government: + display_image(screen, "Assets/cure.png", GameBoard.display_cell_dimensions, (950, 200)) + display_image(screen, "Assets/kill.png", GameBoard.display_cell_dimensions, (800, 200)) + display_image(screen, "Assets/RedCross.png", GameBoard.display_cell_dimensions, (800, 300)) + else: + display_image(screen, "Assets/bite.png", GameBoard.display_cell_dimensions, (950, 200)) + #Draw the kill button slightly to the left of heal + display_people(GameBoard) + display_reset_move_button() + screen.blit(font.render(f"public outrage: {int(GameBoard.outrage)} %", True, WHITE), (10, 10)) + screen.blit(font.render(f"public anxiety: {int(GameBoard.anxiety)} %", True, WHITE), (10, 40)) + return pygame.event.get() + + +def disp_title_screen(): + """ + Displays a basic title screen with title, start button, and quit button + """ + start_text = font.render('START', True, WHITE) + quit_text = font.render('QUIT', True, WHITE) + screen.fill(BACKGROUND) + #Draw title + display_image(screen, "Assets/Outbreak_title.png", (1048, 238), (76, 100)) + #Check if the user has clicked either start or quit + while True: + mouse = pygame.mouse.get_pos() + #Draw the start and quit buttons (They might need a little bit more work at some point, they're not centered well) + pygame.draw.rect(screen,BLACK,[500,350,200,100]) + pygame.draw.rect(screen,BLACK,[500,500,200,100]) + screen.blit(start_text, (560, 375)) + screen.blit(quit_text, (570, 525)) + for i in pygame.event.get(): + if i.type == pygame.MOUSEBUTTONDOWN: + if 500 <= mouse[0] <= 700 and 350 <= mouse[1] <= 450: + return True + elif 500 <= mouse[0] <= 700 and 500 <= mouse[1] <= 600: + pygame.display.quit() + break + pygame.display.update() + +def display_safe_space(GameBoard): + """ + Creates a blue rectangle at every safe space state + """ + for state in GameBoard.States: + if state.safeSpace: + coords = ( + int(GameBoard.toCoord(state.location)[0]) * GameBoard.display_cell_dimensions[0] + + GameBoard.display_border, + int(GameBoard.toCoord(state.location)[1]) * GameBoard.display_cell_dimensions[1] + + GameBoard.display_border, + ) + #draw a rectangle of dimensions 100x100 at the coordinates created above + pygame.draw.rect(screen, SAFE_COLOR, pygame.Rect(coords[0], coords[1], 100, 100)) + + +def display_reset_move_button(): + rect = pygame.Rect( + RESET_MOVE_COORDS[0], + RESET_MOVE_COORDS[1], + RESET_MOVE_DIMS[0], + RESET_MOVE_DIMS[1], + ) + pygame.draw.rect(screen, BLACK, rect) + screen.blit(font.render("Reset move?", True, WHITE), RESET_MOVE_COORDS) + + +def display_image( + screen: pygame.Surface, + itemStr: str, + dimensions: Tuple[int, int], + position: Tuple[int, int], +): + """ + Draw an image on the screen at the indicated position. + """ + v = pygame.image.load(itemStr).convert_alpha() + v = pygame.transform.scale(v, dimensions) + screen.blit(v, position) + + +def build_grid(GameBoard: Board): + """ + Draw the grid on the screen. + """ + + grid_width = GameBoard.columns * GameBoard.display_cell_dimensions[0] + grid_height = GameBoard.rows * GameBoard.display_cell_dimensions[1] + # left + pygame.draw.rect( + screen, + BLACK, + [ + GameBoard.display_border - LINE_WIDTH, + GameBoard.display_border - LINE_WIDTH, + LINE_WIDTH, + grid_height + (2 * LINE_WIDTH), + ], + ) + # right + pygame.draw.rect( + screen, + BLACK, + [ + GameBoard.display_border + grid_width, + GameBoard.display_border - LINE_WIDTH, + LINE_WIDTH, + grid_height + (2 * LINE_WIDTH), + ], + ) + # bottom + pygame.draw.rect( + screen, + BLACK, + [ + GameBoard.display_border - LINE_WIDTH, + GameBoard.display_border + grid_height, + grid_width + (2 * LINE_WIDTH), + LINE_WIDTH, + ], + ) + # top + pygame.draw.rect( + screen, + BLACK, + [ + GameBoard.display_border - LINE_WIDTH, + GameBoard.display_border - LINE_WIDTH, + grid_width + (2 * LINE_WIDTH), + LINE_WIDTH, + ], + ) + # Fill the inside wioth the cell color + pygame.draw.rect( + screen, + CELL_COLOR, + [GameBoard.display_border, GameBoard.display_border, grid_width, grid_height], + ) + #Draw the safe space so that it is under the lines + display_safe_space(GameBoard) + # Draw the vertical lines + i = GameBoard.display_border + GameBoard.display_cell_dimensions[0] + while i < GameBoard.display_border + grid_width: + pygame.draw.rect( + screen, BLACK, [i, GameBoard.display_border, LINE_WIDTH, grid_height] + ) + i += GameBoard.display_cell_dimensions[0] + # Draw the horizontal lines + i = GameBoard.display_border + GameBoard.display_cell_dimensions[1] + while i < GameBoard.display_border + grid_height: + pygame.draw.rect( + screen, BLACK, [GameBoard.display_border, i, grid_width, LINE_WIDTH] + ) + i += GameBoard.display_cell_dimensions[1] + + +def display_people(GameBoard: Board): + """ + Draw the people (government, vaccinated, and zombies) on the grid. + """ + for x in range(len(GameBoard.States)): + if GameBoard.States[x].person != None: + p = GameBoard.States[x].person + char = "Assets/person_normal.png" + if p.isZombie: + char = "Assets/person_zombie.png" + elif p.isVaccinated: + char = "Assets/person_normal.png" + coords = ( + int(x % GameBoard.rows) * GameBoard.display_cell_dimensions[0] + + GameBoard.display_border + + 35, + int(x / GameBoard.columns) * GameBoard.display_cell_dimensions[1] + + GameBoard.display_border + + 20, + ) + display_image(screen, char, (35, 60), coords) + if p.hasMed == True: + display_image(screen, "Assets/RedCross.png", (20, 20), (coords[0]+25, coords[1])) + +#Creates buttons that allow the player to quit or restart +def display_win_screen(): + restart_text = font.render('PLAY AGAIN', True, WHITE) + quit_text = font.render('QUIT', True, WHITE) + screen.fill(BACKGROUND) + screen.blit( + font.render("You win!", True, WHITE), + (500, 350), + ) + screen.blit( + font.render("There were no possible moves for the computer.", True, WHITE), + (500, 400), + ) + + while True: + mouse = pygame.mouse.get_pos() + pygame.draw.rect(screen,BLACK,[500,450,200,100]) + pygame.draw.rect(screen,BLACK,[500,600,200,100]) + screen.blit(restart_text, (550, 475)) + screen.blit(quit_text, (570, 625)) + for i in pygame.event.get(): + if i.type == pygame.MOUSEBUTTONDOWN: + if 500 <= mouse[0] <= 700 and 450 <= mouse[1] <= 550: + return True + elif 500 <= mouse[0] <= 700 and 600 <= mouse[1] <= 700: + return False + break + pygame.display.update() + + # catch quit event + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + +#similar code, just for a loss case +def display_lose_screen(): + restart_text = font.render('PLAY AGAIN', True, WHITE) + quit_text = font.render('QUIT', True, WHITE) + + screen.fill(BACKGROUND) + screen.blit( + font.render("You lose!", True, WHITE), + (500, 350), + ) + screen.blit( + font.render("You had no possible moves...", True, WHITE), + (500, 400), + ) + + while True: + mouse = pygame.mouse.get_pos() + pygame.draw.rect(screen,BLACK,[500,450,200,100]) + pygame.draw.rect(screen,BLACK,[500,600,200,100]) + screen.blit(restart_text, (550, 475)) + screen.blit(quit_text, (570, 625)) + for i in pygame.event.get(): + if i.type == pygame.MOUSEBUTTONDOWN: + if 500 <= mouse[0] <= 700 and 450 <= mouse[1] <= 550: + return True + elif 500 <= mouse[0] <= 700 and 600 <= mouse[1] <= 700: + return False + break + pygame.display.update() + + # catch quit event + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + +#gets the reward for a certain action +def get_reward(action): + if action == Action.move: + return 10 + elif action == Action.heal: + return 1000 + elif action == Action.kill: + return 100 + elif action == Action.bite: + return -100 + +def direction(coord1: Tuple[int, int], coord2: Tuple[int, int]): + if coord1 == coord2: + return Direction.self + elif coord2[1] > coord1[1]: + return Direction.down + elif coord2[1] < coord1[1]: + return Direction.up + elif coord2[0] > coord1[0]: + return Direction.right + elif coord2[0] < coord1[0]: + return Direction.left diff --git a/README.md b/README.md index 17b4fdf..331f4ec 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# SGAI - Outbreak +# SGAI - DRHAT - Outbreak This is the repository that Beaverworks' SGAI 2022 will be using to understand serious games and reinforcement learning. -## Team Courtney II +## Team Courtney I - DR. HAT! The best team out there. ## How to run @@ -12,19 +12,24 @@ You must open the folder SGAI_MK3 with vscode. Then, you can run main.py from VS Code. ### cmd line version First, `cd ./SGAI_MK3`. Then, `python main.py` +### PyCharm version +Download the zip file and open SGAI-DRHAT-Outbreak. Ensure that numpy and pygame are installed, then run main.py. ## How to play +This game implements a KILL/CURE option, where you choose if you want to kill a zombie or cure it. +Pay attention to the public outrage and public anxiety, as if they get too high, you lose. There are basic moves: - Move - click on a person that you control and a square next to them. If the square isn't occupied, the person will move to that square. -- Bite - If you are playing as a zombie, -you can click the bite button and then a person next to a zombie -to turn the person into a zombie. NOTE: THIS DOES NOT ALWAYS SUCCEED -BECAUSE GAME MECHANICS MAKE IT SO THAT THERE IS A CHANCE THAT BITING WILL -FAIL. THIS IS NOT A BUG. THIS IS A GAME MECHANIC. +- Bite - If you are playing as a zombie, +you can click the bite button and a zombie and then a person. +to turn the person into a zombie. - Heal - If you are playing as the government, you -can click the cure button and a person or zombie. -If you clicked a zombie, the zombie will become a person again -(this is curing). If you clicked a person, the person will become -vaccinated (this is vaccination). Vaccination lasts for 5 turns and -gives 100% immunity to being zombified. \ No newline at end of file +can click the cure button and a person (the healer) and then a zombie (the healee). +There is 50% chance that the zombie will be healed and become a person again +(this is curing). If the zombie is not healed, the healer may become a zombie. +If they are healed, they now have 100% immunity to being zombified. +- Kill - If you are playing as the government, you can click the kill button and a person (the killer) and then a zombie (the victim). +Killing ensures that the zombie goes away with no chance of infection, however, it spikes the public's outrage, so choose carefully. +- Vaccinate - In order to vaccinate a human, you must move into a "safe space". Safe spaces are slate blue on the board. Clicking on the red plus symbol will vaccinate everyone in a safe space. There can be up to two safe spaces, and up to one person in each safe space. + diff --git a/SGAI_MK3/.DS_Store b/SGAI_MK3/.DS_Store deleted file mode 100644 index 59beab6..0000000 Binary files a/SGAI_MK3/.DS_Store and /dev/null differ diff --git a/SGAI_MK3/Assets/bite.png b/SGAI_MK3/Assets/bite.png deleted file mode 100644 index 5fa384a..0000000 Binary files a/SGAI_MK3/Assets/bite.png and /dev/null differ diff --git a/SGAI_MK3/Assets/cure.jpeg b/SGAI_MK3/Assets/cure.jpeg deleted file mode 100644 index b50956d..0000000 Binary files a/SGAI_MK3/Assets/cure.jpeg and /dev/null differ diff --git a/SGAI_MK3/Assets/person_normal.png b/SGAI_MK3/Assets/person_normal.png deleted file mode 100644 index 2309f02..0000000 Binary files a/SGAI_MK3/Assets/person_normal.png and /dev/null differ diff --git a/SGAI_MK3/Assets/person_zombie.png b/SGAI_MK3/Assets/person_zombie.png deleted file mode 100644 index 3701546..0000000 Binary files a/SGAI_MK3/Assets/person_zombie.png and /dev/null differ diff --git a/SGAI_MK3/Board.py b/SGAI_MK3/Board.py deleted file mode 100644 index d041d30..0000000 --- a/SGAI_MK3/Board.py +++ /dev/null @@ -1,362 +0,0 @@ -from State import State -import random as rd -from Person import Person -from typing import List, Tuple -from constants import * - - -class Board: - def __init__( - self, - dimensions: Tuple[int, int], - player_role: str, - ): - self.rows = dimensions[0] - self.columns = dimensions[1] - self.player_role = player_role - self.player_num = ROLE_TO_ROLE_NUM[player_role] - self.population = 0 - self.States = [] - self.QTable = [] - for s in range(dimensions[0] * dimensions[1]): - self.States.append(State(None, s)) - self.QTable.append([0] * 6) - - self.actionToFunction = { - "moveUp": self.moveUp, - "moveDown": self.moveDown, - "moveLeft": self.moveLeft, - "moveRight": self.moveRight, - "heal": self.heal, - "bite": self.bite, - } - - def num_zombies(self) -> int: - r = 0 - for state in self.States: - if state.person != None: - if state.person.isZombie: - r += 1 - return r - - def act(self, oldstate: Tuple[int, int], givenAction: str): - cell = self.toCoord(oldstate) - f = self.actionToFunction[givenAction](cell) - reward = self.States[oldstate].evaluate(givenAction, self) - if f[0] == False: - reward = 0 - return [reward, f[1]] - - def containsPerson(self, isZombie: bool): - for state in self.States: - if state.person is not None and state.person.isZombie == isZombie: - return True - return False - - def get_possible_moves(self, action: str, role: str): - """ - Get the coordinates of people (or zombies) that are able - to make the specified move. - @param action - the action to return possibilities for (options are 'bite', 'moveUp', 'moveDown','moveLeft', 'moveRight', and 'heal') - @param role - either 'Zombie' or 'Government'; helps decide whether an action - is valid and which people/zombies it applies to - """ - poss = [] - B = self.clone(self.States, role) - - if role == "Zombie": - if not self.containsPerson(True): - return poss - for idx in range(len(self.States)): - state = self.States[idx] - if state.person is not None: - changed_states = False - - if ( - action == "bite" - and not state.person.isZombie - and self.isAdjacentTo(self.toCoord(idx), True) - ): - # if the current space isn't a zombie and it is adjacent - # a space that is a zombie - poss.append(B.toCoord(idx)) - changed_states = True - elif ( - action != "bite" - and state.person.isZombie - and B.actionToFunction[action](B.toCoord(idx))[0] - ): - poss.append(B.toCoord(idx)) - changed_states = True - - if changed_states: - # reset the states - B.States = [ - self.States[i].clone() - if self.States[i] != B.States[i] - else B.States[i] - for i in range(len(self.States)) - ] - - elif role == "Government": - if not self.containsPerson(False): - return poss - for idx in range(len(self.States)): - state = self.States[idx] - if state.person is not None: - changed_states = False - if action == "heal" and ( - state.person.isZombie or not state.person.isVaccinated - ): - poss.append(B.toCoord(idx)) - changed_states = True - elif ( - action != "heal" - and not state.person.isZombie - and B.actionToFunction[action](B.toCoord(idx))[0] - ): - poss.append(B.toCoord(idx)) - changed_states = True - - if changed_states: - # reset the states - B.States = [ - self.States[i].clone() - if self.States[i] != B.States[i] - else B.States[i] - for i in range(len(self.States)) - ] - return poss - - def toCoord(self, i: int): - return (int(i % self.columns), int(i / self.rows)) - - def toIndex(self, coordinates: Tuple[int, int]): - return int(coordinates[1] * self.columns) + int(coordinates[0]) - - def isValidCoordinate(self, coordinates: Tuple[int, int]): - return ( - coordinates[1] < self.rows - and coordinates[1] >= 0 - and coordinates[0] < self.columns - and coordinates[0] >= 0 - ) - - def clone(self, L: List[State], role: str): - NB = Board( - (self.rows, self.columns), - self.player_role, - ) - NB.States = [state.clone() for state in L] - NB.player_role = role - return NB - - def isAdjacentTo(self, coord: Tuple[int, int], is_zombie: bool) -> bool: - ret = False - vals = [ - (coord[0], coord[1] + 1), - (coord[0], coord[1] - 1), - (coord[0] + 1, coord[1]), - (coord[0] - 1, coord[1]), - ] - for coord in vals: - if ( - self.isValidCoordinate(coord) - and self.States[self.toIndex(coord)].person is not None - and self.States[self.toIndex(coord)].person.isZombie == is_zombie - ): - ret = True - break - - return ret - - def move( - self, from_coords: Tuple[int, int], new_coords: Tuple[int, int] - ) -> Tuple[bool, int]: - """ - Check if the move is valid. - If valid, then implement the move and return [True, destination_idx] - If invalid, then return [False, None] - If the space is currently occupied, then return [False, destination_idx] - """ - # Get the start and destination index (1D) - start_idx = self.toIndex(from_coords) - destination_idx = self.toIndex(new_coords) - - # Check if the new coordinates are valid - if not self.isValidCoordinate(new_coords): - return [False, destination_idx] - - # Check if the destination is currently occupied - if self.States[destination_idx].person is None: - self.States[destination_idx].person = self.States[start_idx].person - self.States[start_idx].person = None - return [True, destination_idx] - return [False, destination_idx] - - def moveUp(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - new_coords = (coords[0], coords[1] - 1) - return self.move(coords, new_coords) - - def moveDown(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - new_coords = (coords[0], coords[1] + 1) - return self.move(coords, new_coords) - - def moveLeft(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - new_coords = (coords[0] - 1, coords[1]) - return self.move(coords, new_coords) - - def moveRight(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - new_coords = (coords[0] + 1, coords[1]) - return self.move(coords, new_coords) - - def QGreedyat(self, state_id: int): - biggest = self.QTable[state_id][0] * self.player_num - ind = 0 - A = self.QTable[state_id] - i = 0 - for qval in A: - if (qval * self.player_num) > biggest: - biggest = qval - ind = i - i += 1 - return [ind, self.QTable[ind]] # action_index, qvalue - - def choose_action(self, state_id: int, lr: float): - L = lr * 100 - r = rd.randint(0, 100) - if r < L: - return self.QGreedyat(state_id) - else: - if self.player_num == 1: # Player is Govt - d = rd.randint(0, 4) - else: - d = rd.randint(0, 5) - while d != 4: - d = rd.randint(0, 4) - return d - - def choose_state(self, lr: float): - L = lr * 100 - r = rd.randint(0, 100) - if r < L: - biggest = None - sid = None - for x in range(len(self.States)): - if self.States[x].person != None: - q = self.QGreedyat(x) - if biggest is None: - biggest = q[1] - sid = x - elif q[1] > biggest: - biggest = q[1] - sid = x - return self.QGreedyat(sid) - else: - if self.player_num == -1: # Player is Govt - d = rd.randint(0, len(self.States)) - while self.States[d].person is None or self.States[d].person.isZombie: - d = rd.randint(0, len(self.States)) - else: - d = rd.randint(0, len(self.States)) - while ( - self.States[d].person is None - or self.States[d].person.isZombie == False - ): - d = rd.randint(0, len(self.States)) - return d - - def bite(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - i = self.toIndex(coords) - if ( - self.States[i].person is None - or self.States[i].person.isZombie - or not self.isAdjacentTo(coords, True) - ): - return [False, None] - self.States[i].person.get_bitten() - return [True, i] - - def heal(self, coords: Tuple[int, int]) -> Tuple[bool, int]: - """ - Cures or vaccinates the person at the stated coordinates. - If there is a zombie there, the person will be cured. - If there is a person there, the person will be vaccinated - If no person is selected, then return [False, None] - if a person is vaccined, then return [True, index] - """ - i = self.toIndex(coords) - if self.States[i].person is None: - return [False, None] - p = self.States[i].person - - if p.isZombie: - p.get_cured() - else: - p.get_vaccinated() - return [True, i] - - def get_possible_states(self, role_number: int): - indexes = [] - i = 0 - for state in self.States: - if state.person != None: - if role_number == 1 and state.person.isZombie == False: - indexes.append(i) - elif role_number == -1 and state.person.isZombie: - indexes.append(i) - i += 1 - return indexes - - def step(self, role_number: int, learningRate: float): - P = self.get_possible_states(role_number) - r = rd.uniform(0, 1) - if r < learningRate: - rs = rd.randrange(0, len(self.States) - 1) - if role_number == 1: - while ( - self.States[rs].person is not None - and self.States[rs].person.isZombie - ): - rs = rd.randrange(0, len(self.States) - 1) - else: - while ( - self.States[rs].person is not None - and self.States[rs].person.isZombie == False - ): - rs = rd.randrange(0, len(self.States) - 1) - - # random state and value - # old_value = QTable[state][acti] - # next_max = np.max(QTable[next_state]) - # new_value = (1 - alpha) * old_value + alpha * (reward + gamma * next_max) - # QTable[state][acti] = new_value - - def populate(self): - total = rd.randint(7, ((self.rows * self.columns) / 3)) - poss = [] - for x in range(len(self.States)): - r = rd.randint(0, 100) - if r < 60 and self.population < total: - p = Person(False) - self.States[x].person = p - self.population = self.population + 1 - poss.append(x) - else: - self.States[x].person = None - used = [] - for x in range(4): - s = rd.randint(0, len(poss) - 1) - while s in used: - s = rd.randint(0, len(poss) - 1) - self.States[poss[s]].person.isZombie = True - used.append(s) - - def update(self): - """ - Update each of the states; - This method should be called at the end of each round - (after player and computer have each gone once) - """ - for state in self.States: - state.update() diff --git a/SGAI_MK3/PygameFunctions.py b/SGAI_MK3/PygameFunctions.py deleted file mode 100644 index 328cde1..0000000 --- a/SGAI_MK3/PygameFunctions.py +++ /dev/null @@ -1,245 +0,0 @@ -from typing import List, Tuple -import pygame -from constants import * -from Board import Board - - -# Initialize pygame -screen = pygame.display.set_mode(GAME_WINDOW_DIMENSIONS) -pygame.display.set_caption("Outbreak!") -pygame.font.init() -font = pygame.font.SysFont("Comic Sans", 20) -screen.fill(BACKGROUND) - - -def get_action(GameBoard: Board, pixel_x: int, pixel_y: int): - """ - Get the action that the click represents. - If the click was on the heal button, returns "heal" - Else, returns the board coordinates of the click (board_x, board_y) if valid - Return None otherwise - """ - # Check if the user clicked on the "heal" or "bite" icon, return "heal" or "bite" if so - heal_bite_check = ( - pixel_x >= CURE_BITE_COORDS[0] - and pixel_x <= CURE_BITE_COORDS[0] + CURE_BITE_DIMS[0] - and pixel_y >= CURE_BITE_COORDS[1] - and pixel_y <= CURE_BITE_COORDS[1] + CURE_BITE_DIMS[1] - ) - reset_move_check = ( - pixel_x >= RESET_MOVE_COORDS[0] - and pixel_x <= RESET_MOVE_COORDS[0] + RESET_MOVE_DIMS[0] - and pixel_y >= RESET_MOVE_COORDS[1] - and pixel_y <= RESET_MOVE_COORDS[1] + RESET_MOVE_DIMS[1] - ) - board_x = int((pixel_x - MARGIN) / CELL_DIMENSIONS[0]) - board_y = int((pixel_y - MARGIN) / CELL_DIMENSIONS[1]) - move_check = ( - board_x >= 0 - and board_x < GameBoard.columns - and board_y >= 0 - and board_y < GameBoard.rows - ) - - if heal_bite_check: - if GameBoard.player_role == "Government": - return "heal" - return "bite" - elif reset_move_check: - return "reset move" - elif move_check: - return board_x, board_y - return None - - -def run(GameBoard: Board): - """ - Draw the screen and return any events. - """ - screen.fill(BACKGROUND) - build_grid(GameBoard) # Draw the grid - # Draw the heal icon - if GameBoard.player_role == "Government": - display_image(screen, "Assets/cure.jpeg", CURE_BITE_DIMS, CURE_BITE_COORDS) - else: - display_image(screen, "Assets/bite.png", CURE_BITE_DIMS, CURE_BITE_COORDS) - display_people(GameBoard) - display_reset_move_button() - return pygame.event.get() - - -def display_reset_move_button(): - rect = pygame.Rect( - RESET_MOVE_COORDS[0], - RESET_MOVE_COORDS[1], - RESET_MOVE_DIMS[0], - RESET_MOVE_DIMS[1], - ) - pygame.draw.rect(screen, BLACK, rect) - screen.blit(font.render("Reset move?", True, WHITE), RESET_MOVE_COORDS) - - -def display_image( - screen: pygame.Surface, - itemStr: str, - dimensions: Tuple[int, int], - position: Tuple[int, int], -): - """ - Draw an image on the screen at the indicated position. - """ - v = pygame.image.load(itemStr).convert_alpha() - v = pygame.transform.scale(v, dimensions) - screen.blit(v, position) - - -def build_grid(GameBoard: Board): - """ - Draw the grid on the screen. - """ - grid_width = GameBoard.columns * CELL_DIMENSIONS[0] - grid_height = GameBoard.rows * CELL_DIMENSIONS[1] - # left - pygame.draw.rect( - screen, - BLACK, - [ - MARGIN - LINE_WIDTH, - MARGIN - LINE_WIDTH, - LINE_WIDTH, - grid_height + (2 * LINE_WIDTH), - ], - ) - # right - pygame.draw.rect( - screen, - BLACK, - [ - MARGIN + grid_width, - MARGIN - LINE_WIDTH, - LINE_WIDTH, - grid_height + (2 * LINE_WIDTH), - ], - ) - # bottom - pygame.draw.rect( - screen, - BLACK, - [ - MARGIN - LINE_WIDTH, - MARGIN + grid_height, - grid_width + (2 * LINE_WIDTH), - LINE_WIDTH, - ], - ) - # top - pygame.draw.rect( - screen, - BLACK, - [ - MARGIN - LINE_WIDTH, - MARGIN - LINE_WIDTH, - grid_width + (2 * LINE_WIDTH), - LINE_WIDTH, - ], - ) - # Fill the inside wioth the cell color - pygame.draw.rect( - screen, - CELL_COLOR, - [MARGIN, MARGIN, grid_width, grid_height], - ) - - # Draw the vertical lines - i = MARGIN + CELL_DIMENSIONS[0] - while i < MARGIN + grid_width: - pygame.draw.rect(screen, BLACK, [i, MARGIN, LINE_WIDTH, grid_height]) - i += CELL_DIMENSIONS[0] - # Draw the horizontal lines - i = MARGIN + CELL_DIMENSIONS[1] - while i < MARGIN + grid_height: - pygame.draw.rect(screen, BLACK, [MARGIN, i, grid_width, LINE_WIDTH]) - i += CELL_DIMENSIONS[1] - - -def display_people(GameBoard: Board): - """ - Draw the people (government, vaccinated, and zombies) on the grid. - """ - for x in range(len(GameBoard.States)): - if GameBoard.States[x].person != None: - p = GameBoard.States[x].person - char = "Assets/" + IMAGE_ASSETS[0] - if p.isVaccinated: - char = "Assets/" + IMAGE_ASSETS[1] - elif p.isZombie: - char = "Assets/" + IMAGE_ASSETS[2] - coords = ( - int(x % GameBoard.rows) * CELL_DIMENSIONS[0] + MARGIN + 35, - int(x / GameBoard.columns) * CELL_DIMENSIONS[1] + MARGIN + 20, - ) - display_image(screen, char, (35, 60), coords) - - -def display_cur_move(cur_move: List): - # Display the current action - screen.blit( - font.render("Your move is currently:", True, WHITE), - CUR_MOVE_COORDS, - ) - screen.blit( - font.render(f"{cur_move}", True, WHITE), - ( - CUR_MOVE_COORDS[0], - CUR_MOVE_COORDS[1] + font.size("Your move is currently:")[1] * 2, - ), - ) - - -def display_win_screen(): - screen.fill(BACKGROUND) - screen.blit( - font.render("You win!", True, WHITE), - (500, 350), - ) - screen.blit( - font.render("There were no possible moves for the computer.", True, WHITE), - (500, 400), - ) - pygame.display.update() - - # catch quit event - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - return - - -def display_lose_screen(): - screen.fill(BACKGROUND) - screen.blit( - font.render("You lose!", True, WHITE), - (500, 350), - ) - screen.blit( - font.render("You had no possible moves...", True, WHITE), - (500, 400), - ) - pygame.display.update() - - # catch quit event - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - return - - -def direction(coord1: Tuple[int, int], coord2: Tuple[int, int]): - if coord2[1] > coord1[1]: - return "moveDown" - elif coord2[1] < coord1[1]: - return "moveUp" - elif coord2[0] > coord1[0]: - return "moveRight" - elif coord2[0] < coord1[0]: - return "moveLeft" diff --git a/SGAI_MK3/constants.py b/SGAI_MK3/constants.py deleted file mode 100644 index 52440c6..0000000 --- a/SGAI_MK3/constants.py +++ /dev/null @@ -1,28 +0,0 @@ -# Constants -ROWS = 6 -COLUMNS = 6 -ACTION_SPACE = ["moveUp", "moveDown", "moveLeft", "moveRight", "heal", "bite"] - -# Player role variables -ROLE_TO_ROLE_NUM = {"Government": 1, "Zombie": -1} -ROLE_TO_ROLE_BOOLEAN = {"Government": False, "Zombie": True} - -# Pygame constants -BACKGROUND = "#DDC2A1" -BLACK = (0, 0, 0) -WHITE = (255, 255, 255) -CELL_COLOR = (233, 222, 188) -LINE_WIDTH = 5 -IMAGE_ASSETS = [ - "person_normal.png", - "person_vax.png", - "person_zombie.png", -] -GAME_WINDOW_DIMENSIONS = (1200, 800) -RESET_MOVE_COORDS = (800, 600) -RESET_MOVE_DIMS = (200, 50) -CURE_BITE_COORDS = (950, 200) -CURE_BITE_DIMS = (200, 200) -CELL_DIMENSIONS = (100, 100) # number of pixels (x, y) for each cell -CUR_MOVE_COORDS = (800, 400) -MARGIN = 150 # Number of pixels to offset grid to the top-left side diff --git a/SGAI_MK3/main.py b/SGAI_MK3/main.py index 49990b4..e0e8b15 100644 --- a/SGAI_MK3/main.py +++ b/SGAI_MK3/main.py @@ -1,13 +1,21 @@ +#from msilib.schema import Class import pygame from Board import Board import PygameFunctions as PF import random as rd from constants import * +import time + +# Player role variables +player_role = Role.government # Valid options are Role.government and Role.zombie +roleToRoleNum = {Role.government: 1, Role.zombie: -1} + +#initialize sound effect + + -SELF_PLAY = True # whether or not a human will be playing -player_role = "Zombie" # Valid options are "Government" and "Zombie" # Create the game board -GameBoard = Board((ROWS, COLUMNS), player_role) +GameBoard = Board((ROWS, COLUMNS), BORDER, CELL_DIMENSIONS, player_role) GameBoard.populate() # Self play variables @@ -23,90 +31,110 @@ running = True take_action = [] playerMoved = False +font = pygame.font.SysFont("Comic Sans", 20) + while running: P = PF.run(GameBoard) - if SELF_PLAY: - if not playerMoved: - if not GameBoard.containsPerson(False): - PF.display_lose_screen() - running = False - continue - # Event Handling - for event in P: - if event.type == pygame.MOUSEBUTTONUP: - x, y = pygame.mouse.get_pos() - action = PF.get_action(GameBoard, x, y) - if action == "heal" or action == "bite": - # only allow healing by itself (prevents things like ['move', (4, 1), 'heal']) - if len(take_action) == 0: - take_action.append(action) - elif action == "reset move": - take_action = [] - elif action is not None: - idx = GameBoard.toIndex(action) - # action is a coordinate - if idx < (GameBoard.rows * GameBoard.columns) and idx > -1: - if "move" not in take_action and len(take_action) == 0: - # make sure that the space is not an empty space or a space of the opposite team - # since cannot start a move from those invalid spaces - if ( - GameBoard.States[idx].person is not None - and GameBoard.States[idx].person.isZombie - == ROLE_TO_ROLE_BOOLEAN[player_role] - ): - take_action.append("move") - else: - continue - - # don't allow duplicate cells - if action not in take_action: - take_action.append(action) - if event.type == pygame.QUIT: - running = False - - PF.display_cur_move(take_action) - - # Action handling - if len(take_action) > 1: - if take_action[0] == "move": - if len(take_action) > 2: - directionToMove = PF.direction(take_action[1], take_action[2]) - result = GameBoard.actionToFunction[directionToMove]( - take_action[1] - ) - if result[0] is not False: - playerMoved = True - take_action = [] - - elif take_action[0] == "heal" or take_action[0] == "bite": - result = GameBoard.actionToFunction[take_action[0]](take_action[1]) - if result[0] is not False: - playerMoved = True + if not GameBoard.containsPerson(bool(player_role.value)): + PF.display_lose_screen() + running = False + continue + # Event Handling + for event in P: + if event.type == pygame.MOUSEBUTTONUP: + x, y = pygame.mouse.get_pos() + action = PF.get_action(GameBoard, x, y) + + if( + type(action) == Action + and len(take_action) == 0 + ): + # only allow healing by itself (prevents things like ['move', (4, 1), 'heal']) + take_action.append(action) + + elif action == "reset move": take_action = [] + + elif type(action) is tuple: + idx = GameBoard.toIndex(action) + # action is a coordinate + if idx < (GameBoard.rows * GameBoard.columns) and idx > -1: + if Action.move not in take_action and len(take_action) == 0: + # make sure that the space is not an empty space or a space of the opposite team + # since cannot start a move from those invalid spaces + if ( + GameBoard.States[idx].person is not None + and GameBoard.States[idx].person.isZombie + == bool(player_role.value) + ): + take_action.append(Action.move) + else: + continue + + # don't allow duplicate cells + if action not in take_action: + take_action.append(action) + if event.type == pygame.QUIT: + running = False - # Computer turn - else: + # Display the current action + PF.screen.blit( + font.render("Your move is currently:", True, PF.WHITE), + (800, 400), + ) + PF.screen.blit(font.render(f"{take_action}", True, PF.WHITE), (800, 450)) + + # Action handling + if len(take_action) > 2: + directionToMove = PF.direction(take_action[1], take_action[2]) + print("Implementing", take_action[0], "to", directionToMove) + result = GameBoard.actionToFunction[take_action[0]](take_action[1], directionToMove) + print(f"did it succeed? {result[0]}") + if result[0] is not False: + playerMoved = True + take_action = [] + + if playerMoved: + # Intermission + PF.run(GameBoard) + pygame.display.update() + time.sleep(0.1) + print("Enemy turn") + + # Computer turn playerMoved = False take_action = [] - # Make a list of all possible actions that the computer can take - possible_actions = [ - ACTION_SPACE[i] - for i in range(6) - if (i != 4 and player_role == "Government") - or (i != 5 and player_role == "Zombie") - ] + if player_role == Role.government: + possible_actions = [Action.move, Action.bite] + computer_role = Role.zombie + else: + possible_actions = [Action.move, Action.heal, Action.kill] + computer_role = Role.government + possible_move_coords = [] + #Cycles through actions while len(possible_move_coords) == 0 and len(possible_actions) != 0: - action = possible_actions.pop(rd.randint(0, len(possible_actions) - 1)) - possible_move_coords = GameBoard.get_possible_moves( - action, "Government" if player_role == "Zombie" else "Zombie" - ) + possible_direction = [member for name, member in Direction.__members__.items()] + action = rd.choice(possible_actions) + #cycles through directions + while len(possible_move_coords) == 0 and len(possible_direction) != 0: + direction = rd.choice(possible_direction) + possible_direction.remove(direction) + possible_move_coords = GameBoard.get_possible_moves( + action, direction, computer_role + ) + possible_actions.remove(action) + print("possible actions is", possible_actions) # no valid moves, player wins - if len(possible_actions) == 0 and len(possible_move_coords) == 0: + if ( + len(possible_actions) == 0 + and len(possible_direction) == 0 + and len(possible_move_coords) == 0 + ): PF.display_win_screen() running = False continue @@ -115,14 +143,15 @@ move_coord = rd.choice(possible_move_coords) # Implement the selected action - GameBoard.actionToFunction[action](move_coord) + print("action chosen is", action) + print("move start coord is", move_coord) + + GameBoard.actionToFunction[action](move_coord, direction) - # update the board's states - GameBoard.update() + print("stopping") # Update the display pygame.display.update() - pygame.time.wait(75) else: if epochs_ran % 100 == 0: @@ -145,10 +174,10 @@ if biggest is None: biggest = exp i = x - elif biggest < exp and player_role == "Government": + elif biggest < exp and player_role == Role.government: biggest = exp i = x - elif biggest > exp and player_role != "Government": + elif biggest > exp and player_role != Role.government: biggest = exp i = x state = GameBoard.QTable[i] @@ -156,10 +185,10 @@ j = 0 ind = 0 for v in state: - if v > b and player_role == "Government": + if v > b and player_role == Role.government: b = v ind = j - elif v < b and player_role != "Government": + elif v < b and player_role != Role.government: b = v ind = j j += 1 @@ -180,7 +209,7 @@ take_action = [] print("Enemy turn") ta = "" - if player_role == "Government": + if player_role == Role.government: r = rd.randint(0, 5) while r == 4: r = rd.randint(0, 5) @@ -188,7 +217,7 @@ else: r = rd.randint(0, 4) ta = ACTION_SPACE[r] - poss = GameBoard.get_possible_moves(ta, "Zombie") + poss = GameBoard.get_possible_moves(ta, Role.zombie) if len(poss) > 0: r = rd.randint(0, len(poss) - 1) diff --git a/SGAI_MK3/State.py b/State.py similarity index 60% rename from SGAI_MK3/State.py rename to State.py index 48a8cf9..eb98789 100644 --- a/SGAI_MK3/State.py +++ b/State.py @@ -1,24 +1,18 @@ -from typing import Tuple from Person import Person -import math - class State: - def __init__(self, p: Person, i) -> None: + def __init__(self, p: Person, i, safeSpace = False) -> None: self.person = p self.location = i + self.safeSpace = safeSpace pass - def distance(self, GameBoard, other_location: int): - first_coord = GameBoard.toCoord(self.location) - second_coord = GameBoard.toCoord(other_location) - a = second_coord[0] - first_coord[0] - b = second_coord[1] - first_coord[1] - a = a * a - b = b * a - return math.pow(int(a + b), 0.5) + def distance(self, other_id): # gets the distance between two states + first_coord = self.toCoord(self.location) + second_coord = self.toCoord(other_id) + return (float)((second_coord[1] - first_coord[1])**2 + (second_coord[0] - first_coord[0])**2)**0.5 - def nearest_zombie(self, GameBoard): + def nearest_zombie(self, GameBoard): #pretty self explanatory smallest_dist = 100 for state in GameBoard.States: if state.person != None: @@ -28,7 +22,7 @@ def nearest_zombie(self, GameBoard): smallest_dist = d return smallest_dist - def evaluate(self, action: str, GameBoard): + def evaluate(self, action: str, GameBoard): # decides on the reward for a specific action based on what the board is like (for q learning) reward = 0 reward += self.nearest_zombie(GameBoard) - 3 if action == "heal": @@ -42,18 +36,19 @@ def evaluate(self, action: str, GameBoard): reward = reward + int(5 * (2 + chance)) return reward - def adjacent(self, GameBoard): + def adjacent(self, GameBoard): # returns the four adjacent boxes that are in bounds newCoord = GameBoard.toCoord(self.location) - moves = [ + print(newCoord) + moves = [ #puts all four adjacent locations into moves (newCoord[0], newCoord[1] - 1), (newCoord[0], newCoord[1] + 1), (newCoord[0] - 1, newCoord[1]), (newCoord[0] + 1, newCoord[1]), ] - remove = [] + remove = [] #creates the ones to remove for i in range(4): move = moves[i] - if ( + if ( #removes all illigal options move[0] < 0 or move[0] > GameBoard.columns or move[1] < 0 @@ -65,23 +60,15 @@ def adjacent(self, GameBoard): moves.pop(r) return moves - def clone(self): + def clone(self): #clones the state (for the purpose of moving people and zombies) if self.person is None: return State(self.person, self.location) return State(self.person.clone(), self.location) - def __eq__(self, __o: object) -> bool: + def __eq__(self, __o: object) -> bool: # compares if two states are the same, not just the same person but also the same location if type(__o) == State: return self.person == __o.person and self.location == __o.location return False - def __ne__(self, __o: object) -> bool: + def __ne__(self, __o: object) -> bool: # same as over but not equals return not self == __o - - def update(self): - """ - If this has a person, update the person within. - """ - if self.person is None: - return - self.person.update() diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..aad47b0 --- /dev/null +++ b/constants.py @@ -0,0 +1,41 @@ +import enum +import pygame +import os +pygame.mixer.init() + + +ROWS = 6 +COLUMNS = 6 +BORDER = 150 # Number of pixels to offset grid to the top-left side +CELL_DIMENSIONS = (100, 100) # Number of pixels (x,y) for each cell +SELF_PLAY = True # whether or not a human will be playing +KILL_SOUND = pygame.mixer.Sound(os.path.join('Assets', 'Kill sound.mp3')) + +class Action(enum.Enum): + move = 1 + bite = 2 + heal = 3 + kill = 4 + +class Direction(enum.Enum): + self = 0 + up = 1 + down = 2 + left = 3 + right = 4 + +class Role(enum.Enum): + government = 0 + zombie = 1 + +class Result(enum.Enum): + invalid = 0 + success = 1 + failure = 2 + +reverse_dir = { + Direction.up: Direction.down, + Direction.down: Direction.up, + Direction.left: Direction.right, + Direction.right: Direction.left +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..cdc5f8f --- /dev/null +++ b/main.py @@ -0,0 +1,266 @@ +import pygame +from Board import Board +import PygameFunctions as PF +import random as rd +from constants import * +import time + +# Player role variables +player_role = Role.government # Valid options are Role.government and Role.zombie +roleToRoleNum = {Role.government: 1, Role.zombie: -1} + +# Create the game board +GameBoard = Board((ROWS, COLUMNS), BORDER, CELL_DIMENSIONS, player_role) +GameBoard.populate() + +# Self play variables +alpha = 0.1 +gamma = 0.6 +epsilon = 0.1 +epochs = 1000 +epochs_ran = 0 +Original_Board = GameBoard.clone(GameBoard.States, GameBoard.player_role) + + +# Initialize variables +running = True +take_action = [] +playerMoved = False +font = pygame.font.SysFont("Comic Sans", 20) +start = False + +#Initial player score +player_score = 0 + + +while running: + #displays the main menu until user hits start or quit + if start == False: + try: + start = PF.disp_title_screen() + #Throws exception when user quits program using in-game button + except pygame.error: + print("Game closed by user") + break + elif start == True: + P = PF.run(GameBoard) + if SELF_PLAY: + if( + not GameBoard.containsPerson(bool(player_role.value)) + or GameBoard.outrage >= 100 + ): + running = PF.display_lose_screen() + for state in GameBoard.States: + state.person = None + state.safeSpace = False + GameBoard.populate() + start = False + continue + # Event Handling + for event in P: + if event.type == pygame.MOUSEBUTTONUP: + x, y = pygame.mouse.get_pos() + action = PF.get_action(GameBoard, x, y) + + if(action == "Distrb Med"): + take_action.append("Distrb Med") + time.sleep(0.1) + GameBoard.med() + take_action = [] + + elif( + type(action) == Action + and len(take_action) == 0 + ): + # only allow healing by itself (prevents things like ['move', (4, 1), 'heal']) + take_action.append(action) + + elif action == "reset move": + take_action = [] + + elif type(action) is tuple: + idx = GameBoard.toIndex(action) + # action is a coordinate + if idx < (GameBoard.rows * GameBoard.columns) and idx > -1: + if Action.move not in take_action and len(take_action) == 0: + # make sure that the space is not an empty space or a space of the opposite team + # since cannot start a move from those invalid spaces + if ( + GameBoard.States[idx].person is not None + and GameBoard.States[idx].person.isZombie + == bool(player_role.value) + ): + take_action.append(Action.move) + else: + continue + + + take_action.append(action) + if event.type == pygame.QUIT: + running = False + + # Display the current action + PF.screen.blit( + font.render("Your move is currently:", True, PF.WHITE), + (800, 400), + ) + PF.screen.blit(font.render(f"{take_action}", True, PF.WHITE), (800, 450)) + + + + # Action handling + if len(take_action) > 2: + directionToMove = PF.direction(take_action[1], take_action[2]) + print("Implementing", take_action[0], "to", directionToMove) + result = GameBoard.actionToFunction[take_action[0]](take_action[1], directionToMove) + print(f"did it succeed? {result}") + + if result == Result.success: + player_score += PF.get_reward(take_action[0]) + #if it succeeds, the player gets a reward corresponding to their action + + if result != Result.invalid: + playerMoved = True + take_action = [] + #Display the player's current score + PF.screen.blit(font.render("Score: " + str(player_score), True, PF.WHITE),(900,500)) + + if playerMoved: + # Intermission + PF.run(GameBoard) + pygame.display.update() + time.sleep(0.1) + print("Enemy turn") + + # Computer turn + playerMoved = False + take_action = [] + + if player_role == Role.government: + possible_actions = [Action.move, Action.bite] + computer_role = Role.zombie + else: + possible_actions = [Action.move, Action.heal, Action.kill] + computer_role = Role.government + + possible_move_coords = [] + #Cycles through actions + while len(possible_move_coords) == 0 and len(possible_actions) != 0: + possible_direction = [member for name, member in Direction.__members__.items()] + action = rd.choice(possible_actions) + #cycles through directions + while len(possible_move_coords) == 0 and len(possible_direction) != 0: + direction = rd.choice(possible_direction) + possible_direction.remove(direction) + possible_move_coords = GameBoard.get_possible_moves( + action, direction, computer_role + ) + possible_actions.remove(action) + print("possible actions is", possible_actions) + + # no valid moves, player wins + #Displays two buttons and allows the player to play again on a new randomized map + if ( + len(possible_actions) == 0 + and len(possible_direction) == 0 + and len(possible_move_coords) == 0 + ): + running = PF.display_win_screen() + for state in GameBoard.States: + state.person = None + state.safeSpace = False + GameBoard.populate() + start = False + continue + + # Select the destination coordinates + move_coord = rd.choice(possible_move_coords) + + # Implement the selected action + print("action chosen is", action) + print("move start coord is", move_coord) + print(GameBoard.actionToFunction[action](move_coord, direction)) + print("stopping") + + # Update the display + pygame.display.update() + pygame.time.wait(75) + + else: + if epochs_ran % 100 == 0: + print("Board Reset!") + GameBoard = Original_Board # reset environment + for event in P: + i = 0 + r = rd.uniform(0.0, 1.0) + st = rd.randint(0, len(GameBoard.States) - 1) + state = GameBoard.QTable[st] + + if r < gamma: + while GameBoard.States[st].person is None: + st = rd.randint(0, len(GameBoard.States) - 1) + else: + biggest = None + for x in range(len(GameBoard.States)): + arr = GameBoard.QTable[x] + exp = sum(arr) / len(arr) + if biggest is None: + biggest = exp + i = x + elif biggest < exp and player_role == Role.government: + biggest = exp + i = x + elif biggest > exp and player_role != Role.government: + biggest = exp + i = x + state = GameBoard.QTable[i] + b = 0 + j = 0 + ind = 0 + for v in state: + if v > b and player_role == Role.government: + b = v + ind = j + elif v < b and player_role != Role.government: + b = v + ind = j + j += 1 + action_to_take = ACTION_SPACE[ind] + old_qval = b + old_state = i + + # Update + # Q(S, A) = Q(S, A) + alpha[R + gamma * max_a Q(S', A) - Q(S, A)] + reward = GameBoard.act(old_state, action_to_take) + ns = reward[1] + NewStateAct = GameBoard.QGreedyat(ns) + NS = GameBoard.QTable[ns][NewStateAct[0]] + # GameBoard.QTable[i] = GameBoard.QTable[i] + alpha * (reward[0] + gamma * NS) - GameBoard.QTable[i] + if GameBoard.num_zombies() == 0: + print("winCase") + + take_action = [] + print("Enemy turn") + ta = "" + if player_role == Role.government: + r = rd.randint(0, 5) + while r == 4: + + r = rd.randint(0, 5) + while r == 4: + r = rd.randint(0, 5) + ta = ACTION_SPACE[r] + else: + r = rd.randint(0, 4) + ta = ACTION_SPACE[r] + poss = GameBoard.get_possible_moves(ta, Role.zombie) + + if len(poss) > 0: + r = rd.randint(0, len(poss) - 1) + a = poss[r] + GameBoard.actionToFunction[ta](a) + if GameBoard.num_zombies() == GameBoard.population: + print("loseCase") + if event.type == pygame.QUIT: + running = False +pygame.display.quit()