306 lines
12 KiB
Python
306 lines
12 KiB
Python
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,
|
|
color="white", fill=0):
|
|
self.x = x
|
|
self.y = y
|
|
self.radius = radius
|
|
self.speed = speed
|
|
self.turn_rate = turn_rate
|
|
self.heading = heading
|
|
self.sight_distance = sight_distance
|
|
self.color = color
|
|
self.fill = fill
|
|
def pos(self):
|
|
return (self.x, self.y)
|
|
def move(self, dt, direction=1.0):
|
|
self.x += direction*self.speed*dt*math.cos(self.heading)
|
|
self.y += direction*self.speed*dt*math.sin(self.heading)
|
|
def turn(self, dt, direction):
|
|
self.heading += direction*self.turn_rate*dt
|
|
# def can_see(self, gameobj):
|
|
# ox = gameobj.x
|
|
# oy = gameobj.y
|
|
|
|
# x1 = self.x
|
|
# y1 = self.y
|
|
# x2 = x1+math.cos(self.heading)*self.sight_distance
|
|
# y2 = y1+math.sin(self.heading)*self.sight_distance
|
|
# D = x1*y2 - x2*y1
|
|
# I = gameobj.radius**2 * self.sight_distance**2 - D**2
|
|
# return I >= 0
|
|
def check_collision(self, gameobj):
|
|
distance = math.sqrt( (gameobj.x-self.x)**2 + (gameobj.y-self.y)**2 )
|
|
if distance < self.radius+gameobj.radius:
|
|
return True
|
|
else:
|
|
return False
|
|
def onscreen(self, rect):
|
|
if (self.x+self.radius >= rect[0] and
|
|
self.x-self.radius <= rect[2] and
|
|
self.y+self.radius >= rect[1] and
|
|
self.y-self.radius <= rect[3]):
|
|
return True
|
|
else:
|
|
return False
|
|
def draw(self, screen):
|
|
pygame.draw.circle(
|
|
screen,
|
|
self.color,
|
|
(self.x, self.y),
|
|
self.radius,
|
|
self.fill
|
|
)
|
|
|
|
class Player(GObj):
|
|
def __init__(self, x, y, radius=10, speed=100, turn_rate=5.0,
|
|
heading=0, sight_distance=0, color="darkorchid",
|
|
fill=0):
|
|
GObj.__init__(self, x, y, radius, speed, turn_rate,
|
|
heading, sight_distance, color,
|
|
fill)
|
|
def draw(self, screen):
|
|
GObj.draw(self, screen)
|
|
pygame.draw.line(
|
|
screen,
|
|
"black",
|
|
(self.x, self.y),
|
|
(
|
|
self.x+math.cos(self.heading)*self.radius,
|
|
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,
|
|
heading=0.0, sight_distance=0.0,
|
|
color="red", fill=3):
|
|
GObj.__init__(self, x, y, radius, speed, turn_rate,
|
|
heading, sight_distance, color,
|
|
fill)
|
|
self.touched = False
|
|
def touch(self):
|
|
self.color = "green"
|
|
self.touched = True
|
|
def is_touched(self):
|
|
return self.touched
|
|
|
|
class Enemy(GObj):
|
|
def __init__(self, x, y, radius, speed, turn_rate,
|
|
heading, sight_distance,
|
|
color, fill, goals):
|
|
GObj.__init__(self, x, y, radius, speed, turn_rate,
|
|
heading, sight_distance, color,
|
|
fill)
|
|
self.goals = goals
|
|
self.sight_cone_color_clear = 'white'
|
|
self.sight_cone_color_obj = 'fuchsia'
|
|
self.sight_cone_color = self.sight_cone_color_clear
|
|
|
|
x0 = self.x
|
|
y0 = self.y
|
|
x1 = self.x+math.cos(self.heading-math.pi/6)*(self.radius+self.sight_distance)
|
|
y1 = self.y+math.sin(self.heading-math.pi/6)*(self.radius+self.sight_distance)
|
|
x2 = self.x+math.cos(self.heading+math.pi/6)*(self.radius+self.sight_distance)
|
|
y2 = self.y+math.sin(self.heading+math.pi/6)*(self.radius+self.sight_distance)
|
|
|
|
self.sight_cone = [
|
|
(x0,y0),
|
|
(x1,y1),
|
|
(x2,y2)
|
|
]
|
|
|
|
self.ticks = random.randint(1,10) * 60
|
|
|
|
def draw(self, screen):
|
|
xy0 = self.sight_cone[0]
|
|
xy1 = self.sight_cone[1]
|
|
xy2 = self.sight_cone[2]
|
|
|
|
pygame.draw.line(
|
|
screen,
|
|
self.sight_cone_color,
|
|
(xy0[0], xy0[1]),
|
|
(xy1[0], xy1[1]),
|
|
1)
|
|
pygame.draw.line(
|
|
screen,
|
|
self.sight_cone_color,
|
|
(xy1[0],xy1[1]),
|
|
(xy2[0],xy2[1]),
|
|
1)
|
|
pygame.draw.line(
|
|
screen,
|
|
self.sight_cone_color,
|
|
(xy2[0],xy2[1]),
|
|
(xy0[0],xy0[1]),
|
|
1)
|
|
|
|
GObj.draw(self, screen)
|
|
|
|
def update(self, gameobj):
|
|
ox = gameobj.x
|
|
oy = gameobj.y
|
|
|
|
x0 = self.x
|
|
y0 = self.y
|
|
x1 = self.x+math.cos(self.heading-math.pi/6)*(self.radius+self.sight_distance)
|
|
y1 = self.y+math.sin(self.heading-math.pi/6)*(self.radius+self.sight_distance)
|
|
x2 = self.x+math.cos(self.heading+math.pi/6)*(self.radius+self.sight_distance)
|
|
y2 = self.y+math.sin(self.heading+math.pi/6)*(self.radius+self.sight_distance)
|
|
|
|
self.sight_cone[0] = (x0, y0)
|
|
self.sight_cone[1] = (x1, y1)
|
|
self.sight_cone[2] = (x2, y2)
|
|
|
|
|
|
for i in range(len(self.sight_cone)):
|
|
x1 = self.sight_cone[i][0]
|
|
y1 = self.sight_cone[i][1]
|
|
x2 = self.sight_cone[(i+1)%len(self.sight_cone)][0]
|
|
y2 = self.sight_cone[(i+1)%len(self.sight_cone)][1]
|
|
if (x2-x1)*(oy-y1) - (y2-y1)*(ox-x1) <= 0:
|
|
self.sight_cone_color = self.sight_cone_color_clear
|
|
return (False,None)
|
|
self.sight_cone_color = self.sight_cone_color_obj
|
|
dx = ox-self.x
|
|
dy = oy-self.y
|
|
dist = math.sqrt(dx*dx + dy*dy)
|
|
dx /= dist
|
|
dy /= dist
|
|
return (True, (dx, dy), dist)
|
|
|
|
# Base class AI routine
|
|
# 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)
|
|
|
|
# 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,
|
|
color="yellow", fill=0, goals=[]):
|
|
Enemy.__init__(self, x, y, radius, speed, turn_rate,
|
|
heading, sight_distance, color,
|
|
fill, goals)
|
|
# This is ai routine for the type A enemy.
|
|
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=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,
|
|
fill, goals)
|
|
|
|
self.ticks = 0
|
|
# This is the ai routing for the type B enemy.
|
|
def ai(self, percept, goals, comms):
|
|
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=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)
|
|
|
|
# This is the ai routing for the type B enemy.
|
|
def ai(self, percept, goals, comms):
|
|
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 |