Prevent hang on ill-formed torpedo command.
[super-star-trek.git] / sst.py
diff --git a/sst.py b/sst.py
index cdc50ee938c3911508452cbe2130a58985cd4f2c..12f1bad0900bffbfc751773126266268ac1a3351 100755 (executable)
--- a/sst.py
+++ b/sst.py
@@ -36,43 +36,57 @@ docpath     = (".", "doc/", "/usr/share/doc/sst/")
 def _(st):
     return gettext.gettext(st)
 
-# This is all encapsulated not just for logging but because someday
-# we'll probably want to replace it with something like an LCG that
-# can be forward-ported off Python
-
-import random
+# Rolling our own LCG because Python changed its incompatibly in 3.2.
+# Thus, we need to have our own to be 2/3 polyglot, which will also
+# be helpful when we forwrard-port.
 
 class randomizer:
+    # LCG PRNG parameters tested against
+    # Knuth vol. 2. by the authors of ADVENT 
+    LCG_A = 1093
+    LCG_C = 221587
+    LCG_M = 1048576
+
+    @staticmethod
+    def random():
+        old_x = game.lcg_x
+        game.lcg_x = (randomizer.LCG_A * game.lcg_x + randomizer.LCG_C) % randomizer.LCG_M
+        return old_x / randomizer.LCG_M;
+
     @staticmethod
     def withprob(p):
-        b = random.random() < p
-        if logfp:
-            logfp.write("#withprob(%.2f) -> %s\n" % (p, b))
-        return b
+        v = randomizer.random()
+        #if logfp:
+        #    logfp.write("#withprob(%.2f) -> %s\n" % (p, v < p))
+        return v < p
 
     @staticmethod
-    def randrange(*args):
-        s = random.randrange(*args)
-        if logfp:
-            logfp.write("#randrange%s -> %s\n" % (args, s))
-        return s
+    def integer(*args):
+        v = randomizer.random()
+        if len(args) == 1:
+            v = int(v * args[0])
+        else:
+            v = args[0] + int(v * (args[1] - args[0]))
+        #if logfp:
+        #    logfp.write("#integer%s -> %s\n" % (args, v))
+        return int(v)
 
     @staticmethod
     def real(*args):
-        v = random.random()
+        v = randomizer.random()
         if len(args) == 1:
             v *= args[0]                 # from [0, args[0])
         elif len(args) == 2:
             v = args[0] + v*(args[1]-args[0])        # from [args[0], args[1])
-        if logfp:
-            logfp.write("#real%s -> %f\n" % (args, v))
+        #if logfp:
+        #    logfp.write("#real%s -> %f\n" % (args, v))
         return v
 
     @staticmethod
     def seed(n):
