Add separate test for malformed torpedo command...
[super-star-trek.git] / sst.py
diff --git a/sst.py b/sst.py
index 07317a04faee98b4f6fb831087fedb577ff37281..16d39e9707ccf0f242c496806889b52a9e349117 100755 (executable)
--- a/sst.py
+++ b/sst.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 """
 sst.py -- Super Star Trek 2K
 
@@ -12,13 +12,20 @@ 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!
+# 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
+
+# pylint: disable=multiple-imports
 import os, sys, math, curses, time, pickle, copy, gettext, getpass
 import getopt, socket, locale
+import codecs
 
 # This import only works on Unixes.  The intention is to enable
 # Ctrl-P, Ctrl-N, and friends in Cmd.
 try:
+    # pylint: disable=unused-import
     import readline
 except ImportError:
     pass
@@ -29,7 +36,7 @@ try:
 except NameError:
     my_input = input
 
-version = "2.4"
+version = "2.7"
 
 docpath        = (".", "doc/", "/usr/share/doc/sst/")
 
@@ -42,7 +49,7 @@ def _(st):
 
 class randomizer:
     # LCG PRNG parameters tested against
-    # Knuth vol. 2. by the authors of ADVENT 
+    # Knuth vol. 2. by the authors of ADVENT
     LCG_A = 1093
     LCG_C = 221587
     LCG_M = 1048576
@@ -51,7 +58,7 @@ class randomizer:
     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;
+        return old_x / randomizer.LCG_M
 
     @staticmethod
     def withprob(p):
@@ -135,9 +142,9 @@ class Coord:
         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
+        return (self.i is not None) and (self.j is not None) and (self.i >= 0) and (self.i < GALSIZE) and (self.j >= 0) and (self.j < GALSIZE)
     def valid_sector(self):
-        return self.i >= 0 and self.i < QUADSIZE and self.j >= 0 and self.j < QUADSIZE
+        return (self.i is not None) and (self.j is not None) and (self.i >= 0) and (self.i < QUADSIZE) and (self.j >= 0) and (self.j < QUADSIZE)
     def invalidate(self):
         self.i = self.j = None
     def __eq__(self, other):
@@ -194,8 +201,8 @@ class Coord:
         return self.roundtogrid() % QUADSIZE
     def scatter(self):
         s = Coord()
-        s.i = self.i + rnd.range(-1, 2)
-        s.j = self.j + rnd.range(-1, 2)
+        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:
@@ -307,6 +314,7 @@ OPTION_CLOAK      = 0x80004000        # Enable BSD-Trek capture (Almy, 2013).
 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)
 
 # Define devices
 DSRSENS         = 0
@@ -536,9 +544,9 @@ def tryexit(enemy, look, irun):
     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
+        return []
     if enemy.type == 'R':
-        return False # Romulans cannot escape!
+        return [] # Romulans cannot escape!
     if not irun:
         # avoid intruding on another commander's territory
         if enemy.type == 'C':
@@ -669,10 +677,8 @@ def movebaddy(enemy):
     nsteps = abs(int(motion))
     if motion > 0 and nsteps > mdist:
         nsteps = mdist # don't overshoot
-    if nsteps > QUADSIZE:
-        nsteps = QUADSIZE # This shouldn't be necessary
-    if nsteps < 1:
-        nsteps = 1 # This shouldn't be necessary
+    nsteps = min(nsteps, QUADSIZE) # This shouldn't be necessary
+    nsteps = max(nsteps, 1) # This shouldn't be necessary
     if game.idebug:
         proutn("NSTEPS = %d:" % nsteps)
     # Compute preferred values of delta X and Y
@@ -718,7 +724,7 @@ def movebaddy(enemy):
             elif (game.options & OPTION_RAMMING) and game.quad[look.i][look.j] != '.':
                 # See if enemy should ram ship
                 if game.quad[look.i][look.j] == game.ship and \
-                    (enemy.type == 'C' or enemy.type == 'S'):
+                    enemy.type in ('C', 'S'):
                     collision(rammed=True, enemy=enemy)
                     return []
                 if krawli != m.i and m.j != 0:
@@ -801,7 +807,7 @@ def movescom(iq, avoid):
             if communicating():
                 announce()
                 prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
-                proutn(_("   a planet in Quadrant %s has been destroyed") % game.state.kscmdr)
+                prout(_("   a planet in Quadrant %s has been destroyed") % game.state.kscmdr)
                 prout(_("   by the Super-commander.\""))
             break
     return True # looks good!
@@ -834,7 +840,7 @@ def supercommander():
         sc = game.state.kscmdr
         for (i, base) in enumerate(game.state.baseq):
             basetbl.append((i, (base - sc).distance()))
-        if game.state.baseq > 1:
+        if len(game.state.baseq) > 1:
             basetbl.sort(key=lambda x: x[1])
         # look for nearest base without a commander, no Enterprise, and
         # without too many Klingons, and not already under attack.
@@ -907,7 +913,7 @@ def supercommander():
                 prout(_("Lt. Uhura-  \"Captain, the starbase in Quadrant %s") \
                       % game.state.kscmdr)
                 prout(_("   reports that it is under attack from the Klingon Super-commander."))
-                proutn(_("   It can survive until stardate %d.\"") \
+                prout(_("   It can survive until stardate %d.\"") \
                        % int(scheduled(FSCDBAS)))
                 if not game.resting:
                     return
@@ -925,7 +931,7 @@ def supercommander():
         return
     announce()
     prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
-    proutn(_("   the Super-commander is in Quadrant %s,") % game.state.kscmdr)
+    prout(_("   the Super-commander is in Quadrant %s.") % game.state.kscmdr)
     return
 
 def movetholian():
@@ -950,7 +956,6 @@ 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
@@ -1243,7 +1248,6 @@ def collision(rammed, enemy):
         damagereport()
     else:
         finish(FWON)
-    return
 
 def torpedo(origin, bearing, dispersion, number, nburst):
     "Let a photon torpedo fly"
@@ -1323,12 +1327,12 @@ def torpedo(origin, bearing, dispersion, number, nburst):
                         deadkl(w, iquad, w)
                         return None
                     proutn(crmena(True, iquad, "sector", w))
-                    displacement = course(track.bearing+rnd.real(-2.4, 2.4), distance=2**0.5)
+                    displacement = course(track.bearing+rnd.real(-2.4, 2.4), distance=2**0.5, origin=w)
                     displacement.nexttok()
                     bumpto = displacement.sector()
                     if not bumpto.valid_sector():
                         prout(_(" damaged but not destroyed."))
-                        return
+                        return None
                     if game.quad[bumpto.i][bumpto.j] == ' ':
                         prout(_(" buffeted into black hole."))
                         deadkl(w, iquad, bumpto)
@@ -1583,8 +1587,7 @@ def attack(torps_ok):
             propor = pfac * game.shield
             if game.condition == "docked":
                 propor *= 2.1
-            if propor < 0.1:
-                propor = 0.1
+            propor = max(propor, 0.1)
             hitsh = propor*chgfac*hit+1.0
             absorb = 0.8*hitsh
             if absorb > game.shield:
@@ -1738,7 +1741,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
@@ -1895,6 +1902,7 @@ def hittem(hits):
                 return
             continue
         else: # decide whether or not to emasculate klingon
+            # pylint: disable=chained-comparison
             if kpow > 0 and rnd.withprob(0.9) and kpow <= rnd.real(0.4, 0.8)*kpini:
                 prout(_("***Mr. Spock-  \"Captain, the vessel at Sector %s")%w)
                 prout(_("   has just lost its firepower.\""))
@@ -2083,7 +2091,6 @@ def phasers():
                 scanner.chew()
                 key = "IHEOL"
                 hits[k] = 0 # prevent overflow -- thanks to Alexei Voitenko
-                k += 1
                 continue
             if key == "IHEOL":
                 scanner.chew()
@@ -2113,7 +2120,7 @@ def phasers():
                 # abort out
                 scanner.chew()
                 return
-            hits[k] = scanner.real
+            hits.append(scanner.real)
             rpow += scanner.real
             # If total requested is too much, inform and start over
             if rpow > avail:
@@ -2121,7 +2128,6 @@ def phasers():
                 scanner.chew()
                 return
             key = scanner.nexttok() # scan for next value
-            k += 1
         if rpow == 0.0:
             # zero energy -- abort
             scanner.chew()
@@ -2512,7 +2518,7 @@ def events():
         elif evcode == FCDBAS: # Commander succeeds in destroying base
             if evcode == FCDBAS:
                 unschedule(FCDBAS)
-                if not game.state.baseq() \
+                if not game.state.baseq \
                        or not game.state.galaxy[game.battle.i][game.battle.j].starbase:
                     game.battle.invalidate()
                     continue
@@ -2558,7 +2564,7 @@ def events():
                 pdest.charted = True
             game.probe.moves -= 1 # One less to travel
             if game.probe.arrived() and game.isarmed and pdest.stars:
-                supernova(game.probe)                # fire in the hole!
+                supernova(game.probe.quadrant())                # fire in the hole!
                 unschedule(FDSPROB)
                 if game.state.galaxy[pquad.i][pquad.j].supernova:
                     return
@@ -2854,7 +2860,7 @@ def supernova(w):
         for nq.i in range(GALSIZE):
             for nq.j in range(GALSIZE):
                 nstars += game.state.galaxy[nq.i][nq.j].stars
-        if stars == 0:
+        if nstars == 0:
             return # nothing to supernova exists
         num = rnd.integer(nstars) + 1
         for nq.i in range(GALSIZE):
@@ -2868,7 +2874,7 @@ def supernova(w):
             proutn("=== Super nova here?")
             if ja():
                 nq = game.quadrant
-    if not nq == game.quadrant or game.justin:
+    if nq != game.quadrant or game.justin:
         # it isn't here, or we just entered (treat as enroute)
         if communicating():
             skip(1)
@@ -2931,7 +2937,7 @@ def supernova(w):
     if game.quadrant == nq or communicating():
         game.state.galaxy[nq.i][nq.j].supernova = True
     # If supernova destroys last Klingons give special message
-    if game.unwon()==0 and not nq == game.quadrant:
+    if game.unwon()==0 and nq != game.quadrant:
         skip(2)
         if w is None:
             prout(_("Lucky you!"))
@@ -3518,7 +3524,7 @@ def prouts(proutsline):
 def cgetline():
     "Get a line of input."
     if game.options & OPTION_CURSES:
-        linein = curwnd.getstr() + "\n"
+        linein = codecs.decode(curwnd.getstr()) + "\n"
         curwnd.refresh()
     else:
         if replayfp and not replayfp.closed:
@@ -3694,7 +3700,7 @@ def tracktorpedo(w, step, i, n, iquad):
             else:
                 skip(1)
                 proutn(_("Torpedo track- "))
-        elif step==4 or step==9:
+        elif step in {4, 9}:
             skip(1)
         proutn("%s   " % w)
     else:
@@ -3702,7 +3708,7 @@ def tracktorpedo(w, step, i, n, iquad):
             if i != 0 and step == 1:
                 drawmaps(2)
                 time.sleep(0.4)
-            if (iquad=='.') or (iquad==' '):
+            if iquad in {'.', ' '}:
                 put_srscan_sym(w, '+')
                 #sound(step*10)
                 #time.sleep(0.1)
@@ -3862,7 +3868,7 @@ def imove(icourse=None, noattack=False):
         if game.iscloaked:
             # We can't be tractor beamed if cloaked,
             # so move the event into the future
-            postpone(FTBEAM, game.optime + expran(1.5*game.intime/len(game.kcmdr)))
+            postpone(FTBEAM, game.optime + expran(1.5*game.intime/len(game.state.kcmdr)))
         else:
             trbeam = True
             game.condition = "red"
@@ -3877,7 +3883,7 @@ def imove(icourse=None, noattack=False):
             newquadrant(noattack)
             break
         elif check_collision(w):
-            print("Collision detected")
+            prout(_("Collision detected"))
             break
         else:
             game.sector = w
@@ -3896,7 +3902,6 @@ def imove(icourse=None, noattack=False):
     newcnd()
     drawmaps(0)
     setwnd(message_window)
-    return
 
 def dock(verbose):
     "Dock our ship at a starbase."
@@ -3907,7 +3912,10 @@ def dock(verbose):
     if game.inorbit:
         prout(_("You must first leave standard orbit."))
         return
-    if game.base is None or abs(game.sector.i-game.base.i) > 1 or abs(game.sector.j-game.base.j) > 1:
+    if game.base is None or not game.base.valid_sector():
+        prout(_("No starbase available for docking in this quadrant."))
+        return
+    if (abs(game.sector.i-game.base.i) > 1) or (abs(game.sector.j-game.base.j) > 1):
         prout(crmshp() + _(" not adjacent to base."))
         return
     if game.iscloaked:
@@ -4561,7 +4569,8 @@ def mayday():
             break
         prout(_("fails."))
         textcolor(DEFAULT)
-        curses.delay_output(500)
+        if game.options & OPTION_CURSES:
+            curses.delay_output(500)
     if m > 3:
         game.quad[game.sector.i][game.sector.j]='?'
         game.alive = False
@@ -5156,6 +5165,7 @@ def attackreport(curt):
 def report():
     # report on general game status
     scanner.chew()
+    # pylint: disable=consider-using-ternary
     s1 = (game.thawed and _("thawed ")) or ""
     s2 = {1:"short", 2:"medium", 4:"long"}[game.length]
     s3 = (None, _("novice"), _("fair"),
@@ -5321,6 +5331,8 @@ def chart():
                 show = ".1."
             elif game.state.galaxy[i][j].charted:
                 show = "%3d" % (game.state.chart[i][j].klingons*100 + game.state.chart[i][j].starbase * 10 + game.state.chart[i][j].stars)
+                if (game.options & OPTION_DOTFILL):
+                    show = show.replace(" ", ".")
             else:
                 show = "..."
             proutn(show)
@@ -5350,6 +5362,8 @@ def sectscan(goodScan, i, j):
                        'C':LIGHTRED,
                        'R':LIGHTRED,
                        'T':LIGHTRED,
+                       '@':LIGHTGREEN,
+                       'P':LIGHTGREEN,
                       }.get(game.quad[i][j], DEFAULT))
         proutn("%c " % game.quad[i][j])
         textcolor(DEFAULT)
@@ -5504,8 +5518,7 @@ def eta():
                 prout(_("We'll never make it, sir."))
                 scanner.chew()
                 return
-            if twarp < 1.0:
-                twarp = 1.0
+            twarp = max(twarp, 1.0)
             break
         scanner.chew()
         proutn(_("Warp factor? "))
@@ -5784,13 +5797,11 @@ def setup():
     # Position ordinary Klingon Battle Cruisers
     krem = game.inkling
     klumper = 0.25*game.skill*(9.0-game.length)+1.0
-    if klumper > MAXKLQUAD:
-        klumper = MAXKLQUAD
+    klumper = min(klumper, MAXKLQUAD)
     while True:
         r = rnd.real()
         klump = int((1.0 - r*r)*klumper)
-        if klump > krem:
-            klump = krem
+        klump = min(klump, krem)
         krem -= klump
         while True:
             w = randplace(GALSIZE)
@@ -5983,11 +5994,11 @@ 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)
+        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_PLAIN
     elif scanner.sees("almy"):
         # Approximates Tom Almy's version.
-        game.options &=~ (OPTION_THINGY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR)
+        game.options &=~ (OPTION_THINGY | OPTION_BLKHOLE | OPTION_BASE | OPTION_WORLDS | OPTION_COLOR | OPTION_DOTFILL)
         game.options |= OPTION_ALMY
     elif scanner.sees("fancy") or scanner.sees("\n"):
         pass
@@ -6051,6 +6062,7 @@ def newqad():
     game.iplnet = None
     game.neutz = game.inorbit = game.landed = False
     game.ientesc = game.iseenit = game.isviolreported = False
+    game.tholian = None
     # Create a blank quadrant
     game.quad = fill2d(QUADSIZE, lambda i, j: '.')
     if game.iscate:
@@ -6655,7 +6667,7 @@ def debugme():
             proutn(legends[i])
             if is_scheduled(i):
                 proutn("%.2f" % (scheduled(i)-game.state.date))
-                if i == FENSLV or i == FREPRO:
+                if i in {FENSLV, FREPRO}:
                     ev = findevent(i)
                     proutn(" in %s" % ev.quadrant)
             else:
@@ -6668,7 +6680,7 @@ def debugme():
                 scanner.chew()
             elif key == "IHREAL":
                 ev = schedule(i, scanner.real)
-                if i == FENSLV or i == FREPRO:
+                if i in {FENSLV, FREPRO}:
                     scanner.chew()
                     proutn("In quadrant- ")
                     key = scanner.nexttok()
@@ -6710,14 +6722,17 @@ if __name__ == '__main__':
         replay = False
         for (switch, val) in options:
             if switch == '-r':
+                # pylint: disable=raise-missing-from
                 try:
                     replayfp = open(val, "r")
                 except IOError:
                     sys.stderr.write("sst: can't open replay file %s\n" % val)
                     raise SystemExit(1)
+                # pylint: disable=raise-missing-from
                 try:
                     line = replayfp.readline().strip()
                     (leader, __, seed) = line.split()
+                    # pylint: disable=eval-used
                     seed = eval(seed)
                     line = replayfp.readline().strip()
                     arguments += line.split()[2:]
@@ -6757,7 +6772,7 @@ if __name__ == '__main__':
             logfp.write("# options %s\n" % " ".join(arguments))
             logfp.write("# SST2K version %s\n" % version)
             logfp.write("# recorded by %s@%s on %s\n" % \
-                    (getpass.getuser(),socket.gethostname(),time.ctime()))
+                    (getpass.getuser(),socket.getfqdn(),time.ctime()))
         rnd.seed(seed)
         scanner = sstscanner()
         for arg in arguments: