diff --git a/__pycache__/ai.cpython-312.pyc b/__pycache__/ai.cpython-312.pyc index a24255d..3166d7b 100644 Binary files a/__pycache__/ai.cpython-312.pyc and b/__pycache__/ai.cpython-312.pyc differ diff --git a/__pycache__/cell.cpython-312.pyc b/__pycache__/cell.cpython-312.pyc index c29f788..4fc7bc7 100644 Binary files a/__pycache__/cell.cpython-312.pyc and b/__pycache__/cell.cpython-312.pyc differ diff --git a/__pycache__/cell_terrain.cpython-312.pyc b/__pycache__/cell_terrain.cpython-312.pyc index aec276e..128f610 100644 Binary files a/__pycache__/cell_terrain.cpython-312.pyc and b/__pycache__/cell_terrain.cpython-312.pyc differ diff --git a/__pycache__/faction.cpython-312.pyc b/__pycache__/faction.cpython-312.pyc index 9ff820c..0f6e0da 100644 Binary files a/__pycache__/faction.cpython-312.pyc and b/__pycache__/faction.cpython-312.pyc differ diff --git a/__pycache__/params.cpython-312.pyc b/__pycache__/params.cpython-312.pyc index fd51a0b..d195f36 100644 Binary files a/__pycache__/params.cpython-312.pyc and b/__pycache__/params.cpython-312.pyc differ diff --git a/__pycache__/unit.cpython-312.pyc b/__pycache__/unit.cpython-312.pyc index d0cbe04..3f81f24 100644 Binary files a/__pycache__/unit.cpython-312.pyc and b/__pycache__/unit.cpython-312.pyc differ diff --git a/ai.py b/ai.py index ac00d85..3893d8b 100644 --- a/ai.py +++ b/ai.py @@ -95,3 +95,184 @@ class AI: # return all the command objects. return cmds + + + # Needs Seperate AI's for each faction + + # AI for Zombies + def run_ai_zombies(self, faction_id, factions, cities, units, gmap): + cmds = [] + + # Generate Zombies (Typically Done Only At Start) + # Verify there are remaining zombie cities + if (len(cities['Zombies']) != 0): + cmd = BuildUnitCommand(faction_id, + cities['Zombies'][0].ID, + "Z") + cmds.append(cmd) + + # Random moves (zombies) + my_units = units[faction_id] + for u in my_units: + rand_dir = random.choice(list(vec2.MOVES.keys())) + cmd = MoveUnitCommand(faction_id, u.ID, rand_dir) + cmds.append(cmd) + + return cmds + + # AI for Survivors + def run_ai_survivors(self, faction_id, factions, cities, units, gmap): + cmds = [] + + # Attempts to create new survivors via reproduction + my_cities = cities[faction_id] + city_indexes = list(range(len(my_cities))) + random.shuffle(city_indexes) + for ci in city_indexes: + cmd = BuildUnitCommand(faction_id, + my_cities[ci].ID, + "S") + cmds.append(cmd) + + # Rare case + # If cured city is lost, and there are adequate survivors to make a new cured city + # one survivor city at random can produce a cured unit + if (len(cities['Cured']) == 0) and (len(units[faction_id]) >= 50): + my_cities = cities[faction_id] + city_indexes = list(range(len(my_cities))) + random.shuffle(city_indexes) + cmd = BuildUnitCommand("Cured", + my_cities[city_indexes].ID, + "C") + cmds.append(cmd) + + # Survivors will also attempt to defend cured cities + totalCities = cities[faction_id] + cities['Cured'] + + # Loop through all of the survivors + for u in units[faction_id]: + # First checks to see if the survivor was given another tasking previously (i.e defend x city) + # If so, verify city had not fallen and move towards it + # If within 3 tiles of city, pick another target + if u.currentTarget in totalCities: + + # 10% chance of random movement + if random.random() < 0.1: + rand_dir = random.choice(list(vec2.MOVES.keys())) + cmd = MoveUnitCommand(faction_id, u.ID, rand_dir) + cmds.append(cmd) + continue + + # Target is a city, so move towards it + target = u.currentTarget + + #Given current position as (x,y) and target as (x,y), calculate next move to get closer to city + vectorDirection = (target.pos.x - u.pos.x, target.pos.y - u.pos.y) + + # X is larger, move that way first + if abs(vectorDirection[0]) > abs(vectorDirection[1]): + if vectorDirection[0] > 0: + cmd = MoveUnitCommand(faction_id, u.ID, 'E') + else: + cmd = MoveUnitCommand(faction_id, u.ID, 'W') + else: + if vectorDirection[1] > 0: + cmd = MoveUnitCommand(faction_id, u.ID, 'S') + else: + cmd = MoveUnitCommand(faction_id, u.ID, 'N') + + cmds.append(cmd) + + # Too close to city, so pick another target + if abs(u.pos.distance_man(target.pos)) <= 4: + if len(totalCities) != 0: + u.currentTarget = random.choice(totalCities) + # 2 Actions to prevent clumping + cmds.append(MoveUnitCommand(faction_id, u.ID, random.choice(list(vec2.MOVES.keys())))) + cmds.append(MoveUnitCommand(faction_id, u.ID, random.choice(list(vec2.MOVES.keys())))) + else: u.currentTarget = None + + else: + # City has fallen, so clear target + # Establish new target (if available) and move randomly + if len(totalCities) != 0: u.currentTarget = random.choice(totalCities) + else: u.cuurrentTarget = None + rand_dir = random.choice(list(vec2.MOVES.keys())) + cmd = MoveUnitCommand(faction_id, u.ID, rand_dir) + cmds.append(cmd) + + return cmds + + # AI for Cured + def run_ai_cured(self, faction_id, factions, cities, units, gmap): + cmds = [] + + # Attempts to create new cured + my_cities = cities[faction_id] + city_indexes = list(range(len(my_cities))) + random.shuffle(city_indexes) + for ci in city_indexes: + cmd = BuildUnitCommand(faction_id, + my_cities[ci].ID, + "C") + cmds.append(cmd) + + allCities = cities[faction_id] + cities['Zombies'] + cities['Survivors'] + + # Loop through all of the cured + for u in units[faction_id]: + # Once there are cured in the world, they will attempt to "infect" any cities on the map, one at a time + # Check if we are currently targeting a city + target = u.currentTarget + + if target in allCities: + + # 10% chance of random movement + if random.random() < 0.1: + rand_dir = random.choice(list(vec2.MOVES.keys())) + cmd = MoveUnitCommand(faction_id, u.ID, rand_dir) + cmds.append(cmd) + continue + + #Given current position as (x,y) and target as (x,y), calculate next move to get closer to city + vectorDirection = (target.pos.x - u.pos.x, target.pos.y - u.pos.y) + + # X is larger, move that way first + if abs(vectorDirection[0]) > abs(vectorDirection[1]): + if vectorDirection[0] > 0: + cmd = MoveUnitCommand(faction_id, u.ID, 'E') + else: + cmd = MoveUnitCommand(faction_id, u.ID, 'W') + else: + if vectorDirection[1] > 0: + cmd = MoveUnitCommand(faction_id, u.ID, 'S') + else: + cmd = MoveUnitCommand(faction_id, u.ID, 'N') + + cmds.append(cmd) + + if (target not in allCities) or (abs(u.pos.distance_man(target.pos)) <= 4): + # Pick first city that is not already Cured and target it + if len(allCities) != 0: + for city in allCities: + if city.faction_id != "Cured": + u.currentTarget = city + break + else: u.currentTarget = None + + cmds.append(MoveUnitCommand(faction_id, u.ID, random.choice(list(vec2.MOVES.keys())))) + cmds.append(MoveUnitCommand(faction_id, u.ID, random.choice(list(vec2.MOVES.keys())))) + + # No cities left to infect, so target remaining uncured units + if u.currentTarget == None or u.currentTarget.faction_id == "Cured": + for faction in factions: + if faction != faction_id: + for unit in units[faction]: + u.currentTarget = unit + break + else: u.currentTarget = None + rand_dir = random.choice(list(vec2.MOVES.keys())) + cmd = MoveUnitCommand(faction_id, u.ID, rand_dir) + cmds.append(cmd) + + return cmds \ No newline at end of file diff --git a/cell.py b/cell.py index 23d1491..6e595a6 100644 --- a/cell.py +++ b/cell.py @@ -12,7 +12,9 @@ import cell_terrain - +# Modified +# Open: -2 for Zombies +# Shadows: +4 for Zombies class Cell: def __init__(self, terrain): self.terrain = terrain @@ -23,19 +25,19 @@ class Cell: match self.terrain: case cell_terrain.Terrain.Open: return "cornsilk2" - case cell_terrain.Terrain.Forest: + case cell_terrain.Terrain.Shadows: return "cornsilk3" - def get_attack_mod(self): + def get_attack_mod(self, unitType): match self.terrain: case cell_terrain.Terrain.Open: - return 2 - case cell_terrain.Terrain.Forest: - return 0 + return -2 if unitType == "Z" else 0 + case cell_terrain.Terrain.Shadows: + return 4 if unitType == "Z" else 0 - def get_defense_mod(self): + def get_defense_mod(self, unitType): match self.terrain: case cell_terrain.Terrain.Open: - return 0 - case cell_terrain.Terrain.Forest: - return 2 + return -2 if unitType == "Z" else 0 + case cell_terrain.Terrain.Shadows: + return +4 if unitType == "Z" else 0 diff --git a/cell_terrain.py b/cell_terrain.py index 006a525..4f8c39d 100644 --- a/cell_terrain.py +++ b/cell_terrain.py @@ -8,4 +8,4 @@ import enum class Terrain(enum.Enum): Open = 0 - Forest = 1 + Shadows = 1 diff --git a/faction.py b/faction.py index fea3eb4..0c3b05c 100644 --- a/faction.py +++ b/faction.py @@ -25,4 +25,10 @@ class Faction: # ################################################################ def run_ai(self, factions, cities, units, gmap): - return self.ai.run_ai(self.ID, factions, cities, units, gmap) + match self.ID: + case "Zombies": + return self.ai.run_ai_zombies(self.ID, factions, cities, units, gmap) + case "Survivors": + return self.ai.run_ai_survivors(self.ID, factions, cities, units, gmap) + case "Cured": + return self.ai.run_ai_cured(self.ID, factions, cities, units, gmap) diff --git a/main.py b/main.py index 7bb52fe..984d4c9 100644 --- a/main.py +++ b/main.py @@ -99,6 +99,7 @@ class Display: def init_display(sw, sh): pygame.init() + pygame.display.set_caption('Infection') screen = pygame.display.set_mode((sw, sh)) clock = pygame.time.Clock() display = Display(screen, clock) @@ -124,7 +125,7 @@ def gen_factions(gmap): # Allows one time generation of X zombies # x = random.randint(4, 8) * 100 factions['Zombies'] = faction.Faction( - 'Zombies', random.randint(8, 16) * 100, + 'Zombies', random.randint(3, 16) * 100, ai.AI(), 'darkgreen') # Starts with normal amount of money @@ -380,8 +381,8 @@ def RunCombat(attacker, defender, cmd, factions, unit_dict, cities, gmap): def_roll = defender.roll(attacker.utype) # Add terrain modifiers. - att_roll += att_cell.get_attack_mod() - def_roll += def_cell.get_defense_mod() + att_roll += att_cell.get_attack_mod(attacker.faction_id) + def_roll += def_cell.get_defense_mod(defender.faction_id) # Damage health. defender.health -= att_roll @@ -491,11 +492,20 @@ class UnitDict: -def CheckForGameOver(cities): +def CheckForGameOver(cities, unit_dict): faction_ids_with_cities = [] for c in cities: if c.faction_id not in faction_ids_with_cities: faction_ids_with_cities.append(c.faction_id) + + # Verify all units are of same type + for unit in unit_dict.allUnits: + if unit.faction_id not in faction_ids_with_cities: + faction_ids_with_cities.append(unit.faction_id) + + if len(faction_ids_with_cities) == 2 and "Survivors" in faction_ids_with_cities and "Cured" in faction_ids_with_cities: + return True, "Cured" + return len(faction_ids_with_cities) == 1, faction_ids_with_cities[0] @@ -517,6 +527,8 @@ def GameLoop(display): # - The map_cell_size given in the Display class above. gmap = gen_game_map(40, 30) + maxY = None + factions = gen_factions(gmap) # City gen is basically locked cities = gen_cities(gmap) @@ -547,8 +559,10 @@ def GameLoop(display): speed = speed * 2 - display.screen.fill("white") + display.screen.fill("cornsilk3") + game_over = [False, None] + if ticks >= speed: ticks = 0 cities_by_faction = {} @@ -562,11 +576,7 @@ def GameLoop(display): RunAllCommands(commands, factions, unit_dict, cities, gmap) turn += 1 - game_over = CheckForGameOver(cities) - if game_over[0]: - print(f"Winning faction: {game_over[1]}") - display.run = False - + game_over = CheckForGameOver(cities, unit_dict) display.draw_map(gmap) display.draw_cities(cities, factions) @@ -575,26 +585,51 @@ def GameLoop(display): # ###########################################3 # RIGHT_SIDE UI display.draw_text(f"TURN {turn}", 805, 5, "black") - display.draw_text(f"{'Fctn':<5} {'C':>2} {'U':>3} {'M':>4}", - 805, 25, "black") - y = 45 - for fid, f in factions.items(): - num_cities = 0 - for c in cities: - if c.faction_id == fid: - num_cities += 1 - - display.draw_text(f"{fid:<5} {num_cities:>2} {len(unit_dict.unitsByFaction()[fid]):>3} {f.money:>4}", - 805, y, "black") - y += 20 + + numZombies = len(unit_dict.unitsByFaction()["Zombies"]) + numSurvivors = len(unit_dict.unitsByFaction()["Survivors"]) + numCured = len(unit_dict.unitsByFaction()["Cured"]) + totalUnits = len(unit_dict.allUnits) + + infectionRate = numZombies / totalUnits * 100 if totalUnits > 0 else 0 + cureRate = numCured / totalUnits * 100 if totalUnits > 0 else 0 + + display.draw_text("Infection Rate", 805, 45, "black") + display.draw_text(f"{infectionRate:.2f}%", 805, 65, "black") + + display.draw_text("Cure Progress", 805, 85, "black") + display.draw_text(f"{cureRate:.2f}%", 805, 105, "black") + + y = 145 + display.draw_text(f"Cities", 805, y, "black") + y += 20 + for city in cities: + display.draw_text(f"{city.ID}", 805, y, factions[city.faction_id].color) + y += 20 + + maxY = y + + if game_over[0]: + display.draw_text(f"{game_over[1]} won", 805, maxY + 20, "black") + display.draw_text("Press any key to quit", 805, maxY + 40, "black") + pygame.display.flip() + key = None + while key == None: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + display.run = False + elif event.type == pygame.KEYDOWN: + key = event.key + print(f"Winning faction: {game_over[1]}") + display.run = False pygame.display.flip() def main(): random.seed(None) - display = init_display(1000, 600) + display = init_display(1100, 600) GameLoop(display) diff --git a/params.py b/params.py index 561be31..a507f63 100644 --- a/params.py +++ b/params.py @@ -14,7 +14,7 @@ import random # algorithms here. If you want something fancier, you'd need # to add them below and call them in game_map.py. CELL_TERRAIN_PROBABILITY = { - cell_terrain.Terrain.Forest: 1, + cell_terrain.Terrain.Shadows: 1, cell_terrain.Terrain.Open: 4 } diff --git a/unit.py b/unit.py index 1aa43dd..a646065 100644 --- a/unit.py +++ b/unit.py @@ -15,8 +15,8 @@ import math # C = CURED (EXTREMELY SLOW) UNIT_COSTS = { "Z": 100, - "S": 500, - "C": 2500 + "S": 250, + "C": 2000 } # UNIT_HEALATH is a constant dictionary that holds the starting health @@ -28,7 +28,7 @@ UNIT_COSTS = { # C = CURED (HIGH HEALTH) UNIT_HEALTH = { - "Z": 5, + "Z": 6, "S": 7, "C": 7 } @@ -75,6 +75,9 @@ class Unit: # sight_radius: int - how far it sees # NOT USED. self.sight_radius = sight_radius + + # Allows for persistent target (if applicable) + self.currentTarget = None def __eq__(self, o): return self.ID == o.ID and self.faction_id == o.faction_id @@ -91,11 +94,4 @@ class Unit: # rock-paper-scissors have max damage of 20. All other # combinations are 10. Feel free to modify if you want. def roll(self, op_utype): - if op_utype == 'R' and self.utype == 'P': - return random.randint(0, 20) - elif op_utype == 'P' and self.utype == 'S': - return random.randint(0, 20) - elif op_utype == 'S' and self.utype == 'R': - return random.randint(0, 20) - else: return random.randint(0, 10)