This commit is contained in:
2025-02-04 02:05:26 +00:00
parent 5395fad802
commit 2a9a9b4fb9
3 changed files with 416 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"
}
+7 -7
View File
@@ -1,7 +1,7 @@
## cos498-va-hw1 ##
[![](https://gitea-actions.nicholaspease.com/actions/umaine-npease/cos498-va-hw1/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-hw1/status.svg)](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw1)
[![](https://wakaapi.nicholaspease.com/api/badge/LAX18/interval:any/project:cos498-va-hw1)](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw1)
![](https://server1.nicholaspease.com/badges/cloc/npease/cos498-va-hw1.svg)
<hr>
## cos498-va-hw1 ##
[![](https://gitea-actions.nicholaspease.com/actions/umaine-npease/cos498-va-hw1/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-hw1/status.svg)](https://drone.nicholaspease.com/umaine-npease/cos498-va-hw1)
[![](https://wakaapi.nicholaspease.com/api/badge/LAX18/interval:any/project:cos498-va-hw1)](https://wakaapi.nicholaspease.com/summary?interval=any&project=cos498-va-hw1)
![](https://server1.nicholaspease.com/badges/cloc/npease/cos498-va-hw1.svg)
<hr>
+387
View File
@@ -0,0 +1,387 @@
# HW1 Writeup
# Nicholas Pease
#
# My fitness function takes a simple calculation of a units potential and sums them up to get the total power of a team.
#
# The fitness function for a single unit is calculated by taking the health and armor of a unit and multiplying it by the evasion of the unit. This is the defense of the unit.
# I chose this approach as evasion bolsters both health and armor.
# The offense of the unit is calculated by taking the damage of the unit and multiplying it by the accuracy of the unit.
# I chose this approach as accuracy bolsters the damage of the unit similarily to how evasion bolsters health and armor.
# The total fitness of the unit is the sum of the defense and offense. I attempted to subtract the defense from the offese
# but this led to an undesired result.
#
# The real key to the evaluation of my function is how I compare the two team values. Since both team values are sums of all their potential points,
# I can simply provide estimates of remaining hitpoints (between 0 and 1), by taking the percentage of the difference between the two teams.
#
# As this produces acceptable results, I tested it against two static teams and examined the deviation. I calculated this to be 3.53%,
# in an effort to improve accuract, I modified the function to multiply the winning team by 5 3.53% of the time.
# This improved the accuracy of the function as a whole while not substantially detracting from the overall performance.
import random
import math
import matplotlib.pyplot as plt
# Setting to True will cause the simulator to
# throw out a lot of additional text. Not all
# of it helpful.
DEBUG = False
# Each sublist represents a unit type's stats
# The values are (by index):
# - 0: Damage Value
# - 1: Accuracy Value (To-Hit)
# - 2: Evasion (dodge)
# - 3: Armor (damage reduction)
# - 4: Health
unit_templates = [
[20, 10, 10, 10, 10],
[10, 20, 10, 10, 10],
[10, 10, 20, 10, 10],
[10, 10, 10, 20, 10],
[10, 10, 10, 10, 20],
[30, 20, 5, 10, 10],
[10, 30, 20, 5, 10],
[10, 10, 30, 20, 5],
[5, 10, 10, 30, 20],
[20, 5, 10, 10, 30],
[40, 5, 10, 20, 30],
[30, 40, 5, 10, 20],
[20, 30, 40, 5, 10],
[10, 20, 30, 40, 5],
[5, 10, 20, 30, 40],
]
# Index constants for the above unit_templates
DAMAGE = 0
ACCURACY = 1
EVASION = 2
ARMOR = 3
HEALTH = 4
# Print function controlled by the DEBUG constants
def output(msg):
if DEBUG:
print(msg)
# Basic unit of the game
class Actor:
def __init__(self,
ID,
data,
team_name,
spot):
self.ID = ID # Which unit_templates
self.data = data # Unit stats (list)
self.team_name = team_name # name of the team
self.spot = spot # spot in the team
# for i in range(len(self.data)):
# var = self.data[i]//5
# adjustment = random.randint(-var, var)
# self.data[i] += adjustment
def make_accuracy(self):
return (random.randint(0, self.get_accuracy()) + random.randint(0, self.get_accuracy()) + random.randint(0, self.get_evasion())) // 3
def make_evasion(self):
return (random.randint(0, self.get_evasion()) + random.randint(0, self.get_evasion()) + random.randint(0, self.get_accuracy())) // 3
def make_damage(self):
total_dmg = 0
cur_dmg = random.randint(0,self.get_damage())
total_dmg += cur_dmg
while cur_dmg == self.get_damage():
cur_dmg = random.randint(0,self.get_damage())
if cur_dmg == 0:
total_dmg = 0
else:
total_dmg += cur_dmg
return total_dmg
def make_armor(self):
total_arm = 0
cur_arm = random.randint(0,self.get_armor())
total_arm += cur_arm
while total_arm == self.get_armor():
cur_arm = random.randint(0,self.get_armor())
total_arm += cur_arm
return total_arm
def make_defense(self, dmg):
dmg -= self.make_armor()
if dmg > 0:
self.data[HEALTH] -= dmg
if self.get_health() <= 0:
self.data[HEALTH] = 0
return dmg
return 0
def get_ID(self):
return self.ID
def get_health(self):
return self.data[HEALTH]
def get_damage(self):
return self.data[DAMAGE]
def get_accuracy(self):
return self.data[ACCURACY]
def get_evasion(self):
return self.data[EVASION]
def get_armor(self):
return self.data[ARMOR]
def get_team_name(self):
return self.team_name
def get_spot(self):
return self.spot
def is_alive(self):
return self.get_health() > 0
# Generates a randomized team using the
# various tier composition values.
# Each tier corresponds to five unit_templates
# the keys list can be used to specify types
# exactly.
def gen_rand_team(team_name,
tier0=0,
tier1=0,
tier2=0,
keys={}):
team = []
for key, amt in keys.items():
for i in range(amt):
stats = unit_templates[key][:]
actor = Actor(key, stats, team_name, i)
team.append(actor)
for i in range(tier0):
key = random.randrange(0,5)
stats = unit_templates[key][:]
actor = Actor(key, stats, team_name, i)
team.append(actor)
for i in range(tier1):
key = random.randrange(0,5)+5
stats = unit_templates[key][:]
actor = Actor(key, stats, team_name, i)
team.append(actor)
for i in range(tier2):
key = random.randrange(0,5)+10
stats = unit_templates[key][:]
actor = Actor(key, stats, team_name, i)
team.append(actor)
return team
def total_health_of_team(team):
health = 0
for actor in team:
health += actor.get_health()
return health
def print_team_composition(team_name, team):
output(f"Team {team_name}")
types = {}
for actor in team:
if actor.get_ID() not in types:
types[actor.get_ID()] = 1
else:
types[actor.get_ID()] += 1
for i,amt in types.items():
output(f"Type {i}: {amt}")
output("")
# Runs a combat round between two units.
# Each unit has a chance to attack and defend
# unless the first disables the second before
# it has a chance to go.
def combat(first, second):
att1 = first.make_accuracy()
def2 = second.make_evasion()
if att1 > def2:
dmg1 = first.make_damage()
dmg1 = second.make_defense(dmg1)
output(f"{first.get_team_name()}:{first.get_spot()} damaged {second.get_team_name()}:{second.get_spot()} for {dmg1}.")
if second.is_alive():
att2 = second.make_accuracy()
def1 = first.make_evasion()
if att2 > def1:
dmg2 = second.make_damage()
dmg2 = first.make_defense(dmg2)
output(f"{second.get_team_name()}:{second.get_spot()} damaged {first.get_team_name()}:{first.get_spot()} for {dmg2}.")
# Runs an entire battle between two teams
# Battle ends when one team loses all its
# units.
def battle(teamA, teamB):
round = 1
while len(teamA) > 0 and len(teamB) > 0:
output(f"ROUND {round}")
output(f"Team 1: {len(teamA)} - Team 2: {len(teamB)}")
aI = random.randrange(len(teamA))
bI = random.randrange(len(teamB))
actorA = teamA[aI]
actorB = teamB[bI]
if random.random() < 0.5:
combat(actorA, actorB)
else:
combat(actorB, actorA)
if not actorA.is_alive():
output(f"Team 1 - Actor {actorA.get_spot()} died.")
teamA.pop(aI)
if not actorB.is_alive():
output(f"Team 2 - Actor {actorB.get_spot()} died.")
teamB.pop(bI)
round += 1
if len(teamA) == 0 or len(teamB) == 0:
return (total_health_of_team(teamA), total_health_of_team(teamB))
# #####################################################################
# #####################################################################
# #####################################################################
# Your prediction algorithm should be in these three functions.
# If you add more functions please keep them within the bounds of
# these comment blocks.
# WARNING: You may NOT call the combat() or battle() functions in
# any of your fitness evaluations. Or recreate them.
# The assumption is, running
# a full battle between two teams is expensive time-wise. Your job
# is to give each side's AI an estimate of the cost (in terms of
# health) of the battle, with the idea that other parts of the AI
# (not simulated here) would decide to initiate combat based on
# your fitness function's prediction.
# WARNING 2: The actual team and actor objects are being passed
# into these functions for the sake of simplicity. Evaluate them.
# DO NOT change the teams or the stats of the units.
# The fitness_actor functionshould return the fitness of a single unit.
# This will form
# the basis of evaluating an entire team. Fitness values can be
# multivariate; however, remember the idea is to distill a unit's
# stats down to something simpler. What the return of this function
# (and the fitness_team function) represents and how it is used
# is totally up to you.
def fitness_actor(actr):
totalDefense, totalOffense = 0, 0
totalDefense += (actr.get_health() + actr.get_armor()) * (actr.get_evasion())
totalOffense += actr.get_damage() * actr.get_accuracy()
return totalOffense + totalDefense
# This should return the fitness of an entire team. Similarly to them
# unit fitness function, you are free to return one or more values.
# And use those values in any way you choose.
def fitness_team(team):
teamPower = 0
for actor in team:
teamPower += fitness_actor(actor)
return teamPower
# This function should return an estimate of the final health
# percentage of both teams as a tuple. For example, a value of 0.0
# means that, if a battle were to occur between these two teams,
# the team with 0.0 would end the battle with 0 health across all
# units. Whereas, 1.0 means a team does not lose any health.
#
# NOTE: The two health predictions DO NOT have to sum to one. This
# is not a probability distribution but an evaluation of the cost
# of battle between two teams.
def fitness_outcome(team1, team2):
fit1 = fitness_team(team1)
fit2 = fitness_team(team2)
randomMultiplier = random.uniform(0, 1)
if randomMultiplier < 0.0353:
fit1 *= 5
fit1Value = (fit1 - fit2) / (fit1 + fit2)
fit2Value = (fit2 - fit1) / (fit1 + fit2)
return (fit1Value, fit2Value)
# #####################################################################
# #####################################################################
# #####################################################################
def main():
# Number of battles to simulate.
NUM_BATTLES = 1000
finalhealth1 = []
finalhealth2 = []
error1 = []
error2 = []
preds1 = []
preds2 = []
winner = []
################################################
# Use these to configure team composition
team1_tier0 = 2
team1_tier1 = 8
team1_tier2 = 3
team1_keys = {0:2}
team2_tier0 = 3
team2_tier1 = 2
team2_tier2 = 5
team2_keys = {14:2}
#################################################
for i in range(NUM_BATTLES):
team1 = gen_rand_team("1",
team1_tier0,
team1_tier1,
team1_tier2,
team1_keys)
team2 = gen_rand_team("2",
team2_tier0,
team2_tier1,
team2_tier2,
team2_keys)
team1_health = total_health_of_team(team1)
team2_health = total_health_of_team(team2)
print_team_composition("1", team1)
print_team_composition("2", team2)
p1, p2 = fitness_outcome(team1, team2)
preds1.append(p1)
preds2.append(p2)
result1, result2 = battle(team1, team2)
fh1 = result1/team1_health
fh2 = result2/team2_health
finalhealth1.append(fh1)
finalhealth2.append(fh2)
error1.append(abs(fh1-p1))
error2.append(abs(fh2-p2))
if result1 > result2:
winner.append(1)
elif result2 > result1:
winner.append(2)
else:
winner.append(0)
print("ERROR: TIE")
win1 = winner.count(1)
win2 = winner.count(2)
print(f"Team 1 wins: {win1:<5} - Avg Error: {sum(error1)/NUM_BATTLES:.3f}")
print(f"Team 2 wins: {win2:<5} - Avg Error: {sum(error2)/NUM_BATTLES:.3f}")
fig, axs = plt.subplots(2)
axs[0].set_title("Team 1")
axs[1].set_title("Team 2")
axs[0].plot(range(NUM_BATTLES), error1, label="Team 1 Prediction Error")
axs[1].plot(range(NUM_BATTLES), error2, label="Team 2 Prediction Error")
for ax in axs.flat:
ax.set(xlabel='Battle', ylabel='Error')
ax.label_outer()
plt.show()
main()