638 lines
20 KiB
Python
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()
|