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/__pycache__/gobjs.cpython-312.pyc b/__pycache__/gobjs.cpython-312.pyc new file mode 100644 index 0000000..c8d1626 Binary files /dev/null and b/__pycache__/gobjs.cpython-312.pyc differ diff --git a/gobjs.py b/gobjs.py index ef082dd..03ffefb 100644 --- a/gobjs.py +++ b/gobjs.py @@ -2,6 +2,9 @@ import math import random import pygame +BLUE_SAYINGS = ("Oh god my back", "I need to do more cardio", "That LASIK was amazing!") +RED_SAYINGS = ("Gotta-gotta-gotta go", "I'm a speed demon", "I'm a blur!") +YELLOW_SAYINGS = ("Look at these idiots","I'm average man!","...") class GObj: def __init__(self, x, y, radius, speed, turn_rate, heading, sight_distance, @@ -74,7 +77,6 @@ class Player(GObj): self.y+math.sin(self.heading)*self.radius ), 2) - class Goal(GObj): def __init__(self, x, y, radius=20, speed=0, turn_rate=0.0, @@ -114,6 +116,8 @@ class Enemy(GObj): (x1,y1), (x2,y2) ] + + self.ticks = random.randint(1,10) * 60 def draw(self, screen): xy0 = self.sight_cone[0] @@ -174,11 +178,54 @@ class Enemy(GObj): return (True, (dx, dy), dist) # Base class AI routine - def ai(self, percept, goals, comms): + # Default patroling state shared by all guards + # added sayings field to feed in default wandering sayings + def ai(self, percept, goals, comms, sayings): + if (self.color == "red"): + current_guard = "R" + elif (self.color == "dodgerblue"): + current_guard = "B" + elif (self.color == "yellow"): + current_guard = "Y" + + # assign self goal if not already assigned + # add guarding attibute to comms if not already present + if comms[current_guard] == None: + comms[current_guard] = random.randint(0, len(goals)-2) + # ensure goal selected is not selected by another guard and update if it is + + # ensure guarding area is not touched, and update if it is + if goals[comms[current_guard]].is_touched(): + while goals[comms[current_guard]].is_touched(): + comms[current_guard] = random.randint(0, len(goals)-1) + + if "spotted" not in comms: + comms["spotted"] = None + elif comms["spotted"] is not None and comms["spotted"]["spotter"] == self.color: + comms["spotted"] = None + + # State checks + + # If enemy is spotted, move toward enemy + if percept[0]: + comms["spotted"] = {"player": CalculatePlayersCurrentPosition(self, percept[2]), "spotter": self.color} + self.ticks = 7 * 60 # reset ticks for sayings after saying something + return (TurnTowardEnemy(self.heading, percept[1]), 1, ("Get back here!",2000)) + # if enemy is spotted by other guard, move toward enemy as reported by guard + elif comms["spotted"] is not None: + spottedPlayer = comms["spotted"]["player"] + self.ticks = 7 * 60 # reset ticks for sayings after saying something + return (TurnTowardCoords(self, Player(spottedPlayer[0], spottedPlayer[1])), 1, ("Engaging Suspect!",2000)) + # check if at guarding goal, assign new goal if there + elif self.check_collision(goals[comms[current_guard]]): + comms[current_guard] = random.randint(0, len(goals)-1) + return (0.0, 0.0, None) + # move toward guarding goal + else: + text = GenerateWanderingText(sayings, self) + return (TurnTowardCoords(self, goals[comms[current_guard]]), 0.7, text) - return (0.0, 0.0, None) - - +# Yellow is the a mix of the two class EnemyYellow(Enemy): def __init__(self, x, y, radius=10, speed=90, turn_rate=3.0, heading=0.0, sight_distance=120, @@ -186,20 +233,14 @@ class EnemyYellow(Enemy): Enemy.__init__(self, x, y, radius, speed, turn_rate, heading, sight_distance, color, fill, goals) - self.ticks = 0 # This is ai routine for the type A enemy. - def ai(self, percept, goals, comms): - if self.ticks > 0: - self.ticks-=1 - elif self.ticks == 0 and percept[0]: - self.ticks = 60*4 - return (0.0, 0.0, ("Don't make me chase you!",2000) ) - return (0.0, 0.0, None) - + def ai(self, percept, goals, comms): + return Enemy.ai(self, percept, goals, comms, YELLOW_SAYINGS) +# Blue is quite slow but has a good sight distance class EnemyBlue(Enemy): - def __init__(self, x, y, radius=10, speed=90, turn_rate=3.0, - heading=0.0, sight_distance=120, + def __init__(self, x, y, radius=10, speed=60, turn_rate=2.0, + heading=0.0, sight_distance=220, color="dodgerblue", fill=0, goals=[]): Enemy.__init__(self, x, y, radius, speed, turn_rate, heading, sight_distance, color, @@ -208,28 +249,58 @@ class EnemyBlue(Enemy): self.ticks = 0 # This is the ai routing for the type B enemy. def ai(self, percept, goals, comms): - if self.ticks > 0: - self.ticks-=1 - elif self.ticks == 0 and percept[0]: - self.ticks = 60*4 - return (0.0, 0.0, ("I see you!",2000) ) - return (0.0, 0.0, None) + return Enemy.ai(self, percept, goals, comms, BLUE_SAYINGS) +# Red is blind and cannot see well +# but red is fast and zippy class EnemyRed(Enemy): - def __init__(self, x, y, radius=10, speed=90, turn_rate=3.0, - heading=0.0, sight_distance=120, + def __init__(self, x, y, radius=10, speed=125, turn_rate=7.0, + heading=0.0, sight_distance=50, color="red", fill=0, goals=[]): Enemy.__init__(self, x, y, radius, speed, turn_rate, heading, sight_distance, color, fill, goals) - self.ticks = 0 - # This is the ai routing for the type B enemy. def ai(self, percept, goals, comms): - if self.ticks > 0: - self.ticks-=1 - elif self.ticks == 0 and percept[0]: - self.ticks = 60*4 - return (0.0, 0.0, ("Enemy!",2000) ) - return (0.0, 0.0, None) + return Enemy.ai(self, percept, goals, comms, RED_SAYINGS) + +# Common Functions +def TurnTowardEnemy(heading_angle, enemy_vector): + # Convert heading angle to a unit vector + heading_vector = [math.cos(heading_angle), math.sin(heading_angle)] + # Calculate the dot product + dot_product = heading_vector[0] * enemy_vector[0] + heading_vector[1] * enemy_vector[1] + # Calculate the angle between the two vectors + angle = math.acos(dot_product) + # Calculate the cross product (in 2D, this is the determinant of the 2x2 matrix) + cross_product = heading_vector[0] * enemy_vector[1] - heading_vector[1] * enemy_vector[0] + return math.copysign(angle / math.pi, cross_product) + +def TurnTowardCoords(self, target): + dx = target.x - self.x + dy = target.y - self.y + dist = math.sqrt(dx*dx + dy*dy) + dx /= dist + dy /= dist + + # Patrolling is slower + return TurnTowardEnemy(self.heading, (dx, dy)) + +def CalculatePlayersCurrentPosition(self, distance): + # calculate player coordinates using position (self.x, self.y), heading (self.heading) and distance (distance) + return (self.x + distance * math.cos(self.heading), self.y + distance * math.sin(self.heading)) + +def GenerateWanderingText(sayings, self): + # Function is run at 60 / second per AI + # This function will generate a random saying from the list of sayings + # One saying per 7 seconds of walking + # 7 seconds is 4200 milliseconds + # 4200 / 60 = 70 + # 70 ticks + if self.ticks == 0: + self.ticks = 60 * random.randint(1,10) + 60 + return (random.choice(sayings), 2000) + else: + self.ticks -= 1 + return None \ No newline at end of file