Trial Python now incorporates ai.c
[super-star-trek.git] / src / sst.py
index 4d98a47953ac9bae432e05f13c233a093acfb002..4f7b6e4d315796e8be7887dbe0a216684caaf962 100644 (file)
@@ -15,21 +15,8 @@ MAXKLGAME    = 127
 MAXKLQUAD      = 9
 FOREVER        = 1e30
 
-# Feature vakues
-IHR = 'R',
-IHK = 'K',
-IHC = 'C',
-IHS = 'S',
-IHSTAR = '*',
-IHP = 'P',
-IHW = '@',
-IHB = 'B',
-IHBLANK = ' ',
-IHDOT = '.',
+# These types have not been dealt with yet 
 IHQUEST = '?',
-IHE = 'E',
-IHF = 'F',
-IHT = 'T',
 IHWEB = '#',
 IHMATER0 = '-',
 IHMATER1 = 'o',
@@ -45,15 +32,97 @@ class coord:
         return self.x != None and self.y != None
     def __eq__(self, other):
         return self.x == other.y and self.x == other.y
+    def __add__(self, other):
+        return coord(self.x+self.x, self.y+self.y)
+    def __sub__(self, other):
+        return coord(self.x-self.x, self.y-self.y)
     def distance(self, other):
         return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
+    def sgn(self):
+        return coord(self.x / abs(x), self.y / abs(y));
+    def __hash__(self):
+        return hash((x, y))
+    def __str__(self):
+        return "%d - %d" % (self.x, self.y)
 
-class planet:
+class feature:
+    "A feature in the current quadrant (ship, star, black hole, etc)." 
+    def __init__(self):
+        self.type = None       # name of feature type
+        self.location = None   # location
+    def distance(self):
+        return self.location.distance(game.sector)
+    def __str__(self):
+        return self.name[0]
+
+empty = None   # Value of empty space in game.quad
+
+class ship(feature):
+    "An enemy ship in the current quadrant." 
+    def __init__(self):
+        feature.__init__(self)
+        self.type = None       # klingon, romulan, commander,
+                               # supercommander, tholian
+        self.power = None      # power
+        if self.type in ("Klingon", "Commander", "Super-Commander"):
+            game.remkl += 1
+        elif self.type == "Romulan":
+            game.romrem += 1
+    def __del__(self):
+        if self.type in ("Klingon", "Commander", "Super-Commander"):
+            game.remkl -= 1
+        elif self.type == "Romulan":
+            game.romrem -= 1
+    def sectormove(self, dest):
+        "Move this ship within the current quadrant." 
+        if self.location:
+            game.quad[self.location] = None
+        game.quad[dest] = self
+        self.location = dest
+
+class planet(feature):
+    "A planet.  May be inhabited or not, may hold dilithium crystals or not."
     def __init(self):
-        self.w = coord()
+        feature.__init__(self)
         self.name = None
         self.crystals = None   # "absent", "present", or "mined"
+        self.inhabited = False
         self.known = "unknown" # Other values: "known" and "shuttle down"
+    def __str__(self):
+        if self.inhabited:
+            return '@'
+        else:
+            return 'P'
+
+class star(feature):
+    "A star.  Has no state, just knows how to identify iself."
+    def __init(self):
+        feature.__init__(self)
+    def __str__(self):
+        return '*'
+
+class web(feature):
+    "A bit of Tholian web.  Has no state, just knows how to identify iself."
+    def __init(self):
+        feature.__init__(self)
+    def __str__(self):
+        return '*'
+
+class blackhole(feature):
+    "A black hole.  Has no hair, just knows how to identify iself."
+    def __init(self):
+        feature.__init__(self)
+    def __str__(self):
+        return '*'
+
+class starbase(feature):
+    "Starbases also have no features."
+    def __init(self):
+        feature.__init__(self)
+    def __del__(self):
+        game.state.bases.remove(self.location)
+    def __str__(self):
+        return 'B'
 
 class quadrant:
     def __init__(self):
