Improve test coverage.
[super-star-trek.git] / sst
diff --git a/sst b/sst
index b9d9a6acf9c3467a64676c7bcca2962f8450dbc0..2c55b91fcb0081ad9e4ee7dc66c8fd2cf5c866fd 100755 (executable)
--- a/sst
+++ b/sst
@@ -18,7 +18,7 @@ on how to modify (and how not to modify!) this code.
 
 # pylint: disable=multiple-imports
 import os, sys, math, curses, time, pickle, copy, gettext, getpass
-import getopt, socket, locale
+import getopt, socket
 import codecs
 
 # This import only works on Unixes.  The intention is to enable
@@ -88,6 +88,14 @@ class randomizer:
         #    logfp.write("#seed(%d)\n" % n)
         game.lcg_x = n % randomizer.LCG_M
 
+    @staticmethod
+    def getrngstate():
+        return game.lcg_x
+
+    @staticmethod
+    def setrngstate(n):
+        game.lcg_x = n
+
 GALSIZE        = 8             # Galaxy size in quadrants
 NINHAB         = (GALSIZE * GALSIZE // 2)      # Number of inhabited worlds
 MAXUNINHAB     = 10            # Maximum uninhabited worlds
@@ -195,11 +203,6 @@ class Coord:
         return self.roundtogrid() // QUADSIZE
     def sector(self):
         return self.roundtogrid() % QUADSIZE
-    def scatter(self):
-        s = Coord()
-        s.i = self.i + rnd.integer(-1, 2)
-        s.j = self.j + rnd.integer(-1, 2)
-        return s
     def __str__(self):
         if self.i is None or self.j is None:
             return "Nowhere"  # pragma: no cover
@@ -208,15 +211,15 @@ class Coord:
         return "%s - %s" % (self.i+1, self.j+1)
     __repr__ = __str__
 
-class Thingy(Coord):
+class Thingy():
     "Do not anger the Space Thingy!"
     def __init__(self):
-        Coord.__init__(self)
+        self.location = Coord()
         self.angered = False
     def angry(self):
         self.angered = True
     def at(self, q):
-        return (q.i, q.j) == (self.i, self.j)
+        return (q.i, q.j) == (self.location.i, self.location.j)
 
 class Planet:
     def __init__(self):
@@ -3412,11 +3415,6 @@ curwnd = None
 
 def iostart():
     global stdscr, rows
-    # for some recent versions of python2, the following enables UTF8
-    # for the older ones we probably need to set C locale, and python3
-    # has no problems at all
-    if sys.version_info[0] < 3:
-        locale.setlocale(locale.LC_ALL, "")
     gettext.bindtextdomain("sst", "/usr/local/share/locale")
     gettext.textdomain("sst")
     if not (game.options & OPTION_CURSES):
@@ -3425,7 +3423,7 @@ def iostart():
             rows = ln_env
         else:
             rows = 25
-    else:
+    else:      # pragma: no cover
         stdscr = curses.initscr()
         stdscr.keypad(True)
         curses.nonl()
@@ -3456,7 +3454,7 @@ def iostart():
 
 def ioend():
     "Wrap up I/O."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         stdscr.keypad(False)
         curses.echo()
         curses.nocbreak()
@@ -3464,7 +3462,7 @@ def ioend():
 
 def waitfor():
     "Wait for user action -- OK to do nothing if on a TTY"
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         stdscr.getch()
 
 def announce():
@@ -3478,7 +3476,7 @@ def pause_game():
     else:
         prompt = _("[PRESS ENTER TO CONTINUE]")
 
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         drawmaps(0)
         setwnd(prompt_window)
         prompt_window.clear()
@@ -3499,7 +3497,7 @@ def pause_game():
 def skip(i):
     "Skip i lines.  Pause game if this would cause a scrolling event."
     for _dummy in range(i):
-        if game.options & OPTION_CURSES:
+        if game.options & OPTION_CURSES:       # pragma: no cover
             (y, _x) = curwnd.getyx()
             try:
                 curwnd.move(y+1, 0)
@@ -3515,7 +3513,7 @@ def skip(i):
 
 def proutn(proutntline):
     "Utter a line with no following line feed."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         (y, x) = curwnd.getyx()
         (my, _mx) = curwnd.getmaxyx()
         if curwnd == message_window and y >= my - 2:
@@ -3539,7 +3537,7 @@ def prouts(proutsline):
         if not replayfp or replayfp.closed:        # Don't slow down replays
             time.sleep(0.03)
         proutn(c)
-        if game.options & OPTION_CURSES:
+        if game.options & OPTION_CURSES:       # pragma: no cover
             curwnd.refresh()
         else:
             sys.stdout.flush()
@@ -3548,7 +3546,7 @@ def prouts(proutsline):
 
 def cgetline():
     "Get a line of input."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         linein = codecs.decode(curwnd.getstr()) + "\n"
         curwnd.refresh()
     else:
@@ -3556,7 +3554,7 @@ def cgetline():
             while True:
                 linein = replayfp.readline()
                 proutn(linein)
-                if linein == '':
+                if linein == '':    # pragma: no cover
                     prout("*** Replay finished")
                     replayfp.close()
                     break
@@ -3575,7 +3573,7 @@ def cgetline():
 def setwnd(wnd):
     "Change windows -- OK for this to be a no-op in tty mode."
     global curwnd
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         if game.cdebug and logfp:
             if wnd == fullscreen_window:
                 legend = "fullscreen"
@@ -3603,21 +3601,21 @@ def setwnd(wnd):
 
 def clreol():
     "Clear to end of line -- can be a no-op in tty mode"
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         curwnd.clrtoeol()
         curwnd.refresh()
 
 def clrscr():
     "Clear screen -- can be a no-op in tty mode."
     global linecount
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         curwnd.clear()
         curwnd.move(0, 0)
         curwnd.refresh()
     linecount = 0
 
 def textcolor(color=DEFAULT):
-    if (game.options & OPTION_COLOR) and (game.options & OPTION_CURSES):
+    if (game.options & OPTION_COLOR) and (game.options & OPTION_CURSES):       # pragma: no cover
         if color == DEFAULT:
             curwnd.attrset(0)
         elif color ==  BLACK:
@@ -3654,7 +3652,7 @@ def textcolor(color=DEFAULT):
             curwnd.attron(curses.color_pair(curses.COLOR_WHITE) | curses.A_BOLD)
 
 def highvideo():
-    if (game.options & OPTION_COLOR) and (game.options & OPTION_CURSES):
+    if (game.options & OPTION_COLOR) and (game.options & OPTION_CURSES):       # pragma: no cover
         curwnd.attron(curses.A_REVERSE)
 
 #
@@ -3663,7 +3661,7 @@ def highvideo():
 
 def drawmaps(mode):
     "Hook to be called after moving to redraw maps."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         if mode == 1:
             sensor()
         setwnd(srscan_window)
@@ -3682,7 +3680,7 @@ def drawmaps(mode):
             lrscan_window.move(0, 0)
             lrscan(silent=False)
 
-def put_srscan_sym(w, sym):
+def put_srscan_sym(w, sym):    # pragma: no cover
     "Emit symbol for short-range scan."
     srscan_window.move(w.i+1, w.j*2+2)
     srscan_window.addch(sym)
@@ -3690,7 +3688,7 @@ def put_srscan_sym(w, sym):
 
 def boom(w):
     "Enemy fall down, go boom."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         drawmaps(0)
         setwnd(srscan_window)
         srscan_window.attron(curses.A_REVERSE)
@@ -3705,12 +3703,12 @@ def boom(w):
 
 def warble():
     "Sound and visual effects for teleportation."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         drawmaps(2)
         setwnd(message_window)
         #sound(50)
     prouts("     . . . . .     ")
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         #curses.delay_output(1000)
         #nosound()
         pass
@@ -3728,7 +3726,7 @@ def tracktorpedo(w, step, i, n, iquad):
         elif step in {4, 9}:
             skip(1)
         proutn("%s   " % w)
-    else:
+    else:      # pragma: no cover
         if not damaged(DSRSENS) or game.condition=="docked":
             if i != 0 and step == 1:
                 drawmaps(2)
@@ -3752,7 +3750,7 @@ def tracktorpedo(w, step, i, n, iquad):
 
 def makechart():
     "Display the current galaxy chart."
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         setwnd(message_window)
         message_window.clear()
     chart()
@@ -3763,14 +3761,14 @@ NSYM        = 14
 
 def prstat(txt, data):
     proutn(txt)
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         skip(1)
         setwnd(status_window)
     else:
         proutn(" " * (NSYM - len(txt)))
     proutn(data)
     skip(1)
-    if game.options & OPTION_CURSES:
+    if game.options & OPTION_CURSES:   # pragma: no cover
         setwnd(report_window)
 
 # Code from moving.c begins here
@@ -3848,7 +3846,7 @@ def imove(icourse=None, noattack=False):
                         return True
                 # This should not happen
                 prout(_("Which way did he go?")) # pragma: no cover
-                return False
+                return False # pragma: no cover
             elif iquad == ' ':
                 skip(1)
                 prouts(_("***RED ALERT!  RED ALERT!"))
@@ -4590,7 +4588,7 @@ def mayday():
             break
         prout(_("fails."))
         textcolor(DEFAULT)
-        if game.options & OPTION_CURSES:
+        if game.options & OPTION_CURSES:       # pragma: no cover
             curses.delay_output(500)
     if m > 3:
         game.quad[game.sector.i][game.sector.j]='?'
@@ -5373,7 +5371,7 @@ def sectscan(goodScan, i, j):
     if goodScan or (abs(i-game.sector.i)<= 1 and abs(j-game.sector.j) <= 1):
         if game.quad[i][j] in ('E', 'F'):
             if game.iscloaked:
-                highvideo()
+                highvideo()    # pragma: no cover
             textcolor({"green":GREEN,
                        "yellow":YELLOW,
                        "red":RED,
@@ -5634,11 +5632,11 @@ def goptions():
             else:
                 prout(_("No such option as ") + scanner.token)
         if mode == "set":
-            if (not (game.options & OPTION_CURSES)) and (changemask & OPTION_CURSES):
+            if (not (game.options & OPTION_CURSES)) and (changemask & OPTION_CURSES):  # pragma: no cover
                 iostart()
             game.options |= changemask
         elif mode == "clear":
-            if (game.options & OPTION_CURSES) and (not (changemask & OPTION_CURSES)):
+            if (game.options & OPTION_CURSES) and (not (changemask & OPTION_CURSES)):  # pragma: no cover
                 ioend()
             game.options &=~ changemask
         prout(_("Acknowledged, Captain."))
@@ -5940,12 +5938,17 @@ def setup():
     # Place thing (in tournament game, we don't want one!)
     # New in SST2K: never place the Thing near a starbase.
     # This makes sense and avoids a special case in the old code.
-    global thing
     if game.tourn is None:
+        # Avoid distrubing the RNG chain.  This code
+        # was a late fix and we don't want to mess up
+        # all the regression tests.
+        state = randomizer.getrngstate()
         while True:
-            thing = randplace(GALSIZE)
-            if thing not in game.state.baseq:
+            thing.location = randplace(GALSIZE)
+            # Put it somewhere a starbase is not
+            if thing.location not in game.state.baseq:
                 break
+        randomizer.setrngstate(state)
     skip(2)
     game.state.snap = False
     if game.skill == SKILL_NOVICE:
@@ -5981,7 +5984,7 @@ def setup():
     clrscr()
     setwnd(message_window)
     newqad()
-    if len(game.enemies) - (thing == game.quadrant) - (game.tholian is not None):
+    if len(game.enemies) - (thing.location == game.quadrant) - (game.tholian is not None):
         game.shldup = True
     if game.neutz:        # bad luck to start in a Romulan Neutral Zone
         attack(torps_ok=False)
@@ -5989,7 +5992,8 @@ def setup():
 def choose():
     "Choose your game type."
     while True:
-        game.tourn = game.length = 0
+        game.tourn = None
+        game.length = 0
         game.thawed = False
         game.skill = SKILL_NONE
         # Do not chew here, we want to use command-line tokens
@@ -5997,6 +6001,7 @@ def choose():
             proutn(_("Would you like a regular, tournament, or saved game? "))
         scanner.nexttok()
         if scanner.sees("tournament"):
+            game.tourn = 0
             while scanner.nexttok() == "IHEOL":
                 proutn(_("Type in tournament number-"))
             if scanner.real == 0:
@@ -6184,7 +6189,7 @@ def newqad():
             prout(_("INTRUDER! YOU HAVE VIOLATED THE ROMULAN NEUTRAL ZONE."))
             prout(_("LEAVE AT ONCE, OR YOU WILL BE DESTROYED!"))
     # Put in THING if needed
-    if thing == game.quadrant:
+    if thing.location == game.quadrant:
         Enemy(etype='?', loc=dropin(),
               power=rnd.real(6000,6500.0)+250.0*game.skill)
         if not damaged(DSRSENS):
@@ -6387,7 +6392,7 @@ def makemoves():
             clrscr()
             proutn("COMMAND> ")
             if scanner.nexttok() == "IHEOL":
-                if game.options & OPTION_CURSES:
+                if game.options & OPTION_CURSES:       # pragma: no cover
                     makechart()
                 continue
             elif scanner.token == "":
@@ -6413,7 +6418,7 @@ def makemoves():
                 huh()
             else:
                 break
-        if game.options & OPTION_CURSES:
+        if game.options & OPTION_CURSES:       # pragma: no cover
             prout("COMMAND> %s" % cmd)
         if cmd == "SRSCAN":                # srscan
             srscan()
@@ -6521,7 +6526,7 @@ def makemoves():
             helpme()                        # get help
         elif cmd == "SCORE":
             score()                         # see current score
-        elif cmd == "CURSES":
+        elif cmd == "CURSES":  # pragma: no cover
             game.options |= (OPTION_CURSES | OPTION_COLOR)
             iostart()
         elif cmd == "OPTIONS":
@@ -6793,7 +6798,7 @@ if __name__ == '__main__':
         logfp = None
         game.options = OPTION_ALL &~ (OPTION_IOMODES | OPTION_PLAIN | OPTION_ALMY)
         if os.getenv("TERM"):
-            game.options |= OPTION_CURSES
+            game.options |= OPTION_CURSES      # pragma: no cover
         else:
             game.options |= OPTION_TTY
         seed = int(time.time())
@@ -6821,7 +6826,7 @@ if __name__ == '__main__':
                 game.options &=~ OPTION_CURSES
             elif switch == '-s':                # pragma: no cover
                 seed = int(val)
-            elif switch == '-t':
+            elif switch == '-t':       # pragma: no cover
                 game.options |= OPTION_TTY
                 game.options &=~ OPTION_CURSES
             elif switch == '-x':                # pragma: no cover