Files
2025-04-18 23:04:16 +00:00

638 lines
20 KiB
Python

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()