@@ -65,6 +134,15 @@ class quadrant:
         self.supernova = None
         self.charted = None
         self.status = "secure" # Other valuues: "distressed", "enslaved"
+    def enemies(self):
+        "List enemies in this quadrant."
+        lst = []
+        for feature in self.quad.values:
+            if not isinstance(feature, ship):
+                continue
+            if feature.name not in ("Enterprise", "Faerie Queene"):
+                lst.append(feature)
+        return lst
 
 class page:
     "A chart page.  The starchart is a 2D array of these."
@@ -116,7 +194,7 @@ class game:
         self.future = []               # future events
         self.passwd = None             # Self Destruct password
         # Coordinate members start here
-       self.ks = {}                    # enemy sector locations
+       self.enemies = {}                       # enemy sector locations
        self.quadrant = None            # where we are
         self.sector = None
        self.tholian = None             # coordinates of Tholian
@@ -170,7 +248,6 @@ class game:
        self.nkinks = None              # count of energy-barrier crossings
        self.iplnet = None              # planet # in quadrant
        self.inplan = None              # initial planets
-       self.nenhere = None             # number of enemies in quadrant
        self.irhere = None              # Romulans in quadrant
        self.isatb = None               # =1 if super commander is attacking base
        self.tourn = None               # tournament number
@@ -201,4 +278,463 @@ class game:
        self.probeiny = None            #
        self.height = None              # height of orbit around planet
 
