Initial
This commit is contained in:
@@ -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.
@@ -1,7 +1,7 @@
|
||||
## cos498-va-hw4 ##
|
||||
|
||||
[](https://gitea-actions.nicholaspease.com/latest-log?branch=main)
|
||||
[](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw4)
|
||||
[](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw4)
|
||||

|
||||
<hr>
|
||||
## cos498-va-hw4 ##
|
||||
|
||||
[](https://gitea-actions.nicholaspease.com/latest-log?branch=main)
|
||||
[](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw4)
|
||||
[](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw4)
|
||||

|
||||
<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.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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**.
|
||||
Binary file not shown.
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user