This commit is contained in:
2025-04-18 13:20:21 +00:00
parent c789673469
commit 7a9acf9cb6
27 changed files with 1617 additions and 7 deletions
+22
View File
@@ -0,0 +1,22 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
Binary file not shown.
+7 -7
View File
@@ -1,7 +1,7 @@
## cos498-va-hw4 ##
[![](https://gitea-actions.nicholaspease.com/actions/umaine-npease/cos498-va-hw4/badge?label=build&style=flat&branch=main)](https://gitea-actions.nicholaspease.com/latest-log?branch=main)
[![](https://drone.nicholaspease.com/api/badges/umaine-npease/cos498-va-hw4/status.svg)](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw4)
[![](https://wakaapi.nicholaspease.com/api/badge/LAX18/interval:any/project:cos498-va-hw4)](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw4)
![](https://server1.nicholaspease.com/badges/cloc/npease/cos498-va-hw4.svg)
<hr>
## cos498-va-hw4 ##
[![](https://gitea-actions.nicholaspease.com/actions/umaine-npease/cos498-va-hw4/badge?label=build&style=flat&branch=main)](https://gitea-actions.nicholaspease.com/latest-log?branch=main)
[![](https://drone.nicholaspease.com/api/badges/umaine-npease/cos498-va-hw4/status.svg)](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw4)
[![](https://wakaapi.nicholaspease.com/api/badge/LAX18/interval:any/project:cos498-va-hw4)](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw4)
![](https://server1.nicholaspease.com/badges/cloc/npease/cos498-va-hw4.svg)
<hr>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+97
View File
@@ -0,0 +1,97 @@
# AI Class
# by zax
# This is the main file you will edit. The run_ai function's job
# is to issue two types of commands (see command.py):
# - BuildUnitCommand: asks the game engine to build a specific type
# of unit at a specific city if the faction has enough cash
# available.
# - MoveUnitCommand: asks the game engine to move a specific unit
# in a specific direction. The engine will only move a unit
# if the destination cell is free. If it is occupied by a friendly
# unit, nothing happens. If it is occupied by another faction,
# combat ensues.
from command import *
import random
import unit
class AI:
# init:
# Currently, the initializer adds nothing to the object.
# You are welcome to modify to have a place to keep
# information that persists across calls to run_ai().
#
# NOTE: AI objects are passed into the Faction initializer
# when a faction is created (see the gen_factions() function
# in the main.py file). If you'd like to subclass the AI class
# to differentiate between faction behaviors, you are welcome
# to do so.
def __init__(self):
pass
# run_ai
# Parameters:
# - faction_id: this is the faction_id of this AI object.
# Use it to access infomation in the other parameters.
# - factions: dictionary of factions by faction_id.
# - cities: dictionary of cities stored by faction_id.
# For example: cities[faction_id] would return all the
# the city objects owned by this faction.
# - units: dictionary of all units by faction_id.
# Similar to the cities dictionary, units[faction_id]
# would return all unit objects belonging to the faction.
# - gmap: the game map object. You can use this (if you wish)
# to get information about the map and terrain.
#
# Return:
# This function should return a list of commands to be processed
# by the game engine this turn.
#
# NOTE: You should replace the following code with your
# own. The code currently gives the factions totally random
# behavior. Totally random behavior, while interesting,
# is not an acceptable solution.
#
# NOTE 2: Every ai has access to ALL game objects. This means
# you can (and should) access the unit and city locations
# of the other faction. I STRONGLY advise against manually
# changing unit or city locations from the AI file. Doing so
# circumvents checks made by the game engine and is likely to
# have bad side effects. If you want something more actions
# than those provided by the two commands, I suggest taking
# the time to create additional Command subclasses and properly
# implement them in the engine (main.py).
def run_ai(self, faction_id, factions, cities, units, gmap):
# A list to hold our commands. This gets returned by
# the function.
cmds = []
# Overview: randomly select a city we own and randomly
# select a unit type (utype). Create a BuildUnitCommand
# This is done every turn knowing most will fail because
# the faction does not have enough money to build them.
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,
random.choice(faction_id[:1]))
cmds.append(cmd)
# Overview: issue a move to every unit giving a random
# direction. Directions can be found in the vec2.py file.
# They are single char strings: 'N', 'E', 'W', 'S'.
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 all the command objects.
return cmds
+41
View File
@@ -0,0 +1,41 @@
# Cell Class
# Stores the attack/defense modifiers and the color
# for each terrain type. Terrain types are defined in
# cell_terrain.py
#
# Currently, Open terrain gives a +2 for an attacker and
# nothing to a defender. Forest gives a +2 to the defender
# and nothing to the attacker.
#
# Feel free to modify.
import cell_terrain
class Cell:
def __init__(self, terrain):
self.terrain = terrain
# TODO: replace this with a data member instead
# of a function.
def get_color(self):
match self.terrain:
case cell_terrain.Terrain.Open:
return "cornsilk2"
case cell_terrain.Terrain.Forest:
return "cornsilk3"
def get_attack_mod(self):
match self.terrain:
case cell_terrain.Terrain.Open:
return 2
case cell_terrain.Terrain.Forest:
return 0
def get_defense_mod(self):
match self.terrain:
case cell_terrain.Terrain.Open:
return 0
case cell_terrain.Terrain.Forest:
return 2
+11
View File
@@ -0,0 +1,11 @@
# Terrain Enum
# If you want to add more terrain types, start here.
#
# Next jump to the params.py and game_map.py file to see how maps
# are generated.
import enum
class Terrain(enum.Enum):
Open = 0
Forest = 1
+29
View File
@@ -0,0 +1,29 @@
# City Class
# Not much to edit here unless you are making the resource
# aspect of the game more complicated.
import unit
class City:
def __init__(self, ID, pos, faction_id, income):
# ID: str - identifier for the city
self.ID = ID
# pos: Vec2 - x,y location of the city on the map
self.pos = pos
# faction_id: str - ID of the faction that owns the city
self.faction_id = faction_id
# income: int - how much money the city generates.
self.income = income
# Zombies don't make income / reproduce
def generate_income(self):
return self.income if self.faction_id != "Zombies" else 0
def build_unit(self, ID, utype):
return unit.Unit(ID, utype, self.faction_id, self.pos)
+385
View File
@@ -0,0 +1,385 @@
Aberdeen
Abilene
Akron
Albany
Albuquerque
Alexandria
Allentown
Amarillo
Anaheim
Anchorage
Ann Arbor
Antioch
Apple Valley
Appleton
Arlington
Arvada
Asheville
Athens
Atlanta
Atlantic City
Augusta
Aurora
Austin
Bakersfield
Baltimore
Barnstable
Baton Rouge
Beaumont
Bel Air
Bellevue
Berkeley
Bethlehem
Billings
Birmingham
Bloomington
Boise
Boise City
Bonita Springs
Boston
Boulder
Bradenton
Bremerton
Bridgeport
Brighton
Brownsville
Bryan
Buffalo
Burbank
Burlington
Cambridge
Canton
Cape Coral
Carrollton
Cary
Cathedral City
Cedar Rapids
Champaign
Chandler
Charleston
Charlotte
Chattanooga
Chesapeake
Chicago
Chula Vista
Cincinnati
Clarke County
Clarksville
Clearwater
Cleveland
College Station
Colorado Springs
Columbia
Columbus
Concord
Coral Springs
Corona
Corpus Christi
Costa Mesa
Dallas
Daly City
Danbury
Davenport
Davidson County
Dayton
Daytona Beach
Deltona
Denton
Denver
Des Moines
Detroit
Downey
Duluth
Durham
El Monte
El Paso
Elizabeth
Elk Grove
Elkhart
Erie
Escondido
Eugene
Evansville
Fairfield
Fargo
Fayetteville
Fitchburg
Flint
Fontana
Fort Collins
Fort Lauderdale
Fort Smith
Fort Walton Beach
Fort Wayne
Fort Worth
Frederick
Fremont
Fresno
Fullerton
Gainesville
Garden Grove
Garland
Gastonia
Gilbert
Glendale
Grand Prairie
Grand Rapids
Grayslake
Green Bay
GreenBay
Greensboro
Greenville
Gulfport-Biloxi
Hagerstown
Hampton
Harlingen
Harrisburg
Hartford
Havre de Grace
Hayward
Hemet
Henderson
Hesperia
Hialeah
Hickory
High Point
Hollywood
Honolulu
Houma
Houston
Howell
Huntington
Huntington Beach
Huntsville
Independence
Indianapolis
Inglewood
Irvine
Irving
Jackson
Jacksonville
Jefferson
Jersey City
Johnson City
Joliet
Kailua
Kalamazoo
Kaneohe
Kansas City
Kennewick
Kenosha
Killeen
Kissimmee
Knoxville
Lacey
Lafayette
Lake Charles
Lakeland
Lakewood
Lancaster
Lansing
Laredo
Las Cruces
Las Vegas
Layton
Leominster
Lewisville
Lexington
Lincoln
Little Rock
Long Beach
Lorain
Los Angeles
Louisville
Lowell
Lubbock
Macon
Madison
Manchester
Marina
Marysville
McAllen
McHenry
Medford
Melbourne
Memphis
Merced
Mesa
Mesquite
Miami
Milwaukee
Minneapolis
Miramar
Mission Viejo
Mobile
Modesto
Monroe
Monterey
Montgomery
Moreno Valley
Murfreesboro
Murrieta
Muskegon
Myrtle Beach
Naperville
Naples
Nashua
Nashville
New Bedford
New Haven
New London
New Orleans
New York
New York City
Newark
Newburgh
Newport News
Norfolk
Normal
Norman
North Charleston
North Las Vegas
North Port
Norwalk
Norwich
Oakland
Ocala
Oceanside
Odessa
Ogden
Oklahoma City
Olathe
Olympia
Omaha
Ontario
Orange
Orem
Orlando
Overland Park
Oxnard
Palm Bay
Palm Springs
Palmdale
Panama City
Pasadena
Paterson
Pembroke Pines
Pensacola
Peoria
Philadelphia
Phoenix
Pittsburgh
Plano
Pomona
Pompano Beach
Port Arthur
Port Orange
Port Saint Lucie
Port St. Lucie
Portland
Portsmouth
Poughkeepsie
Providence
Provo
Pueblo
Punta Gorda
Racine
Raleigh
Rancho Cucamonga
Reading
Redding
Reno
Richland
Richmond
Richmond County
Riverside
Roanoke
Rochester
Rockford
Roseville
Round Lake Beach
Sacramento
Saginaw
Saint Louis
Saint Paul
Saint Petersburg
Salem
Salinas
Salt Lake City
San Antonio
San Bernardino
San Buenaventura
San Diego
San Francisco
San Jose
Santa Ana
Santa Barbara
Santa Clara
Santa Clarita
Santa Cruz
Santa Maria
Santa Rosa
Sarasota
Savannah
Scottsdale
Scranton
Seaside
Seattle
Sebastian
Shreveport
Simi Valley
Sioux City
Sioux Falls
South Bend
South Lyon
Spartanburg
Spokane
Springdale
Springfield
St. Louis
St. Paul
St. Petersburg
Stamford
Sterling Heights
Stockton
Sunnyvale
Syracuse
Tacoma
Tallahassee
Tampa
Temecula
Tempe
Thornton
Thousand Oaks
Toledo
Topeka
Torrance
Trenton
Tucson
Tulsa
Tuscaloosa
Tyler
Utica
Vallejo
Vancouver
Vero Beach
Victorville
Virginia Beach
Visalia
Waco
Warren
Washington
Waterbury
Waterloo
West Covina
West Valley City
Westminster
Wichita
Wilmington
Winston
Winter Haven
Worcester
Yakima
Yonkers
York
Youngstown
+18
View File
@@ -0,0 +1,18 @@
import vec2
class Command:
def __init__(self, faction_id):
self.faction_id = faction_id
class MoveUnitCommand(Command):
def __init__(self, faction_id, unit_id, direction):
Command.__init__(self, faction_id)
self.unit_id = unit_id
self.direction = direction
class BuildUnitCommand(Command):
def __init__(self, faction_id, city_id, utype):
Command.__init__(self, faction_id)
self.city_id = city_id
self.utype = utype
+28
View File
@@ -0,0 +1,28 @@
# Faction class
# You are welcome to add more data here if you want to keep
# persistent info in the faction object rather than the AI object.
# Or if you want to make the game's resources more complex.
# Currently, there's only money.
import unit
class Faction:
def __init__(self, ID, money, ai, color):
self.ID = ID
self.money = money
self.ai = ai
self.next_unit_id = 0
self.color = color
def get_next_unit_id(self):
uid = self.next_unit_id
self.next_unit_id += 1
return uid
def can_build_unit(self, cost):
return cost <= self.money
# ################################################################
def run_ai(self, factions, cities, units, gmap):
return self.ai.run_ai(self.ID, factions, cities, units, gmap)
+21
View File
@@ -0,0 +1,21 @@
# GameMap class
# Not much going on here. Builds the map cell by cell
# with a random terrain type.
import random
import vec2
import cell
import params
class GameMap:
def __init__(self, width, height):
self.height = height
self.width = width
self.cells = {}
for x in range(width):
for y in range(height):
self.cells[vec2.Vec2(x, y)] = cell.Cell(
params.get_random_terrain(random.random()))
def get_cell(self, pos):
return self.cells[pos]
+147
View File
@@ -0,0 +1,147 @@
---
documentclass: extarticle
fontsize: 14pt
geometry: margin=2cm
graphics: yes
colorlinks: true
---
\begin{center}
\large
\textbf{Homework 4 - Strategy and Tactics}
\small
COS498/598 - Video Game AI - Spring 2025
by Zachary Hutchinson
Due Date: April 11, 2025, Midnight, Brightspace
\end{center}
---
## Goal
Use game AI to:
- analyze the strengths and weaknesses of force distributions.
- coordinate the movement of multiple units.
- decide force composition.
## Submission:
You are required to submit the following items.
1. An AI for the game.
2. A write-up describing your AI and any other modifications.
### AI Requirements
First, please submit all game files...even those you didn't modify. This includes any resources distributed with the game. In other words, I should be able to run what you submit without having to add files.
Second, you are only required to write one AI (to be used by all factions). The AI must, at a minimum, build units according to some plan and move units according to some strategy. You are welcome to differentiate the AIs of the different factions but it isn't required.
Third, you are welcome to use some degree of randomness in your AI, however the AI's build and move strategies cannot rely substantially on randomness. In other words, *why* and *how* the AI made and carried out a particular choice must be the result, in part, of decision logic.
### Write-up Requirements
The write-up should contain a detailed description of your AI. Include (if applicable), the general approach, build and move decisions, and low and high level decisions. Do you make use of terrain? How do you go on the attack? How do you defend? How do you identify targets? How do you assess the strength of an AI's position? Or enemy position?
Also, if you've made modifications to the game itself, please include those as well.
To sum up: I will read your write-up first when grading. Once I read it, I will run the game a few times. The write-up should explain what I'm seeing.
## The Game
The game is a typical strategy game based (strongly) on the rock-paper-scissors concept common to so many games. The map consists of cells (default 40x30). Each cell has a terrain which impacts combat asymmetrically.
Factions possess cities which produce income. Income is used to purchase units. All cities in the default game are identical. Cities are displayed by a cell outline in the color of the owning faction.
There are three unit types (utypes) in the game:
- R: rock
- S: scissors
- P: paper
They are displayed on the map with those four capital letters. The unit types are identical except in combat. Combat involves rolling a d10 (0 to 10) for both the attacker and defender and deducting the rolls from the other unit's health. If one of the two units would be a winner in rock-paper-scissors (for example, R vs S), then the would be winner (R in this example) rolls a d20 (0 to 20) while the other still rolls the normal d10.
When a unit loses all its health, it is removed from the game. Combat is rather lethal. It can kill both units.
The game continues until one faction holds all the cities.
### Additional Rules Enforced by the Engine
- Two units cannot occupy the same map cell.
- If a unit is ordered to move into a cell occupied by a friendly unit, the order will be ignored.
- Combat is initiated by a unit trying to move into a cell occupied by a unit of a different faction.
- A city cannot build a unit if the city is occupied by a friendly unit.
- A faction must have enough money to build a unit otherwise the BuildUnitCommand is ignored.
- If your AI script issues more than one BuildUnitCommand but can only afford one unit, only the first encountered will be executed.
- Units can only move once. If issue two MoveUnitCommands for the same unit, only the first encountered will be executed.
- Factions start with a pool of money (default 1000)
- Commands are shuffled before execution. This means they will not be executed in the order your AI creates them or in faction order. (NOTE: I feel this design choice is a bit controversial. You're welcome to turn this off by deleting the shuffle() call at the top of the RunAllCommands() function in main.py if you feel your AI approach would benefit from a predictable turn order.)
- Terrain does not impact movement in the default game (because there are no movement points).
## Files
What follows is a description of the files associated with this project. The descriptions below are minimal. The Python3 code files contains a lot of information. I suggest reading through all the comments in all files before starting.
If you don't plan on modifying the game itself, you probably won't need to edit any files other than ai.py.
NOTE: You are welcome to create more files. For example, if you want to offload AI code to some other module.
### ai.py
This is where most (if not all) of your code will go.
### cell.py
Defines the map cell's (squares).
### cell_terrain.py
Defines map terrain.
### city.py
The city class
### command.py
The Command class. You should take a look at this one. Commands are very simple (mostly data) classes. Your AI script will need to create Commands.
### faction.py
The faction class.
### game_map.py
The GameMap class.
### main.py
A bloated file with display, engine and game loop code. Run this to start the game.
### params.py
Some constants and functions which define several basic aspects of the game.
### unit.py
The unit class. Take a look at this one.
### vec2.py
A 2D vector class to track unit and city position.
## Using the Game
Here are a list of the hot keys for the game:
- LEFT ARROW: Increases game speed.
- RIGHT ARROW:: Decreases game speed.
- Q: quits the game
## Grading
- 30% Write-up
- 60% The AI
## Extra Credit
I will award extra credit for substantial, interesting modifications/extensions/overhauls of the game itself. You must have a goal/reason for your modification and they should be well-reasoned. Changing one or two default values is not really interesting.
Credit will be on a sliding scale from 0 points to 10 points.
Examples: (NOTE: these are just estimates of the scope of work per points.)
- 0 pts: a very lazy, minimal change without a core idea behind it. Or you didn't mention something in your write-up.
- 1 pt: extensively modifying the values in the game to create a new experience.
- 2 pts: adding new terrain types
- 3-9 pts: new unit types, or more (and different) Commands, or a more complex resource game. (The points awarded will be based on how many you include).
- 10 pts: complete overhaul/reimagining of the game turning it into a nightmare of complexity and awesomeness.
With one requirement: whatever you add must be used by your AI. If it isn't used by your AI, the modifications don't count.
**To get extra credit** you must tell me in your write-up, in a separate section, exactly what you did and **why**.
BIN
View File
Binary file not shown.
+602
View File
@@ -0,0 +1,602 @@
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()
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(8, 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()
def_roll += def_cell.get_defense_mod()
# 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):
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)
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)
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("white")
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)
if game_over[0]:
print(f"Winning faction: {game_over[1]}")
display.run = False
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")
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
pygame.display.flip()
def main():
random.seed(None)
display = init_display(1000, 600)
GameLoop(display)
if __name__ == "__main__":
main()
+59
View File
@@ -0,0 +1,59 @@
import cell_terrain
import random
# ##########################################################33
# MAP RELATED STUFF
# Relative probabilities for each terrain type to appear on
# the map. Currently, Open terrain is four times more likely to
# appear than forest. The values are relative (discrete distribution).
# For example, 5 and 20 would produce the same map as the values
# below. If you add more terrain types, adding them here
# will cause them to appear on the map. Terrain generation
# is completely random. There's no fancy procedural content gen.
# 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.Open: 4
}
TERRAINS = list(CELL_TERRAIN_PROBABILITY.keys())
PROBS = list(CELL_TERRAIN_PROBABILITY.values())
TOTAL = sum(PROBS)
running_sum = 0
for i, p in enumerate(PROBS):
PROBS[i] = (p+running_sum)/TOTAL
running_sum += p
def get_random_terrain(roll):
for i, p in enumerate(PROBS):
if roll <= p:
return TERRAINS[i]
#print(roll)
# ##########################################################33
# FACTION STUFF
#
# How much money does a city generate per turn?
CITY_INCOME = 10
# How many cities does each faction start with?
CITIES_PER_FACTION = 5
# How much money does a faction start with?
STARTING_FACTION_MONEY = 1000
# The rest of this is used to give the cities random
# ID (aka names). These random names don't appear visibly
# in the game, but if you want them to, they are there
# and already being loaded into the cities on instantiation.
CITY_IDS = []
with open('city_names') as f:
for line in f:
line = line.strip()
CITY_IDS.append(line)
def get_random_city_ID():
return random.choice(CITY_IDS)
+101
View File
@@ -0,0 +1,101 @@
# The Unit Class
# The two areas of potential modification are:
# - The dictionaries at the top of the file
# - The roll() function.
import random
import math
# UNIT_COSTS is a constant dictionary that holds the resource (money)
# costs for each type of unit.
# UNITS
# Z = ZOMBIE (SPAWN RATE IS ONLY SET INITIALLY [RANDOM NUMBER * 100])
# S = SURVIVOR (DEFAULT SPAWN RATE)
# C = CURED (EXTREMELY SLOW)
UNIT_COSTS = {
"Z": 100,
"S": 500,
"C": 2500
}
# UNIT_HEALATH is a constant dictionary that holds the starting health
# for a unit of a given unit type (utype).
# UNITS
# Z = ZOMBIE (LOW HEALTH)
# S = SURVIVOR (DEFAULT HEALTH)
# C = CURED (HIGH HEALTH)
UNIT_HEALTH = {
"Z": 5,
"S": 7,
"C": 7
}
# UNIT_SIGHT: Not used. Ignore.
UNIT_SIGHT = {
"Z": 2,
"S": 2,
"C": 2
}
UNIT_FACTIONS = {
"Zombies": "Z",
"Survivors": "S",
"Cured": "C"
}
# You should use this function in your AI to test unit costs
def get_unit_cost(utype):
try:
return UNIT_COSTS[utype]
except KeyError:
return math.inf
class Unit:
def __init__(self, ID, utype, faction_id, pos, health, sight_radius):
# id: int
self.ID = ID
# utype: unit type
# string: Z, S, or C
self.utype = utype
# faction_id: str
self.faction_id = faction_id
# pos: vec2
self.pos = pos
# health: int
self.health = health
# sight_radius: int - how far it sees
# NOT USED.
self.sight_radius = sight_radius
def __eq__(self, o):
return self.ID == o.ID and self.faction_id == o.faction_id
# Infection Function
def infect(self, new_faction):
self.utype = UNIT_FACTIONS[new_faction]
self.health = UNIT_HEALTH[self.utype]
self.faction_id = new_faction
# Combat Function:
# Essentially, it is an NxN matrix for all the different
# unit-to-unit match ups. Currently, the winning combinations of
# 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)
+49
View File
@@ -0,0 +1,49 @@
# A simple vector 2 class to track unit and city positions.
#
# NOTE: You can make use of the distance_line and distance_man
# functions in your AI code.
import math
class Vec2:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, o):
return o.x == self.x and o.y == self.y
def __ne__(self, o):
return not (self == o)
def __hash__(self):
return hash((self.x, self.y))
def __str__(self):
return f"({self.x},{self.y})"
def __add__(self, o):
return Vec2(self.x + o.x, self.y + o.y)
# Straight-line distance
def distance_line(self, o):
return math.sqrt(
(o.x - self.x)**2 +
(o.y - self.y)**2)
# Manhattan distance
def distance_man(self, o):
return (o.x - self.x) + (o.y - self.y)
def mod(self, maxX, maxY):
self.x = (self.x + maxX) % maxX
self.y = (self.y + maxY) % maxY
MOVES = {
'N': Vec2(0,-1),
'E': Vec2(1,0),
'S': Vec2(0,1),
'W': Vec2(-1,0)
}