from collections import namedtuple
import numpy as np
c = 299792458 # speed of light (m/s)
x0 = 10 # initial distance of missile (m)
v_M = 1 # velocity of missile (m/s)
a_M = 0.6 # maximum evasion acceleration of missile (m/s^2)
r_M = 0.005 # radius of missile (m)
f = 10 # frequency of firing (1/s)
v_a = c # speed of attack (m/s)
r_a0 = 0.01 # muzzle radius of attack (m), e.g. initial radius of laser beam at the moment it exits the laser
theta_a = 0.001 # angle of dispersion of attack cone (radians)
j_a = 0.0001 # jitter in firing angle (radians)
r_U = 0 # uncertainty in sensing the position of the missile (m)
def r_a(x):
return r_a0 + np.tan(theta_a) * x # radius of attack when it is at distance x
def d1(x):
return x / c # time for signal of missile's last known position to reach the ship (s)
d2 = 0.2 # processing delay to predict missile's next position (s)
d3 = 0.1 # physical aiming delay (s)
def d4(x):
# time for attack to reach missile (s) given the missile *when observed* was at position x
# by the time of launch, d1+d2+d3, the missile has gotten closer by v_M * (d1 + d2 + d3)
return (x - v_M * (d1(x) + d2 + d3)) / (v_a + v_M)
k_aim = 0 # see r_aim. Setting k_aim to 0 (always aim exactly where
# the enemy will be if they don't bother to accelerate) produces good
# results in this simulation because the central point is the most
# likely place for the missile to be with random dodging. However,
# note that against a real enemy, if you are too predictable about
# where you will shoot, he will be able to dodge better.
def r_aim(d):
return k_aim * a_M * d * d # radius of circle we will aim randomly into, given that the delay until the attack hits is d
dmg_0 = 100 # base damage at point blank range
dmg_armor = 1 # the missile's armor reduction
def dmg(x):
if r_a(x) <= r_M: # attack smaller than missile
return max(dmg_0 - dmg_armor, 0) # assumed full damage
else:
# base damage assumed to be proportional to the fraction of the attack that hits the missile
return max(dmg_0 * r_M**2 / r_a(x)**2 - dmg_armor, 0)
verbose = True
enabledMessages = ["advance","aim","hit","end"]
def mPrint(messageType, *args):
if verbose and messageType in enabledMessages:
print(*args)
Attack = namedtuple("Attack", "y z t") # aimed at (y, z), and it will arrive at time t
class State:
def __init__(self):
self.t = 0 # time
self.x = x0 # distance of missile from ship
self.y = 0 ; self.z = 0 # sideways position of missile off center line
self.v_y = 0; self.v_z = 0 #
self.a_y = 0; self.a_z = 0 #
self.hp = 100 # hit points
self.attacks = [] # list of Attacks currently in flight
def moveMissile(state, t1):
"Update the missile position and velocity over a time t1 since last update"
x = state.x
t2 = d1(x) + d2 + d3 + d4(x) # total observation-to-impact delay of an attack
print(t1, t2)
# change direction a random number of times, on average once per interval t2
numAccelChanges = np.random.poisson(t1/t2)
accelChanges = np.random.uniform(0, t1, numAccelChanges)
accelChanges.sort()
accelChanges = list(accelChanges)
prevPoint = 0
for criticalPoint in accelChanges + [t1]:
t = criticalPoint - prevPoint
state.y += state.v_y * t + 0.5 * state.a_y * t * t
state.v_y += state.a_y * t
state.z += state.v_z * t + 0.5 * state.a_z * t * t
state.v_z += state.a_z * t
if criticalPoint != t1:
newAngle = np.random.uniform(0, 2*np.pi)
state.a_y = np.sin(newAngle) * a_M
state.a_z = np.cos(newAngle) * a_M
prevPoint = criticalPoint
state.x -= v_M * t1
mPrint("advance", "Missile advanced to time ", state.t + t1, "x=", state.x, "y=", state.y, "z=", state.z, "v_y=", state.v_y, "v_z=", state.v_z)
def circlePoint(R):
"Find a random point (x, y) on a circle with radius R"
r = R * np.random.uniform(0,1)**0.5
theta = np.random.uniform(0, 2 * np.pi)
return r * np.cos(theta), r * np.sin(theta)
def aim(state):
"Based on a current observation of the missile, predict where the missile will be when our shot hits it, and shoot near there."
x = state.x
t2 = d1(x) + d2 + d3 + d4(x)
x_arrival = x - t2 * v_M
aim_y, aim_z = circlePoint(r_aim(t2))
uncertain_y, uncertain_z = circlePoint(r_U)
aim_y += state.y + uncertain_y + state.v_y * t2
aim_z += state.z + uncertain_z + state.v_z * t2
jitter_y, jitter_z = circlePoint(1)
jitter_y *= np.tan(j_a) * x_arrival
jitter_z *= np.tan(j_a) * x_arrival
aim_y += jitter_y; aim_z += jitter_z
state.attacks.append(Attack(aim_y, aim_z, state.t + t2))
mPrint("aim", "Aiming attack at (", aim_y, "," , aim_z, ") to hit at time ", state.t + t2)
def resolveAttack(state, attack):
"Given that missile time and position has already been advanced to the point of possible impact with an attack, check for impact from this attack and resolve damage."
dist = ((attack.y - state.y)**2 + (attack.z - state.z)**2)**0.5
if dist < r_M + r_a(state.x): #hit!
mPrint("hit", "Missile hit at distance", state.x, "for", dmg(state.x), "damage")
state.hp -= dmg(state.x)
else:
mPrint("hit", "Missile missed at distance ", state.x)
def episode(state):
nextShotTime = 0
nextResolutionTime = np.inf
while True:
# find the next event. Is it time to shoot, or time to resolve an attack?
if nextShotTime < nextResolutionTime:
moveMissile(state, nextShotTime - state.t)
state.t = nextShotTime
if state.x <= 0:
mPrint("end", "Missile won with", state.hp, "hps")
return 0
aim(state)
nextResolutionTime = min([a.t for a in state.attacks])
nextShotTime += 1 / f
else:
moveMissile(state, nextResolutionTime - state.t)
state.t = nextResolutionTime
if state.x <= 0:
mPrint("end", "Missile won with", state.hp, "hps")
return 0
for attack in state.attacks:
if attack.t == nextResolutionTime:
resolveAttack(state, attack)
if state.hp <= 0:
mPrint("end", "Missile destroyed!")
return state.x
state.attacks = [a for a in state.attacks if a.t != nextResolutionTime]
nextResolutionTime = min([a.t for a in state.attacks])
def test(n = 100):
"return the average distance at which the missile dies over many trials"
global verbose
v = verbose
verbose = False
tot = 0
for i in range(n):
x = episode(State())
tot += x
verbose = v
return tot / n
episode(State())