-        if logfp:
-            logfp.write("#seed(%d)\n" % n)
-        random.seed(n)
+        #if logfp:
+        #    logfp.write("#seed(%d)\n" % n)
+        game.lcg_x = n % randomizer.LCG_M
 
 GALSIZE        = 8             # Galaxy size in quadrants
 NINHAB         = (GALSIZE * GALSIZE // 2)      # Number of inhabited worlds
@@ -118,8 +132,8 @@ class JumpOut(Exception):
 
 class Coord:
     def __init__(self, x=None, y=None):
-        self.i = x
-        self.j = y
+        self.i = x     # Row
+        self.j = y     # Column
     def valid_quadrant(self):
         return self.i >= 0 and self.i < GALSIZE and self.j >= 0 and self.j < GALSIZE
     def valid_sector(self):
@@ -162,13 +176,13 @@ class Coord:
         s = Coord()
         if self.i == 0:
             s.i = 0
-        elif s.i < 0:
-            s.i =-1
+        elif self.i < 0:
+            s.i = -1
         else:
             s.i = 1
         if self.j == 0:
             s.j = 0
-        elif s.j < 0:
+        elif self.j < 0:
             s.j = -1
         else:
             s.j = 1
@@ -338,7 +352,7 @@ FSCMOVE = 6        # Supercommander moves (might attack base)
 FSCDBAS = 7        # Supercommander destroys base
 FDSPROB = 8        # Move deep space probe
 FDISTR  = 9        # Emit distress call from an inhabited world
-FENSLV  = 10       # Inhabited word is enslaved */
+FENSLV  = 10       # Inhabited word is enslaved
 FREPRO  = 11       # Klingons build a ship in an enslaved system
 NEVENTS = 12
 
@@ -469,6 +483,7 @@ class Gamestate:
         self.iscloaked = False  # Cloaking device on?
         self.ncviol = 0         # Algreon treaty violations
         self.isviolreported = False # We have been warned
+        self.lcg_x = 0         # LCG generator value
     def remkl(self):
         return sum([q.klingons for (_i, _j, q) in list(self.state.traverse())])
     def recompute(self):
@@ -518,8 +533,8 @@ def welcoming(iq):
 def tryexit(enemy, look, irun):
     "A bad guy attempts to bug out."
     iq = Coord()
-    iq.i = game.quadrant.i+(look.i+(QUADSIZE-1))/QUADSIZE - 1
-    iq.j = game.quadrant.j+(look.j+(QUADSIZE-1))/QUADSIZE - 1
+    iq.i = game.quadrant.i+(look.i+(QUADSIZE-1))//QUADSIZE - 1
+    iq.j = game.quadrant.j+(look.j+(QUADSIZE-1))//QUADSIZE - 1
     if not welcoming(iq):
         return False
     if enemy.type == 'R':
@@ -604,7 +619,7 @@ def movebaddy(enemy):
     irun = False
     # This should probably be just (game.quadrant in game.state.kcmdr) + (game.state.kscmdr==game.quadrant)
     if game.skill >= SKILL_EXPERT:
-        nbaddys = (((game.quadrant in game.state.kcmdr)*2 + (game.state.kscmdr==game.quadrant)*2+game.klhere*1.23+game.irhere*1.5)/2.0)
+        nbaddys = int(((game.quadrant in game.state.kcmdr)*2 + (game.state.kscmdr==game.quadrant)*2+game.klhere*1.23+game.irhere*1.5)/2.0)
     else:
         nbaddys = (game.quadrant in game.state.kcmdr) + (game.state.kscmdr==game.quadrant)
     old_dist = enemy.kdist
@@ -935,6 +950,7 @@ def movetholian():
         game.tholian.move(None)
         prout("***Internal error: Tholian in a bad spot.")
         return
+    print("Tholian moving from %s to %s" % (game.tholian.location, tid))
     # do nothing if we are blocked
     if game.quad[tid.i][tid.j] not in ('.', '#'):
         return
@@ -1177,7 +1193,7 @@ def randdevice():
         10,        # DCLOAK: the cloaking device            1.0
     )
     assert(sum(weights) == 1000)
-    idx = rnd.randrange(1000)
+    idx = rnd.integer(1000)
     wsum = 0
     for (i, w) in enumerate(weights):
         wsum += w
@@ -1204,14 +1220,14 @@ def collision(rammed, enemy):
     skip(1)
     deadkl(enemy.location, enemy.type, game.sector)
     proutn("***" + crmshp() + " heavily damaged.")
-    icas = rnd.randrange(10, 30)
+    icas = rnd.integer(10, 30)
     prout(_("***Sickbay reports %d casualties") % icas)
     game.casual += icas
     game.state.crew -= icas
     # In the pre-SST2K version, all devices got equiprobably damaged,
     # which was silly.  Instead, pick up to half the devices at
     # random according to our weighting table,
-    ncrits = rnd.randrange(NDEVICES/2)
+    ncrits = rnd.integer(NDEVICES//2)
     while ncrits > 0:
         ncrits -= 1
         dev = randdevice()
@@ -1294,7 +1310,7 @@ def torpedo(origin, bearing, dispersion, number, nburst):
             for enemy in game.enemies:
                 if w == enemy.location:
                     kp = math.fabs(enemy.power)
-                    h1 = 700.0 + rnd.randrange(100) - \
+                    h1 = 700.0 + rnd.integer(100) - \
                         1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-track.angle))
                     h1 = math.fabs(h1)
                     if kp < h1:
@@ -1399,7 +1415,7 @@ def torpedo(origin, bearing, dispersion, number, nburst):
             prout(_("***Torpedo absorbed by Tholian web."))
             return None
         elif iquad == 'T':  # Hit a Tholian
-            h1 = 700.0 + rnd.randrange(100) - \
+            h1 = 700.0 + rnd.integer(100) - \
                 1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-track.angle))
             h1 = math.fabs(h1)
             if h1 >= 600:
@@ -1618,7 +1634,7 @@ def attack(torps_ok):
     prout(_("%d%%,   torpedoes left %d") % (percent, game.torps))
     # Check if anyone was hurt
     if hitmax >= 200 or hittot >= 500:
-        icas = rnd.randrange(int(hittot * 0.015))
+        icas = rnd.integer(int(hittot * 0.015))
         if icas >= 2:
             skip(1)
             prout(_("Mc Coy-  \"Sickbay to bridge.  We suffered %d casualties") % icas)
@@ -1722,7 +1738,11 @@ def torps():
             proutn(_("Number of torpedoes to fire- "))
             continue        # Go back around to get a number
         else: # key == "IHREAL"
-            n = scanner.int()
+            try:
+                n = scanner.int()
+            except TypeError:
+                huh()
+                return
             if n <= 0: # abort command
                 scanner.chew()
                 return
@@ -1825,7 +1845,7 @@ def checkshctrl(rpow):
     prouts(_("Sulu-  \"Captain! Shield malfunction! Phaser fire contained!\""))
     skip(2)
     prout(_("Lt. Uhura-  \"Sir, all decks reporting damage.\""))
-    icas = rnd.randrange(int(hit*0.012))
+    icas = rnd.integer(int(hit*0.012))
     skip(1)
     fry(0.8*hit)
     if icas:
@@ -2142,7 +2162,7 @@ def capture():
     game.ididit = False # Nothing if we fail
     game.optime = 0.0
 
-    # Make sure there is room in the brig */
+    # Make sure there is room in the brig
     if game.brigfree == 0:
         prout(_("Security reports the brig is already full."))
         return
@@ -2160,7 +2180,7 @@ def capture():
         prout(_("Uhura- \"Getting no response, sir.\""))
         return
 
-    # if there is more than one Klingon, find out which one */
+    # if there is more than one Klingon, find out which one
     #  Cruddy, just takes one at random.  Should ask the captain.
     #  Nah, just select the weakest one since it is most likely to
     #  surrender (Tom Almy mod)
@@ -2176,12 +2196,12 @@ def capture():
     x = game.energy / (weakest.power * len(klingons))
     #prout(_("Stats: energy = %s, kpower = %s, klingons = %s")
     #      % (game.energy, weakest.power, len(klingons)))
-    x *= 2.5  # would originally have been equivalent of 1.4,
-               # but we want command to work more often, more humanely */
+    x *= 2.5   # would originally have been equivalent of 1.4,
+               # but we want command to work more often, more humanely
     #prout(_("Prob = %.4f" % x))
     #  x = 100; // For testing, of course!
     if x < rnd.real(100):
-        # guess what, he surrendered!!! */
+        # guess what, he surrendered!!!
         prout(_("Klingon captain at %s surrenders.") % weakest.location)
         i = rnd.real(200)
         if i > 0:
@@ -2196,7 +2216,7 @@ def capture():
             finish(FWON)
         return
 
-       # big surprise, he refuses to surrender */
+       # big surprise, he refuses to surrender
     prout(_("Fat chance, captain!"))
 
 # Code from events.c begins here.
@@ -2437,7 +2457,7 @@ def events():
             if not game.state.kcmdr:
                 unschedule(FTBEAM)
                 continue
-            i = rnd.randrange(len(game.state.kcmdr))
+            i = rnd.integer(len(game.state.kcmdr))
             yank = (game.state.kcmdr[i]-game.quadrant).distance()
             if istract or game.condition == "docked" or game.iscloaked or yank == 0:
                 # Drats! Have to reschedule
@@ -2775,7 +2795,8 @@ def nova(nov):
                         finish(FNOVA)
                         return
                     # add in course nova contributes to kicking starship
-                    bump += (game.sector-hits[-1]).sgn()
+                    if hits:
+                        bump += (game.sector-hits[-1]).sgn()
                 elif iquad == 'K': # kill klingon
                     deadkl(neighbor, iquad, neighbor)
                 elif iquad in ('C','S','R'): # Damage/destroy big enemies
@@ -2839,7 +2860,7 @@ def supernova(w):
                 nstars += game.state.galaxy[nq.i][nq.j].stars
         if stars == 0:
             return # nothing to supernova exists
-        num = rnd.randrange(nstars) + 1
+        num = rnd.integer(nstars) + 1
         for nq.i in range(GALSIZE):
             for nq.j in range(GALSIZE):
                 num -= game.state.galaxy[nq.i][nq.j].stars
@@ -2860,7 +2881,7 @@ def supernova(w):
     else:
         ns = Coord()
         # we are in the quadrant!
-        num = rnd.randrange(game.state.galaxy[nq.i][nq.j].stars) + 1
+        num = rnd.integer(game.state.galaxy[nq.i][nq.j].stars) + 1
         for ns.i in range(QUADSIZE):
             for ns.j in range(QUADSIZE):
                 if game.quad[ns.i][ns.j]=='*':
@@ -4614,7 +4635,7 @@ def abandon():
         game.nprobes = 0 # No probes
         prout(_("You are captured by Klingons and released to"))
         prout(_("the Federation in a prisoner-of-war exchange."))
-        nb = rnd.randrange(len(game.state.baseq))
+        nb = rnd.integer(len(game.state.baseq))
         # Set up quadrant and position FQ adjacient to base
         if not game.quadrant == game.state.baseq[nb]:
             game.quadrant = game.state.baseq[nb]
@@ -5699,7 +5720,7 @@ def setup():
     game.quadrant = randplace(GALSIZE)
     game.sector = randplace(QUADSIZE)
     game.torps = game.intorps = 10
-    game.nprobes = rnd.randrange(2, 5)
+    game.nprobes = rnd.integer(2, 5)
     game.warpfac = 5.0
     for i in range(NDEVICES):
         game.damage[i] = 0.0
@@ -5732,7 +5753,7 @@ def setup():
         for j in range(GALSIZE):
             # Can't have more stars per quadrant than fit in one decimal digit,
             # if we do the chart representation will break.
-            k = rnd.randrange(1, min(10, QUADSIZE**2/10))
+            k = rnd.integer(1, min(10, QUADSIZE**2/10))
             game.instar += k
             game.state.galaxy[i][j].stars = k
     # Locate star bases in galaxy
@@ -5809,7 +5830,7 @@ def setup():
             new.name = systnames[i]
             new.inhabited = True
         else:
-            new.pclass = ("M", "N", "O")[rnd.randrange(0, 3)]
+            new.pclass = ("M", "N", "O")[rnd.integer(0, 3)]
             if rnd.withprob(0.33):
                 new.crystals = "present"
             new.known = "unknown"
@@ -5982,13 +6003,13 @@ def choose():
         prout("=== Debug mode enabled.")
     # Use parameters to generate initial values of things
     game.damfac = 0.5 * game.skill
-    game.inbase = rnd.randrange(BASEMIN, BASEMAX+1)
+    game.inbase = rnd.integer(BASEMIN, BASEMAX+1)
     game.inplan = 0
     if game.options & OPTION_PLANETS:
-        game.inplan += rnd.randrange(MAXUNINHAB/2, MAXUNINHAB+1)
+        game.inplan += rnd.integer(MAXUNINHAB/2, MAXUNINHAB+1)
     if game.options & OPTION_WORLDS:
         game.inplan += int(NINHAB)
-    game.state.nromrem = game.inrom = rnd.randrange(2 * game.skill)
+    game.state.nromrem = game.inrom = rnd.integer(2 * game.skill)
     game.state.nscrem = game.inscom = (game.skill > SKILL_FAIR)
     game.state.remtime = 7.0 * game.length
     game.intime = game.state.remtime
@@ -6111,7 +6132,7 @@ def newqad():
                 if game.quad[w.i][w.j] == '.':
                     break
             game.tholian = Enemy(etype='T', loc=w,
-                                 power=rnd.randrange(100, 500) + 25.0*game.skill)
+                                 power=rnd.integer(100, 500) + 25.0*game.skill)
             # Reserve unoccupied corners
             if game.quad[0][0]=='.':
                 game.quad[0][0] = 'X'
@@ -6155,9 +6176,9 @@ def setpassword():
                 break
     else:
         game.passwd = ""
-        game.passwd += chr(ord('a')+rnd.randrange(26))
-        game.passwd += chr(ord('a')+rnd.randrange(26))
-        game.passwd += chr(ord('a')+rnd.randrange(26))
+        game.passwd += chr(ord('a')+rnd.integer(26))
+        game.passwd += chr(ord('a')+rnd.integer(26))
+        game.passwd += chr(ord('a')+rnd.integer(26))
 
 # Code from sst.c begins here
 
@@ -6495,8 +6516,8 @@ def expran(avrage):
 def randplace(size):
     "Choose a random location."
     w = Coord()
-    w.i = rnd.randrange(size)
-    w.j = rnd.randrange(size)
+    w.i = rnd.integer(size)
+    w.j = rnd.integer(size)
     return w
 
 class sstscanner:
@@ -6702,7 +6723,6 @@ if __name__ == '__main__':
                     line = replayfp.readline().strip()
                     (leader, __, seed) = line.split()
                     seed = eval(seed)
-                    sys.stderr.write("sst2k: seed set to %s\n" % seed)
                     line = replayfp.readline().strip()
                     arguments += line.split()[2:]
                     replay = True