+def communicating():
+    "Are we in communication with Starfleet Command?"
+    return (not damaged("DRADIO")) or game.condition == docked
+
+# Code corresponding to ai.c begins here
+
+def tryexit(look, ship, irun):
+    # a bad guy attempts to bug out of the quadrant
+    iq = coord()
+    iq.x = game.quadrant.x+(look.x+(QUADSIZE-1))/QUADSIZE - 1
+    iq.y = game.quadrant.y+(look.y+(QUADSIZE-1))/QUADSIZE - 1
+    if not valid_quadrant(iq) or \
+       game.state.galaxy[iq].supernova or \
+        game.state.galaxy[iq].klingons > 8:
+       return False;   # no can do -- neg energy, supernovae, or >8 Klingons
+    if ship.type == "Romulan":
+        return False   # Romulans cannot escape
+    if not irun:
+       # avoid intruding on another commander's territory
+        if ship.type == "Commander":
+            if iq in gamestate.kcmdr:
+                return False
+           # refuse to leave if currently attacking starbase:
+            if game.battle == game.quadrant:
+                return False;
+       # don't leave if over 1000 units of energy
+        if ship.power > 1000.0:
+            return false;
+    # Print escape message and move out of quadrant.
+    # We know this if either short or long range sensors are working
+    if not damaged("DSRSENS") or not damaged("DLRSENS") or game.condition=="docked":
+       crmena(True, "sector", ship)
+       prout(" escapes to quadrant %s (and regains strength)." % iq)
+    # handle local matters related to escape
+    game.quad[ship.location] = None;
+    if game.condition != "docked":
+        newcnd()
+    # Handle global matters related to escape
+    game.state.galaxy[game.quadrant].klingons -= 1
+    game.state.galaxy[iq].klingons += 1
+    if ship.type == "Super-Commander":
+       game.ishere = False
+       game.iscate = False
+       game.ientesc = False
+       game.isatb = 0
+       schedule("FSCMOVE", 0.2777)
+       unschedule("FSCDBAS")
+       game.state.kscmdr = iq
+    else:
+        for (n, cmdr) in enumerate(game.state.kcmdr):
+            if cmdr == game.quadrant:
+               game.state.kcmdr[n] = iq
+               break
+       game.comhere = False
+    return True                # successful exit
+
+def sgn(n): n / abs(n)
+
+'''
+Algorithm for moving bad guys:
+
+ * Enterprise has "force" based on condition of phaser and photon torpedoes.
+ If both are operating full strength, force is 1000. If both are damaged,
+ force is -1000. Having shields down subtracts an additional 1000.
+
+ * Enemy has forces equal to the energy of the attacker plus
+ 100*(K+R) + 500*(C+S) - 400 for novice through good levels OR
+ 346*K + 400*R + 500*(C+S) - 400 for expert and emeritus.
+
+ Attacker Initial energy levels (nominal):
+ Klingon   Romulan   Commander   Super-Commander
+ Novice    400        700        1200        
+ Fair      425        750        1250
+ Good      450        800        1300        1750
+ Expert    475        850        1350        1875
+ Emeritus  500        900        1400        2000
+ VARIANCE   75        200         200         200
+
+ Enemy vessels only move prior to their attack. In Novice - Good games
+ only commanders move. In Expert games, all enemy vessels move if there
+ is a commander present. In Emeritus games all enemy vessels move.
+
+ *  If Enterprise is not docked, an agressive action is taken if enemy
+ forces are 1000 greater than Enterprise.
+
+ Agressive action on average cuts the distance between the ship and
+ the enemy to 1/4 the original.
+
+ *  At lower energy advantage, movement units are proportional to the
+ advantage with a 650 advantage being to hold ground, 800 to move forward
+ 1, 950 for two, 150 for back 4, etc. Variance of 100.
+
+ If docked, is reduced by roughly 1.75*game.skill, generally forcing a
+ retreat, especially at high skill levels.
+
+ *  Motion is limited to skill level, except for SC hi-tailing it out.
+'''
+
+def movebaddy(ship):
+    # tactical movement for the bad guys
+    bugout = False
+    # This should probably be just game.comhere + game.ishere
+    if game.skill >= SKILL_EXPERT:
+        nbaddys = int((game.comhere*2 + game.ishere*2+game.klhere*1.23+game.irhere*1.5)/2.0)
+    else:
+       nbaddys = game.comhere + game.ishere
+    dist1 = ship.distance()
+    mdist = round(dist1 + 0.5)         # Nearest integer distance
+    # If SC, check with spy to see if should high-tail it
+    if ship.type == "Super-Commander" and \
+           (ship.power <= 500.0 or (game.condition==docked and not damaged("DPHOTON"))):
+       bugout = True;
+       motion = -QUADSIZE;
+    else:
+       # decide whether to advance, retreat, or hold position
+       forces = ship.power + 100.0*len(game.quad.enemies()) + 400*(nbaddys-1)
+        if not game.shldup:
+            forces += 1000.0           # Good for enemy if shield is down!
+        if not damaged("DPHASER") or not damaged("DPHOTON"):
+            if damaged(DPHASER):
+               forces += 300.0
+           else:
+               forces -= 0.2*(game.energy - 2500.0);
+           if damaged("DPHOTON"):
+               forces += 300.0
+           else:
+               forces -= 50.0*game.torps
+       else:
+           # phasers and photon tubes both out!
+           forces += 1000.0
+       motion = 0;
+        if forces <= 1000.0 and game.condition != "docked":    # Typical case
+           motion = ((forces+200.0*Rand())/150.0) - 5.0
+       else:
+            if forces > 1000.0:        # Very strong -- move in for kill
+               motion = (1.0-square(Rand()))*dist1 + 1.0
+           if game.condition == "docked" and "base" in game.options:
+                 # protected by base -- back off !
+               motion -= game.skill * (2.0-Rand()**2)
+       if idebug:
+           proutn("=== MOTION = %1.2f, FORCES = %1.2f, ", motion, forces)
+       # don't move if no motion
+        if motion == 0:
+            return
+       # Limit motion according to skill
+        if abs(motion) > game.skill:
+            if motion < 0:
+                motion = -game.kill
+            else:
+                motion = game.skill
+    # calculate preferred number of steps
+    nsteps = abs(motion)
+    if motion > 0 and nsteps > mdist: # don't overshoot
+        nsteps = mdist
+    if nsteps > QUADSIZE: # This shouldn't be necessary
+        nsteps = QUADSIZE
+    if nsteps < 1:  # This shouldn't be necessary
+        nsteps = 1
+    if idebug:
+       proutn("NSTEPS = %d:", nsteps)
+    # Compute preferred values of delta X and Y
+    me = game.sector - com;
+    if 2.0 * abs(me.x) < abs(me.y):
+        me.x = 0;
+    if 2.0 * abs(me.y) < abs(game.sector.x-com.x):
+        me.y = 0;
+    if me.x != 0: me.x = sgn(me.x*motion)
+    if me.y != 0: me.y = sgn(me.y*motion)
+    next = com;
+    # main move loop
+    for ll in range(nsteps):
+        if idebug:
+           proutn(" %d", ll+1)
+       # Check if preferred position available
+       look = next + me
+       krawl = me.sgn()
+       success = False
+       attempts = 0 # Settle meysterious hang problem
+        while attempts < 20 and not success:
+            attempts += 1
+            if look.x < 1 or look.x > QUADSIZE:
+                if motion < 0 and tryexit(look, ship, bugout):
+                   return
+                if krawl.x == me.x or me.y == 0:
+                    break
+               look.x = next.x + krawl.x
+               krawl.x = -krawl.x
+           elif look.y < 1 or look.y > QUADSIZE:
+                if motion < 0 and tryexit(look, ship, bugout):
+                    return
+                if krawl.y == me.y or me.x == 0:
+                    break
+                look.y = next.y + krawl.y
+                krawl.y = -krawl.y
+           elif "ramming" in game.options and game.quad[look] != IHDOT:
+               # See if we should ram ship
+                if game.quad[look] == game.ship and ienm in (IHC, IHS):
+                   ram(true, ienm, com)
+                   return
+                if krawl.x != me.x and me.y != 0:
+                   look.x = next.x + krawlx
+                   krawl.x = -krawl.x
+               elif krawly != me.y and me.x != 0:
+                   look.y = next.y + krawly
+                   krawl.y = -krawl.y
+               else:
+                    break # we have failed
+           else:
+                success = True
+        if success:
+           next = look
+            if idebug:
+               proutn(str(next))
+       else:
+            break # done early
+    if idebug:
+       prout("")
+    # Put ship in place within same quadrant
+    if next != ship.location:
+       # it moved
+        if not damaged("DSRSENS") or game.condition == "docked":
+           proutn("*** %s from sector %s" % (ship, ship.location))
+            if ship.distance() < dist1:
+                prout(" advances to sector %s" % ship.location)
+            else:
+                proutn(" retreats to sector %s" % ship.location)
+        ship.sectormove(next)
+
+def movcom(): 
+    "Allow enemies to move."
+    for enemy in self.quad.enemies():
+        if enemy.type == "Commander":
+            movebaddy(enemy)
+            break
+    for enemy in self.quad.enemies():
+        if enemy.type == "Super-Commander":
+            movebaddy(enemy)
+            break
+    # Ff skill level is high, move other Klingons and Romulans too!
+    # Move these last so they can base their actions on what the
+    # commander(s) do.
+    if game.skill >= SKILL_EXPERT and "movebaddy" in game.options: 
+        for enemy in self.quad.enemies():
+            if enemy.type in ("Klingon", "Romulan"):
+                movebaddy(enemy)
+                break
+
+def movescom(ship, avoid):
+    # commander movement helper
+    global ipage
+    if game.state.kscmdr == game.quadrant or \
+       game.state.galaxy[iq].supernova or \
+        game.state.galaxy[iq].klingons > 8: 
+       return True
+    if avoid:
+       # Avoid quadrants with bases if we want to avoid Enterprise
+        for base in game.state.starbases:
+            if base.location == ship.location:
+               return True
+    if game.justin and not game.iscate:
+        return True
+    # Super-Commander has scooted, Remove him from current quadrant.
+    if game.state.kscmdr == game.quadrant:
+       game.iscate = False
+       game.isatb = 0
+       game.ientesc = False
+       unschedule("FSCDBAS")
+        if game.condition != "docked":
+            newcnd()
+        ship.sectormove(None)
+    # do the actual move
+    game.state.galaxy[game.state.kscmdr].klingons -= 1
+    game.state.kscmdr = iq
+    game.state.galaxy[game.state.kscmdr].klingons += 1
+    # check for a helpful planet in the destination quadrant
+    for planet in game.state.plnets:
+       if planet.location == game.state.kscmdr and planet.crystals=="present":
+           # destroy the planet
+           game.state.plnets.remove(planet)
+            if communicating():
+                if not ipage:
+                    pause_game(True)
+               ipage = true
+               prout("Lt. Uhura-  \"Captain, Starfleet Intelligence reports")
+               proutn(_("   a planet in "))
+               proutn(cramlc(quadrant, game.state.kscmdr))
+               prout(" has been destroyed")
+               prout("   by the Super-commander.\"")
+           break
+    return False # looks good!
+                       
+def scom():
+    # move the Super Commander
+    if (idebug):
+        prout("== SCOM")
+
+    # Decide on being active or passive
+    passive = ((NKILLC+NKILLK)/(game.state.date+0.01-game.indate) < 0.1*game.skill*(game.skill+1.0) \
+               or (game.state.date-game.indate) < 3.0)
+    if not game.iscate and passive:
+       # compute move away from Enterprise
+       idelta = game.state.kscmdr - game.quadrant
+        if distance(game.state.kscmdr) > 2.0:
+           # circulate in space
+           idelta,x = game.state.kscmdr.y-game.quadrant.y
+           idelta,y = game.quadrant.x-game.state.kscmdr.x
+    else:
+        if len(game.state.bases):
+            unschedule("FSCMOVE")
+           return
+       sc = game.state.kscmdr
+       # compute distances to starbases
+        game.starbases.sort(lambda x, y: cmp(distance(x, game.quadrant), distance(y, game.quadrant)))
+       # look for nearest base without a commander, no Enterprise, and
+       # without too many Klingons, and not already under attack.
+        nearest = filter(game.starbases,
+                         lambda x: game.state.galaxy[x].supernova \
+                         and game.state.galaxy[x].klingons <= 8)
+        if game.quadrant in nearest:
+            nearest.remove(game.quadrant)
+        if game.battle in nearest:
+            nearest.remove(game.battle)
+        # if there is a commander, and no other base is appropriate,
+        # we will take the one with the commander
+        nocmd = filter(lambda x: x.location not in game.state.kcmdr, nearest)
+        if len(nocmd):
+            nearest = nocmd
+        ibq = nearest[0]
+        if len(nearest) == 0:
+            return     # Nothing suitable -- wait until next time
+       # decide how to move toward base
+       idelta = ibq - game.state.kscmdr
+    # maximum movement is 1 quadrant in either or both axis
+    delta = delta.sgn()
+    # try moving in both x and y directions
+    iq = game.state.kscmdr + idelta
+    if movescom(iq, passive):
+       # failed -- try some other maneuvers
+        if ideltax==0 or ideltay==0:
+           # attempt angle move
+            if ideltax != 0:
+               iq.y = game.state.kscmdr.y + 1
+                if movescom(iq, passive):
+                   iq.y = game.state.kscmdr.y - 1
+                   movescom(iq, passive)
+           else:
+               iq.x = game.state.kscmdr.x + 1
+                if movescom(iq, passive):
+                   iq.x = game.state.kscmdr.x - 1
+                   movescom(iq, passive)
+       else:
+           # try moving just in x or y
+           iq.y = game.state.kscmdr.y
+            if movescom(iq, passive):
+               iq.y = game.state.kscmdr.y + ideltay
+               iq.x = game.state.kscmdr.x
+               movescom(iq, passive)
+    # check for a base
+    if game.state.rembase == 0:
+       unschedule("FSCMOVE")
+    else:
+        for ibq in game.bases:
+            if ibq == game.state.kscmdr and game.state.kscmdr == game.battle:
+                # attack the base
+                if passive:
+                    return # no, don't attack base!
+                game.iseenit = false
+                game.isatb = 1
+                schedule("FSCDBAS", 1.0 +2.0*Rand())
+                if is_scheduled("FCDBAS"):
+                    postpone("FSCDBAS", scheduled("FCDBAS")-game.state.date)
+                if not communicating():
+                    return # no warning
+                game.iseenit = True
+                if not ipage:
+                    pause_game(true)
+                ipage = True
+                proutn(_("Lt. Uhura-  \"Captain, the starbase in "))
+                proutn(cramlc(quadrant, game.state.kscmdr))
+                skip(1)
+                prout("   reports that it is under attack from the Klingon Super-commander.")
+                proutn("   It can survive until stardate %d.\"",
+                       int(scheduled(FSCDBAS)))
+                if not game.resting:
+                    return
+                prout("Mr. Spock-  \"Captain, shall we cancel the rest period?\"")
+                if ja() == false:
+                    return
+                game.resting = False
+                game.optime = 0.0 # actually finished
+                return
+    # Check for intelligence report
+    if (Rand() > 0.2 or not communicating() or
+        not game.state.galaxy[game.state.kscmdr].charted):
+       return
+    if ipage:
+        pause_game(true)
+        ipage = true
+    prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
+    proutn(_("   the Super-commander is in "))
+    proutn(cramlc(quadrant, game.state.kscmdr))
+    prout(".\"")
+    return
+
+def movetho(void):
+    "Move the Tholian (an instance of ship type pointed at by game.tholian)." 
+    if not game.tholian or game.justin:
+        return
+    next = coord()
+    if game.tholian.location.x == 1 and game.tholian.location.y == 1:
+       next.x = 1
+        next.y = QUADSIZE
+    elif game.tholian.location.x == 1 and game.tholian.location.y == QUADSIZE:
+       next.x = next.y = QUADSIZE
+    elif game.tholian.location.x == QUADSIZE and game.tholian.location.y == QUADSIZE:
+       next.x = QUADSIZE
+        next.y = 1
+    elif game.tholian.location.x == QUADSIZE and game.tholian.location.y == 1:
+       next.x = next.y = 1
+    else:
+       # something is wrong!
+       game.tholian = None
+       return
+    # Do nothing if we are blocked
+    if game.quad[next] != empty and not isinstance(game.quad[next]. web):
+        return
+    # Now place some web
+    im = (next - game.tholian.location).sgn()
+    if game.tholian.x != next.x:
+       # move in x axis
+       while game.tholian.location.x != next.x:
+           game.tholian.location.x += im.x
+            if game.quad[game.tholian.location] == empty:
+                game.quad[game.tholian.location] = web()
+    elif game.tholian.y != next.y:
+       # move in y axis
+       while game.tholian.y != next.y:
+           game.tholian.y += im.y
+            if game.quad[game.tholian.location] == empty:
+                game.quad[game.tholian.location] = web()
+    # web is done, move ship
+    game.tholian.movesector(next)
+    # check to see if all holes plugged
+    for i in range(1, QUADSIZE+1):
+       if (not isinstance(game.quad[(1,i)],web)) and game.quad[(1,i)]!=game.tholian:
+            return
+        if (not isinstance(game.quad[(QUADSIZE,i)],web)) and game.quad[(QUADSIZE,i)]!=game.tholian:
+            return
+        if (not isinstance(game.quad[(i,1)],web)) and game.quad[(i,1)]!=game.tholian:
+            return
+        if (not isinstance(game.quad[(i.QUADSIZE)],web)) and game.quad[(i,QUADSIZE)]!=game.tholian:
+            return
+    # All plugged up -- Tholian splits
+    game.quad[game.tholian] = web()
+    ship.movesector(None)
+    crmena(True, IHT, sector, game.tholian)
+    prout(" completes web.")
+    game.tholian = None
+    return