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()) 
by