Be a bit more colorful.
[super-star-trek.git] / sst.py
diff --git a/sst.py b/sst.py
index a2cedc575a37e2a08472aec68a0cb3483a6b6383..b2067e965cfa4863bf58e7a9f9a03cf647363a6b 100755 (executable)
--- a/sst.py
+++ b/sst.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python
 """
 sst.py -- Super Star Trek 2K
 
@@ -12,6 +12,8 @@ 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
 
 import os, sys, math, curses, time, pickle, copy, gettext, getpass
 import getopt, socket, locale
@@ -29,50 +31,64 @@ try:
 except NameError:
     my_input = input
 
-version = "2.4"
+version = "2.5"
 
 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):
-        v = random.random()
-        if logfp:
-            logfp.write("#withprob(%.2f) -> %s\n" % (p, v < p))
+        v = randomizer.random()
+        #if logfp:
+        #    logfp.write("#withprob(%.2f) -> %s\n" % (p, v < p))
         return v < p
 
     @staticmethod
     def integer(*args):
-        s = random.randrange(*args)
-        if logfp:
-            logfp.write("#randrange%s -> %s\n" % (args, s))
-        return s
+        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 +134,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 +178,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
@@ -180,8 +196,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:
@@ -293,6 +309,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
@@ -469,6 +486,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):
@@ -786,7 +804,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!
@@ -819,7 +837,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.
@@ -892,7 +910,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
@@ -910,7 +928,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():
@@ -1722,7 +1740,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
@@ -2067,7 +2089,6 @@ def phasers():
                 scanner.chew()
                 key = "IHEOL"
                 hits[k] = 0 # prevent overflow -- thanks to Alexei Voitenko
-                k += 1
                 continue
             if key == "IHEOL":
                 scanner.chew()
@@ -2097,7 +2118,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:
@@ -2105,7 +2126,6 @@ def phasers():
                 scanner.chew()
                 return
             key = scanner.nexttok() # scan for next value
-            k += 1
         if rpow == 0.0:
             # zero energy -- abort
             scanner.chew()
@@ -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
@@ -3860,7 +3881,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
@@ -5304,6 +5325,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)
@@ -5333,6 +5356,9 @@ def sectscan(goodScan, i, j):
                        'C':LIGHTRED,
                        'R':LIGHTRED,
                        'T':LIGHTRED,
+                       '@':LIGHTGREEN,
+                       'P':LIGHTGREEN,
+                       '?':MAGENTA,
                       }.get(game.quad[i][j], DEFAULT))
         proutn("%c " % game.quad[i][j])
         textcolor(DEFAULT)
@@ -5966,11 +5992,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
@@ -6034,6 +6060,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: