Fix code so collisions can actually happen.
[super-star-trek.git] / sst
diff --git a/sst b/sst
index a95c9b7372d8da1f1cb7c700c73122567c7bd21f..b9d9a6acf9c3467a64676c7bcca2962f8450dbc0 100755 (executable)
--- a/sst
+++ b/sst
@@ -11,8 +11,7 @@ Stas Sergeev, and Eric S. Raymond.
 See the doc/HACKING file in the distribution for designers notes and advice
 on how to modify (and how not to modify!) this code.
 """
-from __future__ import print_function, division
-# Runs under Python 2 an Python 3. Preserve this property!
+# Copyright by Eric S. Raymond
 # SPDX-License-Identifier: BSD-2-clause
 
 # pylint: disable=line-too-long,superfluous-parens,too-many-lines,invalid-name,missing-function-docstring,missing-class-docstring,multiple-statements,too-many-branches,too-many-statements,too-many-locals,too-many-nested-blocks,too-many-return-statements,too-many-instance-attributes,global-statement,no-else-break,no-else-return,no-else-continue,too-few-public-methods,too-many-boolean-expressions,consider-using-f-string,consider-using-enumerate,consider-using-with,unspecified-encoding
@@ -27,15 +26,9 @@ import codecs
 try:
     # pylint: disable=unused-import
     import readline
-except ImportError:
+except ImportError:  # pragma: no cover
     pass
 
-# Prevent lossage under Python 3
-try:
-    my_input = raw_input
-except NameError:
-    my_input = input
-
 version = "2.7"
 
 docpath        = (".", "doc/", "/usr/share/doc/sst/")
@@ -44,8 +37,8 @@ def _(st):
     return gettext.gettext(st)
 
 # 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.
+# Thus, we needed to have our own to be 2/3 polyglot; it will be
+# helpful when and if we ever forward-port to another language.
 
 class randomizer:
     # LCG PRNG parameters tested against
@@ -108,7 +101,7 @@ FOREVER     = 1e30          # Time for the indefinite future
 MAXBURST       = 3             # Max # of torps you can launch in one turn
 MINCMDR        = 10            # Minimum number of Klingon commanders
 DOCKFAC                = 0.25          # Repair faster when docked
-PHASEFAC       = 2.0           # Unclear what this is, it was in the C version
+PHASEFAC       = 2.0           # Phaser attenuation factor
 
 ALGERON                = 2311          # Date of the Treaty of Algeron
 
@@ -137,6 +130,9 @@ class TrekError(Exception):
 class JumpOut(Exception):
     pass
 
+def letterize(n):
+    return chr(ord('a') + n - 1)
+
 class Coord:
     def __init__(self, x=None, y=None):
         self.i = x     # Row
@@ -159,17 +155,17 @@ class Coord:
         return Coord(self.i*other, self.j*other)
     def __rmul__(self, other):
         return Coord(self.i*other, self.j*other)
-    def __div__(self, other):
+    def __div__(self, other):  # pragma: no cover
         return Coord(self.i/other, self.j/other)
-    def __truediv__(self, other):
+    def __truediv__(self, other):  # pragma: no cover
         return Coord(self.i/other, self.j/other)
-    def __floordiv__(self, other):
+    def __floordiv__(self, other):  # pragma: no cover
         return Coord(self.i//other, self.j//other)
     def __mod__(self, other):
         return Coord(self.i % other, self.j % other)
-    def __rtruediv__(self, other):
+    def __rtruediv__(self, other):  # pragma: no cover
         return Coord(self.i/other, self.j/other)
-    def __rfloordiv__(self, other):
+    def __rfloordiv__(self, other):  # pragma: no cover
         return Coord(self.i//other, self.j//other)
     def roundtogrid(self):
         return Coord(int(round(self.i)), int(round(self.j)))
@@ -206,7 +202,9 @@ class Coord:
         return s
     def __str__(self):
         if self.i is None or self.j is None:
-            return "Nowhere"
+            return "Nowhere"  # pragma: no cover
+        if (game.options & OPTION_ALPHAMERIC):
+            return letterize(self.i + 1) + str(self.j + 1)
         return "%s - %s" % (self.i+1, self.j+1)
     __repr__ = __str__
 
@@ -242,7 +240,7 @@ class Quadrant:
         self.charted = False
         self.status = "secure"        # Could be "secure", "distressed", "enslaved"
     def __str__(self):
-        return "<Quadrant: %(klingons)d>" % self.__dict__
+        return "<Quadrant: %(klingons)d>" % self.__dict__  # pragma: no cover
     __repr__ = __str__
 
 class Page:
@@ -251,7 +249,7 @@ class Page:
         self.starbase = False
         self.klingons = None
     def __repr__(self):
-        return "<%s,%s,%s>" % (self.klingons, self.starbase, self.stars)
+        return "<%s,%s,%s>" % (self.klingons, self.starbase, self.stars)  # pragma: no cover
 
 def fill2d(size, fillfun):
     "Fill an empty list in 2D."
@@ -315,6 +313,7 @@ OPTION_PLAIN      = 0x01000000        # user chose plain game
 OPTION_ALMY       = 0x02000000        # user chose Almy variant
 OPTION_COLOR      = 0x04000000        # enable color display (ESR, 2010)
 OPTION_DOTFILL    = 0x08000000        # fix dotfill glitch in chart (ESR, 2019)
+OPTION_ALPHAMERIC = 0x10000000        # Alpha Y coordinates (ESR, 2023)
 
 option_names = {
     "ALL": OPTION_ALL,
@@ -389,7 +388,7 @@ NEVENTS = 12
 
 # Abstract out the event handling -- underlying data structures will change
 # when we implement stateful events
-def findevent(evtype):
+def findevent(evtype):  # pragma: no cover
     return game.future[evtype]
 
 class Enemy:
@@ -422,7 +421,7 @@ class Enemy:
                 game.enemies.remove(self)
         return motion
     def __repr__(self):
-        return "<%s,%s.%f>" % (self.type, self.location, self.power)        # For debugging
+        return "<%s,%s,%f>" % (self.type, self.location, self.power)  # pragma: no cover
 
 class Gamestate:
     def __init__(self):
@@ -774,7 +773,7 @@ def movebaddy(enemy):
 def moveklings():
     "Sequence Klingon tactical movement."
     if game.idebug:
-        prout("== MOVCOM")
+        prout("== MOVCOM")        # pragma: no cover
     # Figure out which Klingon is the commander (or Supercommander)
     # and do move
     tacmoves = []
@@ -974,7 +973,7 @@ def movetholian():
     elif game.tholian.location.i == QUADSIZE-1 and game.tholian.location.j == 0:
         tid.i = 0
         tid.j = 0
-    else:
+    else:  # pragma: no cover
         # something is wrong!
         game.tholian.move(None)
         prout("***Internal error: Tholian in a bad spot.")
@@ -1227,10 +1226,10 @@ def randdevice():
         wsum += w
         if idx < wsum:
             return i
-    return None        # we should never get here
+    return None                # pragma: no cover
 
 def collision(rammed, enemy):
-    "Collision handling for rammong events."
+    "Collision handling for ramming events."
     prouts(_("***RED ALERT!  RED ALERT!"))
     skip(1)
     prout(_("***COLLISION IMMINENT."))
@@ -1247,12 +1246,12 @@ def collision(rammed, enemy):
         proutn(_(" (original position)"))
     skip(1)
     deadkl(enemy.location, enemy.type, game.sector)
-    proutn("***" + crmshp() + " heavily damaged.")
+    prout("***" + crmshp() + " heavily damaged.")
     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,
+    # In the pre-SST2K versions, all devices got equiprobably damaged,
     # which was silly.  Instead, pick up to half the devices at
     # random according to our weighting table,
     ncrits = rnd.integer(NDEVICES//2)
@@ -1371,7 +1370,7 @@ def torpedo(origin, bearing, dispersion, number, nburst):
                             tenemy.kdist = tenemy.kavgd = (game.sector-tenemy.location).distance()
                         sortenemies()
                     break
-            else:
+            else:                # pragma: no cover
                 prout("Internal error, no enemy where expected!")
                 raise SystemExit(1)
             return None
@@ -1522,7 +1521,7 @@ def attack(torps_ok):
     chgfac = 1.0
     where = "neither"
     if game.idebug:
-        prout("=== ATTACK!")
+        prout("=== ATTACK!")                # pragma: no cover
     # Tholian gets to move before attacking
     if game.tholian:
         movetholian()
@@ -2376,7 +2375,7 @@ def events():
             game.isatb = 0
         else:
             game.battle.invalidate()
-    if game.idebug:
+    if game.idebug:                # pragma: no cover
         prout("=== EVENTS from %.2f to %.2f:" % (game.state.date, fintim))
         for i in range(1, NEVENTS):
             if   i == FSNOVA:  proutn("=== Supernova       ")
@@ -2406,7 +2405,7 @@ def events():
             if game.future[l].date < datemin:
                 evcode = l
                 if game.idebug:
-                    prout("== Event %d fires" % evcode)
+                    prout("== Event %d fires" % evcode)                # pragma: no cover
                 datemin = game.future[l].date
         xtime = datemin-game.state.date
         if game.iscloaked:
@@ -2608,7 +2607,7 @@ def events():
                     break
             else:
                 # can't seem to find one; ignore this call
-                if game.idebug:
+                if game.idebug:                # pragma: no cover
                     prout("=== Couldn't find location for distress event.")
                 continue
             # got one!!  Schedule its enslavement
@@ -2894,7 +2893,7 @@ def supernova(w):
                     break
             if num <=0:
                 break
-        if game.idebug:
+        if game.idebug:                # pragma: no cover
             proutn("=== Super nova here?")
             if ja():
                 nq = game.quadrant
@@ -3173,13 +3172,14 @@ def finish(ifin):
         prout(_("Your starship is now an expanding cloud of subatomic particles"))
     elif ifin == FMATERIALIZE:
         prout(_("Starbase was unable to re-materialize your starship."))
-        prout(_("Sic transit gloria mundi"))
+        prout(_("Sic transit gloria mundi."))
     elif ifin == FPHASER:
         prout(_("The %s has been cremated by its own phasers.") % crmshp())
     elif ifin == FLOST:
         prout(_("You and your landing party have been"))
         prout(_("converted to energy, dissipating through space."))
     elif ifin == FMINING:
+        # This does not seem to be reachable from any code path.
         prout(_("You are left with your landing party on"))
         prout(_("a wild jungle planet inhabited by primitive cannibals."))
         skip(1)
@@ -3192,6 +3192,7 @@ def finish(ifin):
         prout(_("That was a great shot."))
         skip(1)
     elif ifin == FSSC:
+        # This does not seem to be reachable from any code path.
         prout(_("The Galileo is instantly annihilated by the supernova."))
         prout(_("You and your mining party are atomized."))
         skip(1)
@@ -3491,7 +3492,7 @@ def pause_game():
         sys.stdout.write('\n')
         proutn(prompt)
         if not replayfp:
-            my_input()
+            input()
         sys.stdout.write('\n' * rows)
         linecount = 0
 
@@ -3563,7 +3564,7 @@ def cgetline():
                     break
         else:
             try:
-                linein = my_input() + "\n"
+                linein = input() + "\n"
             except EOFError:
                 prout("")
                 sys.exit(0)
@@ -3842,11 +3843,11 @@ def imove(icourse=None, noattack=False):
             stopegy = 50.0*icourse.distance/game.optime
             if iquad in ('T', 'K', 'C', 'S', 'R', '?'):
                 for enemy in game.enemies:
-                    if enemy.location == game.sector:
+                    if enemy.location == h:
                         collision(rammed=False, enemy=enemy)
                         return True
                 # This should not happen
-                prout(_("Which way did he go?"))
+                prout(_("Which way did he go?")) # pragma: no cover
                 return False
             elif iquad == ' ':
                 skip(1)
@@ -5305,8 +5306,8 @@ def damagereport():
     for i in range(NDEVICES):
         if damaged(i):
             if not jdam:
-                prout(_("\tDEVICE\t\t\t-REPAIR TIMES-"))
-                prout(_("\t\t\tIN FLIGHT\t\tDOCKED"))
+                prout(_("DEVICE                             REPAIR TIMES"))
+                prout(_("                                   IN FLIGHT                DOCKED"))
                 jdam = True
             prout("  %-26s\t%8.2f\t\t%8.2f" % (device[i],
                                                game.damage[i]+0.05,
@@ -5339,7 +5340,10 @@ def chart():
         prout(_("(Last surveillance update %d stardates ago).") % ((int)(game.state.date-game.lastchart)))
     prout("      1    2    3    4    5    6    7    8")
     for i in range(GALSIZE):
-        proutn("%d |" % (i+1))
+        if (game.options & OPTION_ALPHAMERIC):
+            proutn("%c |" % letterize(i+1))
+        else:
+            proutn("%d |" % (i+1))
         for j in range(GALSIZE):
             if (game.options & OPTION_SHOWME) and i == game.quadrant.i and j == game.quadrant.j:
                 proutn("<")
@@ -5478,7 +5482,10 @@ def srscan():
     if game.condition != "docked":
         newcnd()
     for i in range(QUADSIZE):
-        proutn("%2d  " % (i+1))
+        if (game.options & OPTION_ALPHAMERIC):
+            proutn("%c   " % letterize(i+1))
+        else:
+            proutn("%2d  " % (i+1))
         for j in range(QUADSIZE):
             sectscan(goodScan, i, j)
         skip(1)
@@ -6050,18 +6057,18 @@ def choose():
         scanner.nexttok()
     if scanner.sees("plain"):
         # Approximates the UT FORTRAN version.
-        game.options &=~ (OPTION_THOLIAN | OPTION_PLANETS | OPTION_THINGY | OPTION_PROBE | OPTION_RAMMING | OPTION_MVBADDY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR | OPTION_CAPTURE | OPTION_CLOAK | OPTION_DOTFILL)
+        game.options &=~ (OPTION_THOLIAN | OPTION_PLANETS | OPTION_THINGY | OPTION_PROBE | OPTION_RAMMING | OPTION_MVBADDY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR | OPTION_CAPTURE | OPTION_CLOAK | OPTION_DOTFILL | OPTION_ALPHAMERIC)
         game.options |= OPTION_PLAIN
     elif scanner.sees("almy"):
         # Approximates Tom Almy's version.
-        game.options &=~ (OPTION_THINGY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR | OPTION_DOTFILL)
+        game.options &=~ (OPTION_THINGY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR | OPTION_DOTFILL | OPTION_ALPHAMERIC)
         game.options |= OPTION_ALMY
     elif scanner.sees("fancy") or scanner.sees("\n"):
         pass
     elif len(scanner.token):
         proutn(_("What is \"%s\"?") % scanner.token)
     setpassword()
-    if game.passwd == "debug":
+    if game.passwd == "debug":                # pragma: no cover
         game.idebug = True
         prout("=== Debug mode enabled.")
     # Use parameters to generate initial values of things
@@ -6236,6 +6243,7 @@ def setpassword():
             proutn(_("Please type in a secret password- "))
             scanner.nexttok()
             game.passwd = scanner.token
+            #game.passwd = getpass.getpass("Please type in a secret password- ")
             if game.passwd is not None:
                 break
     else:
@@ -6294,8 +6302,10 @@ commands = [
     ("",                 0),
 ]
 
-def listCommands():
+def listCommands():                # pragma: no cover
     "Generate a list of legal commands."
+    # Coverage-disabled because testing for this is fragile
+    # in the presence of changes in the command set.
     prout(_("LEGAL COMMANDS ARE:"))
     emitted = 0
     for (key, opt) in commands:
@@ -6396,7 +6406,7 @@ def makemoves():
                 if cmd == scanner.token.upper() or (not abandon_passed \
                         and cmd.startswith(scanner.token.upper())):
                     break
-            if cmd == "":
+            if cmd == "":                # pragma: no cover
                 listCommands()
                 continue
             elif opt and not (opt & game.options):
@@ -6554,7 +6564,7 @@ def cramen(ch):
     elif ch == '#': s = _("Tholian web")
     elif ch == '?': s = _("Stranger")
     elif ch == '@': s = _("Inhabited World")
-    else: s = "Unknown??"
+    else: s = "Unknown??"                # pragma: no cover
     return s
 
 def crmena(loud, enemy, loctype, w):
@@ -6646,6 +6656,15 @@ class sstscanner:
     def getcoord(self):
         s = Coord()
         self.nexttok()
+        if (game.options & OPTION_ALPHAMERIC):
+            try:
+                if (self.type == "IHALPHA") and (self.token[0] in "abcdefghij") and (self.token[1] in "0123456789"):
+                    s.i = ord(self.token[0]) - ord("a")
+                    s.j = int(self.token[1:])-1
+                    return s
+            except (TypeError, IndexError):
+                huh()
+                return None
         if self.type != "IHREAL":
             huh()
             return None
@@ -6656,7 +6675,7 @@ class sstscanner:
             return None
         s.j = self.int()-1
         return s
-    def __repr__(self):
+    def __repr__(self):                # pragma: no cover
         return "<sstcanner: token=%s, type=%s, queue=%s>" % (self.token, self.type, self.inqueue)
 
 def ja():
@@ -6677,7 +6696,7 @@ def huh():
     skip(1)
     prout(_("Beg your pardon, Captain?"))
 
-def debugme():
+def debugme():                # pragma: no cover
     "Access to the internals for debugging."
     proutn("Reset levels? ")
     if ja():
@@ -6779,7 +6798,6 @@ if __name__ == '__main__':
             game.options |= OPTION_TTY
         seed = int(time.time())
         (options, arguments) = getopt.getopt(sys.argv[1:], "cr:s:txV")
-        replay = False
         for (switch, val) in options:
             if switch == '-r':
                 # pylint: disable=raise-missing-from
@@ -6796,40 +6814,39 @@ if __name__ == '__main__':
                     seed = eval(seed)
                     line = replayfp.readline().strip()
                     arguments += line.split()[2:]
-                    replay = True
-                except ValueError:
+                except ValueError:                # pragma: no cover
                     sys.stderr.write("sst: replay file %s is ill-formed\n"% val)
                     raise SystemExit(1)
                 game.options |= OPTION_TTY
                 game.options &=~ OPTION_CURSES
-            elif switch == '-s':
+            elif switch == '-s':                # pragma: no cover
                 seed = int(val)
             elif switch == '-t':
                 game.options |= OPTION_TTY
                 game.options &=~ OPTION_CURSES
-            elif switch == '-x':
+            elif switch == '-x':                # pragma: no cover
                 game.idebug = True
             elif switch == '-c':       # Enable curses debugging - undocumented
                 game.cdebug = True
-            elif switch == '-V':
+            elif switch == '-V':                # pragma: no cover
                 print("SST2K", version)
                 raise SystemExit(0)
-            else:
+            else:                # pragma: no cover
                 sys.stderr.write("usage: sst [-t] [-x] [startcommand...].\n")
                 raise SystemExit(1)
         # where to save the input in case of bugs
-        if "TMPDIR" in os.environ:
+        if "TMPDIR" in os.environ:                # pragma: no cover
             tmpdir = os.environ['TMPDIR']
         else:
             tmpdir = "/tmp"
         try:
             logfp = open(os.path.join(tmpdir, "sst-input.log"), "w")
-        except IOError:
+        except IOError:                # pragma: no cover
             sys.stderr.write("sst: warning, can't open logfile\n")
             sys.exit(1)
         if logfp:
             logfp.write("# seed %s\n" % seed)
-            logfp.write("# options %s\n" % " ".join(arguments))
+            logfp.write("# arguments %s\n" % " ".join(arguments))
             logfp.write("# SST2K version %s\n" % version)
             logfp.write("# recorded by %s@%s on %s\n" % \
                     (getpass.getuser(),socket.getfqdn(),time.ctime()))
@@ -6849,10 +6866,11 @@ if __name__ == '__main__':
                     game.alldone = False
                 else:
                     makemoves()
-                if replay:
+                if replayfp:
                     break
                 skip(1)
-                stars()
+                if (game.options & OPTION_TTY):
+                    stars()
                 skip(1)
                 if game.tourn and game.alldone:
                     proutn(_("Do you want your score recorded?"))
@@ -6861,15 +6879,18 @@ if __name__ == '__main__':
                         scanner.push("\n")
                         freeze(False)
                 scanner.chew()
-                proutn(_("Do you want to play again? "))
-                if not ja():
+                if (game.options & OPTION_TTY):
+                    proutn(_("Do you want to play again? "))
+                    if not ja():
+                        break
+                else:
                     break
             skip(1)
             prout(_("May the Great Bird of the Galaxy roost upon your home planet."))
         finally:
             ioend()
         raise SystemExit(0)
-    except KeyboardInterrupt:
+    except KeyboardInterrupt:                # pragma: no cover
         if logfp:
             logfp.close()
         print("")