From 2a9a9b4fb923dadddf5763b2dd8549455387c170 Mon Sep 17 00:00:00 2001 From: Nicholas Pease Date: Tue, 4 Feb 2025 02:05:26 +0000 Subject: [PATCH] Final --- .devcontainer/devcontainer.json | 22 ++ README.md | 14 +- hw1_main.py | 387 ++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+), 7 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 hw1_main.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8c6f3fb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} diff --git a/README.md b/README.md index c16eb5d..a372f5a 100644 --- a/README.md +++ b/README.md @@ -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) -
+## 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) +
diff --git a/hw1_main.py b/hw1_main.py new file mode 100644 index 0000000..70c0ffc --- /dev/null +++ b/hw1_main.py @@ -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()