import random import copy import pygame import pygame.freetype import game_map import params import faction import ai import city import vec2 import unit import command # ################################################################### # DISPLAY # The display part of the engine. Unless you want to mess with # the look and feel (add sprites or something) you probably don't need # to mess with anything in this section # #################################################################### class Display: def __init__(self, screen, clock): self.screen = screen self.clock = clock self.run = True self.delta = 0 self.font = None self.map_cell_size = 20 # fmt: off def draw_gobj(self, gobj): pygame.draw.circle( self.screen, gobj.color, gobj.pos(), gobj.radius) def draw_text(self, msg, x, y, color): surface, rect = self.font.render(msg, color) self.screen.blit(surface, (x, y)) def draw_line(self, p1, p2, color, width=1): pygame.draw.line( self.screen, color, p1, p2, width) def draw_map(self, gmap): for v, c in gmap.cells.items(): pygame.draw.rect( self.screen, c.get_color(), pygame.rect.Rect( v.x*self.map_cell_size, v.y*self.map_cell_size, self.map_cell_size, self.map_cell_size), width=0) def draw_cities(self, cities, factions): for c in cities: f = factions[c.faction_id] pygame.draw.rect( self.screen, f.color, pygame.rect.Rect( c.pos.x*self.map_cell_size, c.pos.y*self.map_cell_size, self.map_cell_size, self.map_cell_size ), width=3 ) def draw_units(self, unit_dict, factions): for fid in unit_dict.unitsByFaction(): fcolor = factions[fid].color for u in unit_dict.unitsByFaction()[fid]: pygame.draw.rect( self.screen, fcolor, pygame.rect.Rect( u.pos.x*self.map_cell_size, u.pos.y*self.map_cell_size, self.map_cell_size, self.map_cell_size ), width=0 ) self.draw_text( u.utype, u.pos.x*self.map_cell_size+4, u.pos.y*self.map_cell_size+4, "white") 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) display.font = pygame.freetype.Font('JuliaMono-Bold.ttf', 18) pygame.key.set_repeat(200, 100) return display # ############################################################### # GAME GENERATION FUCNTIONS # This section generates the map, factions and cities. # If you add things to the game (additional terrain, factions, # city types, etc), you'll need to edit these functions to have # them placed on the map # ############################################################### def gen_game_map(width, height): return game_map.GameMap(width, height) # Modified to have proper starting money def gen_factions(gmap): factions = {} # Allows one time generation of X zombies # x = random.randint(4, 8) * 100 factions['Zombies'] = faction.Faction( 'Zombies', random.randint(3, 16) * 100, ai.AI(), 'darkgreen') # Starts with normal amount of money factions['Survivors'] = faction.Faction( 'Survivors', params.STARTING_FACTION_MONEY, ai.AI(), 'orange') # Starts at nothing factions['Cured'] = faction.Faction( 'Cured', 0, ai.AI(), 'blue') return factions # Modified def gen_cities(gmap): city_positions = [] cities = [] faction_id_index = 0 # As each faction requires special rules, this is a switch case # Survivor City Generation # Random between 3 and 8 cities for i in range(random.randint(3, 5)): new_city_pos = None while True: new_city_pos = vec2.Vec2( random.randrange(gmap.width), random.randrange(gmap.height)) if new_city_pos not in city_positions: city_positions.append(new_city_pos) break c = city.City( params.get_random_city_ID(), new_city_pos, 'Survivors', params.CITY_INCOME) cities.append(c) # Cured City Generation # Only 1 City new_city_pos = None while True: new_city_pos = vec2.Vec2( random.randrange(gmap.width), random.randrange(gmap.height)) if new_city_pos not in city_positions: city_positions.append(new_city_pos) break c = city.City( params.get_random_city_ID(), new_city_pos, 'Cured', params.CITY_INCOME) cities.append(c) # Zombies City Generation # Only 1 City new_city_pos = None while True: new_city_pos = vec2.Vec2( random.randrange(gmap.width), random.randrange(gmap.height)) if new_city_pos not in city_positions: city_positions.append(new_city_pos) break c = city.City( params.get_random_city_ID(), new_city_pos, 'Zombies', params.CITY_INCOME) cities.append(c) return cities # ########################################################### # GAME ENGINE CODE # See specific function comments below # ########################################################## # FactionPreTurn: # You don't need to edit this unless you make city resources # more complex. # - awards each faction its income from the cities # - stores cities in the city dictionary passed onto the AI. def FactionPreTurn(cities, faction): faction_cities = [] # ##################################################### # FACTION DATA # Award income for c in cities: if c.faction_id == faction.ID: income = c.generate_income() faction.money += income # ##################################################### # CITY DATA for c in cities: if c.faction_id == faction.ID: faction_cities.append(c) return faction_cities # Turn: # The actual turn taking function. Calls each faction's ai # Gathers all the commands in a giant list and returns it. def Turn(factions, gmap, cities_by_faction, units_by_faction): commands = [] for fid, f in factions.items(): cmds = f.run_ai(factions, cities_by_faction, units_by_faction, gmap) commands += cmds return commands # RunAllCommands: # Executes all commands from the current turn. # Shuffles the commands to reduce P1 bias (maybe). # Basically this is just a dispatch function. def RunAllCommands(commands, factions, unit_dict, cities, gmap): random.shuffle(commands) move_list = [] for cmd in commands: if isinstance(cmd, command.MoveUnitCommand): RunMoveCommand(cmd, factions, unit_dict, cities, gmap, move_list) elif isinstance(cmd, command.BuildUnitCommand): RunBuildCommand(cmd, factions, unit_dict, cities, gmap) else: print(f"Bad command type: {type(cmd)}") # RunMoveCommand: # The function that handles MoveUnitCommands. def RunMoveCommand(cmd, factions, unit_dict, cities, gmap, move_list): if (cmd.unit_id,cmd.faction_id) in move_list: return else: move_list.append((cmd.unit_id, cmd.faction_id)) # Find the unit ulist = unit_dict.unitsByFaction()[cmd.faction_id] theunit = None for u in ulist: if u.ID == cmd.unit_id: theunit = u break # Unit might have died before it's command could be run. if theunit is None: return # Get new position delta = vec2.Vec2(0, 0) try: delta = vec2.MOVES[cmd.direction] except KeyError: print(f"{cmd.direction} is not a valid direction") return new_pos = theunit.pos + delta # Modulo the new pos to the map size new_pos.mod(gmap.width, gmap.height) # Check if new_pos is free. move_successful = False if unit_dict.is_pos_free(new_pos): old_pos = theunit.pos theunit.pos = new_pos # unit_dict.move_unit(u, old_pos, new_pos) move_successful = True # Occupied by a unit else: other_unit = unit_dict.getUnitAtPos(new_pos) # Is the other unit an enemy? if other_unit.faction_id != theunit.faction_id: space_now_free = RunCombat(theunit, other_unit, cmd, factions, unit_dict, cities, gmap) # Perhaps combat freed the space. # if so, move. if space_now_free: old_pos = theunit.pos theunit.pos = new_pos # unit_dict.move_unit(u, old_pos, new_pos) move_successful = True # Check if the move conquerored a city if move_successful: for c in cities: if new_pos == c.pos: print(f"City {c.ID} conquered by {theunit.faction_id}") if c.faction_id == "Cured" and u.faction_id == "Survivors": break else: c.faction_id = u.faction_id # RunBuildCommand: # Executes the BuildUnitCommand. def RunBuildCommand(cmd, factions, unit_dict, cities, gmap): # How much does the unit cost? f = factions[cmd.faction_id] cost = unit.get_unit_cost(cmd.utype) # Does the faction have the available resources (money)? if f.can_build_unit(cost): # Look for the city for c in cities: if c.ID == cmd.city_id: # If there's no unit in the city, build. # Add to the unit dictionary and charge # the faction for its purchase. if unit_dict.is_pos_free(c.pos): uid = f.get_next_unit_id() new_unit = unit.Unit(uid, cmd.utype, f.ID, copy.copy(c.pos), unit.UNIT_HEALTH[cmd.utype], 0) unit_dict.add_unit(new_unit) f.money -= cost # RunCombat: # Called by the MoveUnitCommand if a unit tries to move into a cell # containing a unit of the opposing faction. # # Combat is mutually destructive in that both units damage each other. # and can both die. You are welcome to edit this if you want combat # to work differently. # # Returns whether the defender was destroyed (and the attacker not) # allowing the attacker to move into the cell. # Modified to "infect" units (if applicable) def RunCombat(attacker, defender, cmd, factions, unit_dict, cities, gmap): # Find the terrain each unit stands in. att_cell = gmap.get_cell(attacker.pos) def_cell = gmap.get_cell(defender.pos) # Make the combat rolls. att_roll = attacker.roll(defender.utype) def_roll = defender.roll(attacker.utype) # Add terrain modifiers. 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 attacker.health -= def_roll # Debug output # print(f"Combat - {attacker.faction_id}: {att_roll} v {defender.faction_id}: {def_roll}") # Did anyone die? If the defender died and the attacker # did not, return that the attacker is free to move into # the cell. # Case off the type of unit and what happens to it if it dies can_move = False # SECTION: ZOMBIE/SURVIVOR COMBAT if attacker.faction_id == "Zombies" and defender.faction_id == "Survivors" and defender.health <= 0: print("Zombie killed Survivor 1") defender.infect('Zombies') # Survivor Attacks Zombie # Survivor Dies -> Zombie if attacker.faction_id == "Survivors" and defender.faction_id == "Zombies" and attacker.health <= 0: print("Zombie killed Survivor 2") attacker.infect('Zombies') # Zombie Attacks Survivor # Zombie Dies -> Dead if attacker.faction_id == "Zombies" and defender.faction_id == "Survivors" and attacker.health <= 0: print("Survivor killed Zombie 1") unit_dict.remove_unit(attacker) # Survivor Attacks Zombie # Zombie Dies -> Dead if attacker.faction_id == "Survivors" and defender.faction_id == "Zombies" and defender.health <= 0: print("Survivor killed Zombie 2") unit_dict.remove_unit(defender) # SECTION: ZOMBIE/CURED COMBAT # Zombie Attacks Cured # Cured Dies -> Dead if attacker.faction_id == "Zombies" and defender.faction_id == "Cured" and defender.health <= 0: print("Zombie Killed Cured 1") unit_dict.remove_unit(defender) # Cured Attacks Zombie # Cured Dies -> Dead if attacker.faction_id == "Cured" and defender.faction_id == "Zombies" and attacker.health <= 0: print("Zombie Killed Cured 2") unit_dict.remove_unit(attacker) # Zombie Attacks Cured # Zombie Dies -> Cured if attacker.faction_id == "Zombies" and defender.faction_id == "Cured" and attacker.health <= 0: print("Cured Killed Zombie 1") attacker.infect('Cured') # Cured Attacks Zombie # Zombie Dies -> Cured if attacker.faction_id == "Cured" and defender.faction_id == "Zombies" and defender.health <= 0: print("Cured Killed Zombie 1") defender.infect('Cured') # No matter what, if Cured and Survivors "fight", they both become Cured if attacker.faction_id == "Cured" or defender.faction_id == "Cured": print("All Cured") attacker.infect('Cured') defender.infect('Cured') return False # ########################################################### # THE UNIT DICTIONARY # Modify at your own risk. Probably no need. # Rewritten to better handle infection of units (But slightly less efficient) # ########################################################### class UnitDict: def __init__(self, faction_ids): self.allUnits = [] self.faction_ids = faction_ids def add_unit(self, u): self.allUnits.append(u) def remove_unit(self, u): self.allUnits.remove(u) def getUnitAtPos(self, search_position): for u in self.allUnits: if u.pos == search_position: return u return None def unitsByFaction(self): if len(self.allUnits) == 0: return {faction_ids: [] for faction_ids in self.faction_ids} else: tempReturn = {faction_ids: [] for faction_ids in self.faction_ids} for u in self.allUnits: tempReturn[u.faction_id].append(u) return tempReturn def is_pos_free(self, pos): return True if self.getUnitAtPos(pos) is None else False 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] # ###########################################################3 # GAME LOOP # Where the magic happens. # I've marked below where you might want to edit things # for different reasons. # ########################################################### def GameLoop(display): winw, winh = pygame.display.get_window_size() # Width and Height (in cells) of the game map. If you want # a bigger/smaller map you will need to coordinate these values # with two other things. # - The window size below in main(). # - 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) unit_dict = UnitDict(list(factions.keys())) # Starting game speed (real time between turns) in milliseconds. speed = 1024 ticks = 0 turn = 1 while display.run: ticks += display.clock.tick(60) for event in pygame.event.get(): if event.type == pygame.QUIT: display.run = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_q: display.run = False elif event.key == pygame.K_LEFT: # Lower if you want a faster game speed. if speed > 128: speed = speed // 2 elif event.key == pygame.K_RIGHT: # Increase if you want a slower game speed. if speed < 4096: speed = speed * 2 display.screen.fill("cornsilk3") game_over = [False, None] if ticks >= speed: ticks = 0 cities_by_faction = {} for fid, f in factions.items(): faction_cities = FactionPreTurn(cities, f) cities_by_faction[fid] = faction_cities commands = Turn(factions, gmap, cities_by_faction, unit_dict.unitsByFaction()) RunAllCommands(commands, factions, unit_dict, cities, gmap) turn += 1 game_over = CheckForGameOver(cities, unit_dict) display.draw_map(gmap) display.draw_cities(cities, factions) display.draw_units(unit_dict, factions) # ###########################################3 # RIGHT_SIDE UI display.draw_text(f"TURN {turn}", 805, 5, "black") 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(1100, 600) GameLoop(display) if __name__ == "__main__": main()