Imprive documentation, fix a typo, add the hacking guide.
[super-star-trek.git] / src / sst.py
index bd01149621a1f10e24d6386883d9e0f3dbab4b13..3f37ea35b92f1265a942347f5347b90fbec9c152 100644 (file)
 #!/usr/bin/env python
 """
 #!/usr/bin/env python
 """
-sst.py =-- Super Star Trek in Python
-
-This code is a Python translation of a C translation of a FORTRAN
-original dating back to 1973.  Beautiful Python it is not.  But it
-works.
-
-Dave Matuszek says:
-
-SRSCAN, MOVE, PHASERS, CALL, STATUS, IMPULSE, PHOTONS, ABANDON,
-LRSCAN, WARP, SHIELDS, DESTRUCT, CHART, REST, DOCK, QUIT, and DAMAGE
-were in the original non-"super" version of UT FORTRAN Star Trek.
-
-Tholians were not in the original. Dave is dubious about their merits.
-(They are now controlled by OPTION_THOLIAN and turned off if the game
-type is "plain".)
-
-Planets and dilithium crystals were not in the original.  Dave is OK
-with this idea. (It's now controlled by OPTION_PLANETS and turned 
-off if the game type is "plain".)
+sst.py -- Super Star Trek 2K
 
 
-Dave says the bit about the Galileo getting turned into a
-McDonald's is "consistant with our original vision".  (This has been
-left permanently enabled, as it can only happen if OPTION_PLANETS
-is on.)
+SST2K is a Python translation of a C translation of a FORTRAN
+original dating back to 1973.  Beautiful Python it is not, but it
+works.  Translation by Eric S. Raymond; original game by David Matuszek
+and Paul Reynolds, with modifications by Don Smith, Tom Almy,
+Stas Sergeev, and Eric S. Raymond.
 
 
-Dave also says the Space Thingy should not be preserved across saved
-games, so you can't prove to others that you've seen it.  He says it
-shouldn't fire back, either.  It should do nothing except scream and
-disappear when hit by photon torpedos.  It's OK that it may move
-when attacked, but it didn't in the original.  (Whether the Thingy
-can fire back is now controlled by OPTION_THINGY and turned off if the
-game type is "plain" or "almy".  The no-save behavior has been restored.)
-
-The Faerie Queen, black holes, and time warping were in the original.
-
-Here are Tom Almy's changes:
-
-In early 1997, I got the bright idea to look for references to
-"Super Star Trek" on the World Wide Web. There weren't many hits,
-but there was one that came up with 1979 Fortran sources! This
-version had a few additional features that mine didn't have,
-however mine had some feature it didn't have. So I merged its
-features that I liked. I also took a peek at the DECUS version (a
-port, less sources, to the PDP-10), and some other variations.
-
-1, Compared to the original UT version, I've changed the "help"
-command to "call" and the "terminate" command to "quit" to better
-match user expectations. The DECUS version apparently made those
-changes as well as changing "freeze" to "save". However I like
-"freeze".  (Both "freeze" and "save" work in SST2K.)
-
-2. The experimental deathray originally had only a 5% chance of
-success, but could be used repeatedly. I guess after a couple
-years of use, it was less "experimental" because the 1979
-version had a 70% success rate. However it was prone to breaking
-after use. I upgraded the deathray, but kept the original set of
-failure modes (great humor!).  (Now controlled by OPTION_DEATHRAY
-and turned off if game type is "plain".)
-
-3. The 1979 version also mentions srscan and lrscan working when
-docked (using the starbase's scanners), so I made some changes here
-to do this (and indicating that fact to the player), and then realized
-the base would have a subspace radio as well -- doing a Chart when docked
-updates the star chart, and all radio reports will be heard. The Dock
-command will also give a report if a base is under attack.
-
-4. Tholian Web from the 1979 version.  (Now controlled by
-OPTION_THOLIAN and turned off if game type is "plain".)
-
-5. Enemies can ram the Enterprise. (Now controlled by OPTION_RAMMING
-and turned off if game type is "plain".)
-
-6. Regular Klingons and Romulans can move in Expert and Emeritus games. 
-This code could use improvement. (Now controlled by OPTION_MVBADDY
-and turned off if game type is "plain".)
-
-7. The deep-space probe feature from the DECUS version.  (Now controlled
-by OPTION_PROBE and turned off if game type is "plain").
-
-8. 'emexit' command from the 1979 version.
-
-9. Bugfix: Klingon commander movements are no longer reported if long-range 
-sensors are damaged.
-
-10. Bugfix: Better base positioning at startup (more spread out).
-That made sense to add because most people abort games with 
-bad base placement.
-
-In June 2002, I fixed two known bugs and a documentation typo.
-In June 2004 I fixed a number of bugs involving: 1) parsing invalid
-numbers, 2) manual phasers when SR scan is damaged and commander is
-present, 3) time warping into the future, 4) hang when moving
-klingons in crowded quadrants.  (These fixes are in SST2K.)
-
-Here are Stas Sergeev's changes:
-
-1. The Space Thingy can be shoved, if you ram it, and can fire back if 
-fired upon. (Now controlled by OPTION_THINGY and turned off if game 
-type is "plain" or "almy".)
-
-2. When you are docked, base covers you with an almost invincible shield. 
-(A commander can still ram you, or a Romulan can destroy the base,
-or a SCom can even succeed with direct attack IIRC, but this rarely 
-happens.)  (Now controlled by OPTION_BASE and turned off if game 
-type is "plain" or "almy".)
-
-3. Ramming a black hole is no longer instant death.  There is a
-chance you might get timewarped instead. (Now controlled by 
-OPTION_BLKHOLE and turned off if game type is "plain" or "almy".)
-
-4. The Tholian can be hit with phasers.
-
-5. SCom can't escape from you if no more enemies remain 
-(without this, chasing SCom can take an eternity).
-
-6. Probe target you enter is now the destination quadrant. Before I don't 
-remember what it was, but it was something I had difficulty using.
-
-7. Secret password is now autogenerated.
-
-8. "Plaque" is adjusted for A4 paper :-)
-
-9. Phasers now tells you how much energy needed, but only if the computer 
-is alive.
-
-10. Planets are auto-scanned when you enter the quadrant.
-
-11. Mining or using crystals in presense of enemy now yields an attack.
-There are other minor adjustments to what yields an attack
-and what does not.
-
-12. "freeze" command reverts to "save", most people will understand this
-better anyway. (SST2K recognizes both.)
-
-13. Screen-oriented interface, with sensor scans always up.  (SST2K
-supports both screen-oriented and TTY modes.)
-
-Eric Raymond's changes:
-
-Mainly, I translated this C code out of FORTRAN into C -- created #defines
-for a lot of magic numbers and refactored the heck out of it.
-
-1. "sos" and "call" becomes "mayday", "freeze" and "save" are both good.
-
-2. Status report now indicates when dilithium crystals are on board.
-
-3. Per Dave Matuszek's remarks, Thingy state is never saved across games.
-
-4. Added game option selection so you can play a close (but not bug-for-
-bug identical) approximation of older versions.
-
-5. Half the quadrants now have inhabited planets, from which one 
-cannot mine dilithium (there will still be the same additional number
-of dilithium-bearing planets).  Torpedoing an inhabited world is *bad*.
-There is BSD-Trek-like logic for Klingons to attack and enslave 
-inhabited worlds, producing more ships (only is skill is 'good' or 
-better). (Controlled by OPTION_WORLDS and turned off if game 
-type is "plain" or "almy".)
-
-6. User input is now logged so we can do regression testing.
-
-7. More BSD-Trek features: You can now lose if your entire crew
-dies in battle.  When abandoning ship in a game with inhabited
-worlds enabled, they must have one in the quadrant to beam down
-to; otherwise they die in space and this counts heavily against
-your score.  Docking at a starbase replenishes your crew.
-
-8. Still more BSD-Trek: we now have a weighted damage table.
-Also, the nav subsystem (enabling automatic course
-setting) can be damaged separately from the main computer (which
-handles weapons targeting, ETA calculation, and self-destruct).
-
-After these features were added, I translated this into Python and added
-more:
-
-9. A long-range scan is done silently whenever you call CHART; thus
-the LRSCAN command is no longer needed.  (Controlled by OPTION_AUTOSCAN
-and turned off if game type is "plain" or "almy".)
+See the doc/HACKING file in the distribution for designers notes and advice
+ion how to modify (and how not to modify!) this code.
 """
 import os, sys, math, curses, time, readline, cPickle, random, copy, gettext
 
 """
 import os, sys, math, curses, time, readline, cPickle, random, copy, gettext
 
@@ -250,18 +82,17 @@ class coord:
         return coord(self.i*other, self.j*other)
     def __div__(self, other):
         return coord(self.i/other, self.j/other)
         return coord(self.i*other, self.j*other)
     def __div__(self, other):
         return coord(self.i/other, self.j/other)
+    def __mod__(self, other):
+        return coord(self.i % other, self.j % other)
     def __rdiv__(self, other):
         return coord(self.i/other, self.j/other)
     def roundtogrid(self):
         return coord(int(round(self.i)), int(round(self.j)))
     def __rdiv__(self, other):
         return coord(self.i/other, self.j/other)
     def roundtogrid(self):
         return coord(int(round(self.i)), int(round(self.j)))
-    def trunctogrid(self):
-        return coord(int(round(self.i)), int(round(self.j)))
     def distance(self, other=None):
         if not other: other = coord(0, 0)
         return math.sqrt((self.i - other.i)**2 + (self.j - other.j)**2)
     def distance(self, other=None):
         if not other: other = coord(0, 0)
         return math.sqrt((self.i - other.i)**2 + (self.j - other.j)**2)
-    def bearing(self, other=None):
-        if not other: other = coord(0, 0)
-        return 1.90985*math.atan2(self.j-other.j, self.i-other.i)
+    def bearing(self):
+        return 1.90985*math.atan2(self.j, self.i)
     def sgn(self):
         s = coord()
         if self.i == 0:
     def sgn(self):
         s = coord()
         if self.i == 0:
@@ -273,13 +104,16 @@ class coord:
         else:
             s.j = self.j / abs(self.j)
         return s
         else:
             s.j = self.j / abs(self.j)
         return s
+    def quadrant(self):
+        #print "Location %s -> %s" % (self, (self / QUADSIZE).roundtogrid())
+        return self.roundtogrid() / QUADSIZE
+    def sector(self):
+        return self.roundtogrid() % QUADSIZE
     def scatter(self):
         s = coord()
         s.i = self.i + randrange(-1, 2)
         s.j = self.j + randrange(-1, 2)
         return s
     def scatter(self):
         s = coord()
         s.i = self.i + randrange(-1, 2)
         s.j = self.j + randrange(-1, 2)
         return s
-    def __hash__(self):
-        return hash((x, y))
     def __str__(self):
         if self.i == None or self.j == None:
             return "Nowhere"
     def __str__(self):
         if self.i == None or self.j == None:
             return "Nowhere"
@@ -1267,14 +1101,14 @@ def torpedo(origin, bearing, dispersion, number, nburst):
        setwnd(srscan_window)
     else: 
        setwnd(message_window)
        setwnd(srscan_window)
     else: 
        setwnd(message_window)
-    shoved = False
     ac = bearing + 0.25*dispersion     # dispersion is a random variable
     bullseye = (15.0 - bearing)*0.5235988
     track = course(bearing=ac, distance=QUADSIZE, origin=cartesian(origin)) 
     ac = bearing + 0.25*dispersion     # dispersion is a random variable
     bullseye = (15.0 - bearing)*0.5235988
     track = course(bearing=ac, distance=QUADSIZE, origin=cartesian(origin)) 
-    jw = coord(0, 0)
+    bumpto = coord(0, 0)
     # Loop to move a single torpedo 
     # Loop to move a single torpedo 
+    setwnd(message_window)
     for step in range(1, QUADSIZE*2):
     for step in range(1, QUADSIZE*2):
-       track.next()
+        if not track.next(): break
         w = track.sector()
        if not w.valid_sector():
            break
         w = track.sector()
        if not w.valid_sector():
            break
@@ -1283,37 +1117,38 @@ def torpedo(origin, bearing, dispersion, number, nburst):
        if iquad==IHDOT:
            continue
        # hit something 
        if iquad==IHDOT:
            continue
        # hit something 
-       setwnd(message_window)
-       if damaged(DSRSENS) and not game.condition=="docked":
+       if not damaged(DSRSENS) or game.condition == "docked":
            skip(1);    # start new line after text track 
        if iquad in (IHE, IHF): # Hit our ship 
            skip(1)
            prout(_("Torpedo hits %s.") % crmshp())
            hit = 700.0 + randreal(100) - \
            skip(1);    # start new line after text track 
        if iquad in (IHE, IHF): # Hit our ship 
            skip(1)
            prout(_("Torpedo hits %s.") % crmshp())
            hit = 700.0 + randreal(100) - \
-               1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-angle))
+               1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-track.angle))
            newcnd(); # we're blown out of dock 
            newcnd(); # we're blown out of dock 
-           # We may be displaced. 
            if game.landed or game.condition=="docked":
                return hit # Cheat if on a planet 
            if game.landed or game.condition=="docked":
                return hit # Cheat if on a planet 
-           ang = track.angle + 2.5*(randreal()-0.5)
-           temp = math.fabs(math.sin(ang))
-           if math.fabs(math.cos(ang)) > temp:
-               temp = math.fabs(math.cos(ang))
-           xx = -math.sin(ang)/temp
-           yy = math.cos(ang)/temp
-           jw.i = int(w.i+xx+0.5)
-           jw.j = int(w.j+yy+0.5)
-           if not jw.valid_sector():
+            # In the C/FORTRAN version, dispersion was 2.5 radians, which
+            # is 143 degrees, which is almost exactly 4.8 clockface units
+            displacement = course(track.bearing+randreal(-2.4,2.4), distance=2**0.5)
+            displacement.next()
+            bumpto = displacement.sector()
+           if not bumpto.valid_sector():
                return hit
                return hit
-           if game.quad[jw.i][jw.j]==IHBLANK:
+           if game.quad[bumpto.i][bumpto.j]==IHBLANK:
                finish(FHOLE)
                return hit
                finish(FHOLE)
                return hit
-           if game.quad[jw.i][jw.j]!=IHDOT:
+           if game.quad[bumpto.i][bumpto.j]!=IHDOT:
                # can't move into object 
                return hit
                # can't move into object 
                return hit
-           game.sector = jw
+           game.sector = bumpto
            proutn(crmshp())
            proutn(crmshp())
-           shoved = True
+            game.quad[w.i][w.j]=IHDOT
+            game.quad[bumpto.i][bumpto.j]=iquad
+            prout(_(" displaced by blast to Sector %s ") % bumpto)
+            for enemy in game.enemies:
+                enemy.kdist = enemy.kavgd = (game.sector-enemy.kloc).distance()
+            game.enemies.sort(lambda x, y: cmp(x.kdist, y.kdist))
+            return None
        elif iquad in (IHC, IHS, IHR, IHK): # Hit a regular enemy 
            # find the enemy 
            if iquad in (IHC, IHS) and withprob(0.05):
        elif iquad in (IHC, IHS, IHR, IHK): # Hit a regular enemy 
            # find the enemy 
            if iquad in (IHC, IHS) and withprob(0.05):
@@ -1337,30 +1172,26 @@ def torpedo(origin, bearing, dispersion, number, nburst):
                deadkl(w, iquad, w)
                return None
            proutn(crmena(True, iquad, "sector", w))
                deadkl(w, iquad, w)
                return None
            proutn(crmena(True, iquad, "sector", w))
-           # If enemy damaged but not destroyed, try to displace 
-           ang = track.angle + 2.5*(randreal()-0.5)
-           temp = math.fabs(math.sin(ang))
-           if math.fabs(math.cos(ang)) > temp:
-               temp = math.fabs(math.cos(ang))
-           xx = -math.sin(ang)/temp
-           yy = math.cos(ang)/temp
-           jw.i = int(w.i+xx+0.5)
-           jw.j = int(w.j+yy+0.5)
-            if not jw.valid_sector():
+            displacement = course(track.bearing+randreal(-2.4,2.4), distance=2**0.5)
+            displacement.next()
+            bumpto = displacement.sector()
+            if not bumpto.valid_sector():
                prout(_(" damaged but not destroyed."))
                return
                prout(_(" damaged but not destroyed."))
                return
-           if game.quad[jw.i][jw.j]==IHBLANK:
+           if game.quad[bumpto.i][bumpto.j] == IHBLANK:
                prout(_(" buffeted into black hole."))
                prout(_(" buffeted into black hole."))
-               deadkl(w, iquad, jw)
-               return None
-           if game.quad[jw.i][jw.j]!=IHDOT:
-               # can't move into object 
+               deadkl(w, iquad, bumpto)
+           if game.quad[bumpto.i][bumpto.j] != IHDOT:
                prout(_(" damaged but not destroyed."))
                prout(_(" damaged but not destroyed."))
-               return None
-           proutn(_(" damaged--"))
-           enemy.kloc = jw
-           shoved = True
-           break
+            else:
+                prout(_(" damaged-- displaced by blast to Sector %s ")%bumpto)
+                enemy.kloc = bumpto
+                game.quad[w.i][w.j]=IHDOT
+                game.quad[bumpto.i][bumpto.j]=iquad
+                for enemy in game.enemies:
+                    enemy.kdist = enemy.kavgd = (game.sector-enemy.kloc).distance()
+                game.enemies.sort(lambda x, y: cmp(x.kdist, y.kdist))
+            return None
        elif iquad == IHB: # Hit a base 
            skip(1)
            prout(_("***STARBASE DESTROYED.."))
        elif iquad == IHB: # Hit a base 
            skip(1)
            prout(_("***STARBASE DESTROYED.."))
@@ -1395,8 +1226,7 @@ def torpedo(origin, bearing, dispersion, number, nburst):
            if game.landed:
                # captain perishes on planet 
                finish(FDPLANET)
            if game.landed:
                # captain perishes on planet 
                finish(FDPLANET)
-           prout(_("You have just destroyed an inhabited planet."))
-           prout(_("Celebratory rallies are being held on the Klingon homeworld."))
+           prout(_("The torpedo destroyed an inhabited planet."))
            return None
        elif iquad == IHSTAR: # Hit a star 
            if withprob(0.9):
            return None
        elif iquad == IHSTAR: # Hit a star 
            if withprob(0.9):
@@ -1456,16 +1286,6 @@ def torpedo(origin, bearing, dispersion, number, nburst):
            skip(1)
            return None
        break
            skip(1)
            return None
        break
-    if curwnd!=message_window:
-       setwnd(message_window)
-    if shoved:
-       game.quad[w.i][w.j]=IHDOT
-       game.quad[jw.i][jw.j]=iquad
-       prout(_(" displaced by blast to Sector %s ") % jw)
-       for ll in range(len(game.enemies)):
-           game.enemies[ll].kdist = game.enemies[ll].kavgd = (game.sector-game.enemies[ll].kloc).distance()
-        game.enemies.sort(lambda x, y: cmp(x.kdist, y.kdist))
-       return None
     skip(1)
     prout(_("Torpedo missed."))
     return None;
     skip(1)
     prout(_("Torpedo missed."))
     return None;
@@ -2134,7 +1954,7 @@ def phasers():
     if ifast:
        skip(1)
        if no == 0:
     if ifast:
        skip(1)
        if no == 0:
-           if withprob(0.99):
+           if withprob(0.01):
                prout(_("Sulu-  \"Sir, the high-speed shield control has malfunctioned . . ."))
                prouts(_("         CLICK   CLICK   POP  . . ."))
                prout(_(" No response, sir!"))
                prout(_("Sulu-  \"Sir, the high-speed shield control has malfunctioned . . ."))
                prouts(_("         CLICK   CLICK   POP  . . ."))
                prout(_(" No response, sir!"))
@@ -2452,7 +2272,7 @@ def events():
                supercommander()
        elif evcode == FDSPROB: # Move deep space probe 
            schedule(FDSPROB, 0.01)
                supercommander()
        elif evcode == FDSPROB: # Move deep space probe 
            schedule(FDSPROB, 0.01)
-            if game.probe.next(grain=QUADSIZE):
+            if not game.probe.next():
                if not game.probe.quadrant().valid_quadrant() or \
                    game.state.galaxy[game.probe.quadrant().i][game.probe.quadrant().j].supernova:
                    # Left galaxy or ran into supernova
                if not game.probe.quadrant().valid_quadrant() or \
                    game.state.galaxy[game.probe.quadrant().i][game.probe.quadrant().j].supernova:
                    # Left galaxy or ran into supernova
@@ -2478,7 +2298,7 @@ def events():
                chp.stars = pdest.stars
                pdest.charted = True
            game.probe.moves -= 1 # One less to travel
                chp.stars = pdest.stars
                pdest.charted = True
            game.probe.moves -= 1 # One less to travel
-           if game.probe.moves == 0 and game.isarmed and pdest.stars:
+           if game.probe.arrived() and game.isarmed and pdest.stars:
                supernova(game.probe)           # fire in the hole!
                unschedule(FDSPROB)
                if game.state.galaxy[game.quadrant().i][game.quadrant().j].supernova: 
                supernova(game.probe)           # fire in the hole!
                unschedule(FDSPROB)
                if game.state.galaxy[game.quadrant().i][game.quadrant().j].supernova: 
@@ -2755,7 +2575,7 @@ def nova(nov):
     game.optime = course.time(warp=4)
     skip(1)
     prout(_("Force of nova displaces starship."))
     game.optime = course.time(warp=4)
     skip(1)
     prout(_("Force of nova displaces starship."))
-    imove(course, novapush=True)
+    imove(course, noattack=True)
     game.optime = course.time(warp=4)
     return
        
     game.optime = course.time(warp=4)
     return
        
@@ -3525,7 +3345,7 @@ def tracktorpedo(origin, w, step, i, n, iquad):
        if step == 1:
            if n != 1:
                skip(1)
        if step == 1:
            if n != 1:
                skip(1)
-               proutn(_("Track for %s torpedo number %d-  ") % (game.quad[origin.i][origin.j],i+1))
+               proutn(_("Track for torpedo number %d-  ") % (i+1))
            else:
                skip(1)
                proutn(_("Torpedo track- "))
            else:
                skip(1)
                proutn(_("Torpedo track- "))
@@ -3579,178 +3399,151 @@ def prstat(txt, data):
 
 # Code from moving.c begins here
 
 
 # Code from moving.c begins here
 
-def imove(course=None, novapush=False):
+def imove(course=None, noattack=False):
     "Movement execution for warp, impulse, supernova, and tractor-beam events."
     "Movement execution for warp, impulse, supernova, and tractor-beam events."
-    w = coord(); final = coord()
-    trbeam = False
+    w = coord()
 
 
-    def no_quad_change():
-        # No quadrant change -- compute new average enemy distances 
-        game.quad[game.sector.i][game.sector.j] = game.ship
-        if game.enemies:
+    def newquadrant(noattack):
+        # Leaving quadrant -- allow final enemy attack 
+        # Don't do it if being pushed by Nova 
+        if len(game.enemies) != 0 and not noattack:
+            newcnd()
             for enemy in game.enemies:
             for enemy in game.enemies:
-                finald = (w-enemy.kloc).distance()
+                finald = (w - enemy.kloc).distance()
                 enemy.kavgd = 0.5 * (finald + enemy.kdist)
                 enemy.kavgd = 0.5 * (finald + enemy.kdist)
-                enemy.kdist = finald
-            game.enemies.sort(lambda x, y: cmp(x.kdist, y.kdist))
-            if not game.state.galaxy[game.quadrant.i][game.quadrant.j].supernova:
+            # Stas Sergeev added the condition
+            # that attacks only happen if Klingons
+            # are present and your skill is good.
+            if game.skill > SKILL_GOOD and game.klhere > 0 and not game.state.galaxy[game.quadrant.i][game.quadrant.j].supernova:
                 attack(torps_ok=False)
                 attack(torps_ok=False)
-            for enemy in game.enemies:
-                enemy.kavgd = enemy.kdist
-        newcnd()
-        drawmaps(0)
-        setwnd(message_window)
-    w.i = w.j = 0
+            if game.alldone:
+                return
+        # check for edge of galaxy 
+        kinks = 0
+        while True:
+            kink = False
+            if course.final.i < 0:
+                course.final.i = -course.final.i
+                kink = True
+            if course.final.j < 0:
+                course.final.j = -course.final.j
+                kink = True
+            if course.final.i >= GALSIZE*QUADSIZE:
+                course.final.i = (GALSIZE*QUADSIZE*2) - course.final.i
+                kink = True
+            if course.final.j >= GALSIZE*QUADSIZE:
+                course.final.j = (GALSIZE*QUADSIZE*2) - course.final.j
+                kink = True
+            if kink:
+                kinks += 1
+            else:
+                break
+        if kinks:
+            game.nkinks += 1
+            if game.nkinks == 3:
+                # Three strikes -- you're out! 
+                finish(FNEG3)
+                return
+            skip(1)
+            prout(_("YOU HAVE ATTEMPTED TO CROSS THE NEGATIVE ENERGY BARRIER"))
+            prout(_("AT THE EDGE OF THE GALAXY.  THE THIRD TIME YOU TRY THIS,"))
+            prout(_("YOU WILL BE DESTROYED."))
+        # Compute final position in new quadrant 
+        if trbeam: # Don't bother if we are to be beamed 
+            return
+        game.quadrant = course.final.quadrant()
+        game.sector = course.final.sector()
+        skip(1)
+        prout(_("Entering Quadrant %s.") % game.quadrant)
+        game.quad[game.sector.i][game.sector.j] = game.ship
+        newqad()
+        if game.skill>SKILL_NOVICE:
+            attack(torps_ok=False)  
+
+    def check_collision(h):
+        iquad = game.quad[h.i][h.j]
+        if iquad != IHDOT:
+            # object encountered in flight path 
+            stopegy = 50.0*course.distance/game.optime
+            if iquad in (IHT, IHK, IHC, IHS, IHR, IHQUEST):
+                for enemy in game.enemies:
+                    if enemy.kloc == game.sector:
+                        break
+                collision(rammed=False, enemy=enemy)
+                return True
+            elif iquad == IHBLANK:
+                skip(1)
+                prouts(_("***RED ALERT!  RED ALERT!"))
+                skip(1)
+                proutn("***" + crmshp())
+                proutn(_(" pulled into black hole at Sector %s") % h)
+                # Getting pulled into a black hole was certain
+                # death in Almy's original.  Stas Sergeev added a
+                # possibility that you'll get timewarped instead.
+                n=0
+                for m in range(NDEVICES):
+                    if game.damage[m]>0: 
+                        n += 1
+                probf=math.pow(1.4,(game.energy+game.shield)/5000.0-1.0)*math.pow(1.3,1.0/(n+1)-1.0)
+                if (game.options & OPTION_BLKHOLE) and withprob(1-probf): 
+                    timwrp()
+                else: 
+                    finish(FHOLE)
+                return True
+            else:
+                # something else 
+                skip(1)
+                proutn(crmshp())
+                if iquad == IHWEB:
+                    prout(_(" encounters Tholian web at %s;") % h)
+                else:
+                    prout(_(" blocked by object at %s;") % h)
+                proutn(_("Emergency stop required "))
+                prout(_("%2d units of energy.") % int(stopegy))
+                game.energy -= stopegy
+                if game.energy <= 0:
+                    finish(FNRG)
+                return True
+        return False
+
+    trbeam = False
     if game.inorbit:
        prout(_("Helmsman Sulu- \"Leaving standard orbit.\""))
        game.inorbit = False
     if game.inorbit:
        prout(_("Helmsman Sulu- \"Leaving standard orbit.\""))
        game.inorbit = False
-    angle = ((15.0 - course.bearing) * 0.5235988)
-    deltax = -math.sin(angle)
-    deltay = math.cos(angle)
-    if math.fabs(deltax) > math.fabs(deltay):
-       bigger = math.fabs(deltax)
-    else:
-       bigger = math.fabs(deltay)
-    deltay /= bigger
-    deltax /= bigger
     # If tractor beam is to occur, don't move full distance 
     if game.state.date+game.optime >= scheduled(FTBEAM):
        trbeam = True
        game.condition = "red"
        course.distance = course.distance*(scheduled(FTBEAM)-game.state.date)/game.optime + 0.1
        game.optime = scheduled(FTBEAM) - game.state.date + 1e-5
     # If tractor beam is to occur, don't move full distance 
     if game.state.date+game.optime >= scheduled(FTBEAM):
        trbeam = True
        game.condition = "red"
        course.distance = course.distance*(scheduled(FTBEAM)-game.state.date)/game.optime + 0.1
        game.optime = scheduled(FTBEAM) - game.state.date + 1e-5
-    # Move within the quadrant 
+    # Move out
     game.quad[game.sector.i][game.sector.j] = IHDOT
     game.quad[game.sector.i][game.sector.j] = IHDOT
-    x = game.sector.i
-    y = game.sector.j
-    n = int(10.0*course.distance*bigger+0.5)
-    if n > 0:
-       for m in range(1, n+1):
-            x += deltax
-            y += deltay
-           w.i = int(round(x))
-           w.j = int(round(y))
-           if not w.valid_sector():
-               # Leaving quadrant -- allow final enemy attack 
-               # Don't do it if being pushed by Nova 
-               if len(game.enemies) != 0 and not novapush:
-                   newcnd()
-                   for enemy in game.enemies:
-                       finald = (w - enemy.kloc).distance()
-                       enemy.kavgd = 0.5 * (finald + enemy.kdist)
-                   #
-                   # Stas Sergeev added the condition
-                   # that attacks only happen if Klingons
-                   # are present and your skill is good.
-                   # 
-                   if game.skill > SKILL_GOOD and game.klhere > 0 and not game.state.galaxy[game.quadrant.i][game.quadrant.j].supernova:
-                       attack(torps_ok=False)
-                   if game.alldone:
-                       return
-               # compute final position -- new quadrant and sector 
-               x = (QUADSIZE*game.quadrant.i)+game.sector.i
-               y = (QUADSIZE*game.quadrant.j)+game.sector.j
-               w.i = int(round(x+QUADSIZE*course.distance*bigger*deltax))
-               w.j = int(round(y+QUADSIZE*course.distance*bigger*deltay))
-               # check for edge of galaxy 
-               kinks = 0
-                while True:
-                   kink = False
-                   if w.i < 0:
-                       w.i = -w.i
-                       kink = True
-                   if w.j < 0:
-                       w.j = -w.j
-                       kink = True
-                   if w.i >= GALSIZE*QUADSIZE:
-                       w.i = (GALSIZE*QUADSIZE*2) - w.i
-                       kink = True
-                   if w.j >= GALSIZE*QUADSIZE:
-                       w.j = (GALSIZE*QUADSIZE*2) - w.j
-                       kink = True
-                   if kink:
-                       kinks += 1
-                    else:
-                        break
-               if kinks:
-                   game.nkinks += 1
-                   if game.nkinks == 3:
-                       # Three strikes -- you're out! 
-                       finish(FNEG3)
-                       return
-                   skip(1)
-                   prout(_("YOU HAVE ATTEMPTED TO CROSS THE NEGATIVE ENERGY BARRIER"))
-                   prout(_("AT THE EDGE OF THE GALAXY.  THE THIRD TIME YOU TRY THIS,"))
-                   prout(_("YOU WILL BE DESTROYED."))
-               # Compute final position in new quadrant 
-               if trbeam: # Don't bother if we are to be beamed 
-                   return
-               game.quadrant.i = w.i/QUADSIZE
-               game.quadrant.j = w.j/QUADSIZE
-               game.sector.i = w.i - (QUADSIZE*game.quadrant.i)
-               game.sector.j = w.j - (QUADSIZE*game.quadrant.j)
-               skip(1)
-               prout(_("Entering Quadrant %s.") % game.quadrant)
-               game.quad[game.sector.i][game.sector.j] = game.ship
-               newqad()
-               if game.skill>SKILL_NOVICE:
-                   attack(torps_ok=False)  
-               return
-           iquad = game.quad[w.i][w.j]
-           if iquad != IHDOT:
-               # object encountered in flight path 
-               stopegy = 50.0*course.dist/game.optime
-               course.distance = (game.sector - w).distance() / (QUADSIZE * 1.0)
-                if iquad in (IHT, IHK, IHC, IHS, IHR, IHQUEST):
-                   game.sector = w
-                    for enemy in game.enemies:
-                        if enemy.kloc == game.sector:
-                            break
-                   collision(rammed=False, enemy=enemy)
-                   final = game.sector
-               elif iquad == IHBLANK:
-                   skip(1)
-                   prouts(_("***RED ALERT!  RED ALERT!"))
-                   skip(1)
-                   proutn("***" + crmshp())
-                   proutn(_(" pulled into black hole at Sector %s") % w)
-                   # Getting pulled into a black hole was certain
-                   # death in Almy's original.  Stas Sergeev added a
-                   # possibility that you'll get timewarped instead.
-                   n=0
-                   for m in range(NDEVICES):
-                       if game.damage[m]>0: 
-                           n += 1
-                   probf=math.pow(1.4,(game.energy+game.shield)/5000.0-1.0)*math.pow(1.3,1.0/(n+1)-1.0)
-                   if (game.options & OPTION_BLKHOLE) and withprob(1-probf): 
-                       timwrp()
-                   else: 
-                       finish(FHOLE)
-                   return
-               else:
-                   # something else 
-                   skip(1)
-                   proutn(crmshp())
-                   if iquad == IHWEB:
-                       prout(_(" encounters Tholian web at %s;") % w)
-                   else:
-                       prout(_(" blocked by object at %s;") % w)
-                   proutn(_("Emergency stop required "))
-                   prout(_("%2d units of energy.") % int(stopegy))
-                   game.energy -= stopegy
-                   final.i = int(round(deltax))
-                   final.j = int(round(deltay))
-                   game.sector = final
-                   if game.energy <= 0:
-                       finish(FNRG)
-                       return
-                # We're here!
-               no_quad_change()
-                return
-       course.distance = (game.sector - w).distance() / (QUADSIZE * 1.0)
-       game.sector = w
-    final = game.sector
-    no_quad_change()
+    for m in range(course.moves):
+        course.next()
+        w = course.sector()
+        if course.origin.quadrant() != course.location.quadrant():
+            newquadrant(noattack)
+            break
+        elif check_collision(w):
+            print "Collision detected"
+            break
+        else:
+            game.sector = w
+    # We're in destination quadrant -- compute new average enemy distances
+    game.quad[game.sector.i][game.sector.j] = game.ship
+    if game.enemies:
+        for enemy in game.enemies:
+            finald = (w-enemy.kloc).distance()
+            enemy.kavgd = 0.5 * (finald + enemy.kdist)
+            enemy.kdist = finald
+        game.enemies.sort(lambda x, y: cmp(x.kdist, y.kdist))
+        if not game.state.galaxy[game.quadrant.i][game.quadrant.j].supernova:
+            attack(torps_ok=False)
+        for enemy in game.enemies:
+            enemy.kavgd = enemy.kdist
+    newcnd()
+    drawmaps(0)
+    setwnd(message_window)
     return
 
 def dock(verbose):
     return
 
 def dock(verbose):
@@ -3933,34 +3726,41 @@ class course:
     def __init__(self, bearing, distance, origin=None): 
         self.distance = distance
         self.bearing = bearing
     def __init__(self, bearing, distance, origin=None): 
         self.distance = distance
         self.bearing = bearing
+        if origin is None:
+            self.origin = cartesian(game.quadrant, game.sector)
+        else:
+            self.origin = origin
         # The bearing() code we inherited from FORTRAN is actually computing
         # clockface directions!
         if self.bearing < 0.0:
             self.bearing += 12.0
         self.angle = ((15.0 - self.bearing) * 0.5235988)
         if origin is None:
         # The bearing() code we inherited from FORTRAN is actually computing
         # clockface directions!
         if self.bearing < 0.0:
             self.bearing += 12.0
         self.angle = ((15.0 - self.bearing) * 0.5235988)
         if origin is None:
-            self.location = cartesian(game.quadrant, game.sector)
+            self.origin = cartesian(game.quadrant, game.sector)
         else:
         else:
-            self.location = cartesian(game.quadrant, origin)
+            self.origin = cartesian(game.quadrant, origin)
         self.increment = coord(-math.sin(self.angle), math.cos(self.angle))
         bigger = max(abs(self.increment.i), abs(self.increment.j))
         self.increment /= bigger
         self.increment = coord(-math.sin(self.angle), math.cos(self.angle))
         bigger = max(abs(self.increment.i), abs(self.increment.j))
         self.increment /= bigger
-        self.moves = 10*self.distance*bigger +0.5
-    def next(self, grain=1):
+        self.moves = int(round(10*self.distance*bigger))
+        self.reset()
+        self.final = (self.location + self.moves*self.increment).roundtogrid()
+    def reset(self):
+        self.location = self.origin
+        self.step = 0
+    def arrived(self):
+        return self.location.roundtogrid() == self.final
+    def next(self):
         "Next step on course."
         "Next step on course."
-        self.moves -=1
+        self.step += 1
         self.nextlocation = self.location + self.increment
         self.nextlocation = self.location + self.increment
-        oldloc = (self.location/grain).roundtogrid()
-        newloc = (self.nextlocation/grain).roundtogrid()
+        samequad = (self.location.quadrant() == self.nextlocation.quadrant())
         self.location = self.nextlocation
         self.location = self.nextlocation
-        if newloc != oldloc:
-            return True
-        else:
-            return False
+        return samequad
     def quadrant(self):
     def quadrant(self):
-        return (self.location / QUADSIZE).roundtogrid()
+        return self.location.quadrant()
     def sector(self):
     def sector(self):
-        return coord(int(round(self.location.i)) % QUADSIZE, int(round(self.location.j)) % QUADSIZE)
+        return self.location.sector()
     def power(self, warp):
        return self.distance*(warp**3)*(game.shldup+1)
     def time(self, warp):
     def power(self, warp):
        return self.distance*(warp**3)*(game.shldup+1)
     def time(self, warp):
@@ -4004,7 +3804,7 @@ def impulse():
        if ja() == False:
            return
     # Activate impulse engines and pay the cost 
        if ja() == False:
            return
     # Activate impulse engines and pay the cost 
-    imove(course, novapush=False)
+    imove(course, noattack=False)
     game.ididit = True
     if game.alldone:
        return
     game.ididit = True
     if game.alldone:
        return
@@ -4076,12 +3876,12 @@ def warp(course, involuntary):
     if game.warpfac > 6.0:
        # Decide if engine damage will occur
         # ESR: Seems wrong. Probability of damage goes *down* with distance? 
     if game.warpfac > 6.0:
        # Decide if engine damage will occur
         # ESR: Seems wrong. Probability of damage goes *down* with distance? 
-       prob = course.dist*(6.0-game.warpfac)**2/66.666666666
+       prob = course.distance*(6.0-game.warpfac)**2/66.666666666
        if prob > randreal():
            blooey = True
            course.distance = randreal(course.distance)
        # Decide if time warp will occur 
        if prob > randreal():
            blooey = True
            course.distance = randreal(course.distance)
        # Decide if time warp will occur 
-       if 0.5*course.dist*math.pow(7.0,game.warpfac-10.0) > randreal():
+       if 0.5*course.distance*math.pow(7.0,game.warpfac-10.0) > randreal():
            twarp = True
        if idebug and game.warpfac==10 and not twarp:
            blooey = False
            twarp = True
        if idebug and game.warpfac==10 and not twarp:
            blooey = False
@@ -4091,30 +3891,17 @@ def warp(course, involuntary):
        if blooey or twarp:
            # If time warp or engine damage, check path 
            # If it is obstructed, don't do warp or damage 
        if blooey or twarp:
            # If time warp or engine damage, check path 
            # If it is obstructed, don't do warp or damage 
-           angle = ((15.0-course.bearing)*0.5235998)
-           deltax = -math.sin(angle)
-           deltay = math.cos(angle)
-           if math.fabs(deltax) > math.fabs(deltay):
-               bigger = math.fabs(deltax)
-           else:
-               bigger = math.fabs(deltay)
-           deltax /= bigger
-           deltay /= bigger
-           n = 10.0 * course.distance * bigger +0.5
-           x = game.sector.i
-           y = game.sector.j
-           for l in range(1, n+1):
-               x += deltax
-               ix = x + 0.5
-               y += deltay
-               iy = y +0.5
-               if not coord(ix, iy).valid_sector():
-                   break
-               if game.quad[ix][iy] != IHDOT:
+            for m in range(course.moves):
+                course.next()
+                w = course.sector()
+                if not w.valid_sector():
+                    break
+               if game.quad[w.i][w.j] != IHDOT:
                    blooey = False
                    twarp = False
                    blooey = False
                    twarp = False
+            course.reset()
     # Activate Warp Engines and pay the cost 
     # Activate Warp Engines and pay the cost 
-    imove(course, novapush=False)
+    imove(course, noattack=False)
     if game.alldone:
        return
     game.energy -= course.power(game.warpfac)
     if game.alldone:
        return
     game.energy -= course.power(game.warpfac)
@@ -4214,7 +4001,7 @@ def atover(igrab):
            proutn(_("The %s has stopped in a quadrant containing") % crmshp())
            prouts(_("   a supernova."))
            skip(2)
            proutn(_("The %s has stopped in a quadrant containing") % crmshp())
            prouts(_("   a supernova."))
            skip(2)
-       proutn(_("***Emergency automatic override attempts to hurl ")+crmshp())
+       prout(_("***Emergency automatic override attempts to hurl ")+crmshp())
        prout(_("safely out of quadrant."))
        if not damaged(DRADIO):
            game.state.galaxy[game.quadrant.i][game.quadrant.j].charted = True
        prout(_("safely out of quadrant."))
        if not damaged(DRADIO):
            game.state.galaxy[game.quadrant.i][game.quadrant.j].charted = True
@@ -4228,14 +4015,12 @@ def atover(igrab):
        prout(_("Warp factor set to %d") % int(game.warpfac))
        power = 0.75*game.energy
        dist = power/(game.warpfac*game.warpfac*game.warpfac*(game.shldup+1))
        prout(_("Warp factor set to %d") % int(game.warpfac))
        power = 0.75*game.energy
        dist = power/(game.warpfac*game.warpfac*game.warpfac*(game.shldup+1))
-       distreq = randreal(math.sqrt(2))
-       if distreq < game.dist:
-           dist = distreq
-        course = course(bearing=randreal(12), distance=dist)   # How dumb!
-       game.optime = course.time()
+       dist = max(dist, randreal(math.sqrt(2)))
+        bugout = course(bearing=randreal(12), distance=dist)   # How dumb!
+       game.optime = bugout.time(game.warpfac)
        game.justin = False
        game.inorbit = False
        game.justin = False
        game.inorbit = False
-       warp(course, involuntary=True)
+       warp(bugout, involuntary=True)
        if not game.justin:
            # This is bad news, we didn't leave quadrant. 
            if game.alldone:
        if not game.justin:
            # This is bad news, we didn't leave quadrant. 
            if game.alldone:
@@ -5192,11 +4977,8 @@ def status(req=0):
     if not req or req == 2:
        if game.condition != "docked":
            newcnd()
     if not req or req == 2:
        if game.condition != "docked":
            newcnd()
-        dam = 0
-       for t in range(NDEVICES):
-           if game.damage[t]>0: 
-               dam += 1
-       prstat(_("Condition"), _("%s, %i DAMAGES") % (game.condition.upper(), dam))
+       prstat(_("Condition"), _("%s, %i DAMAGES") % \
+               (game.condition.upper(), sum(map(lambda x: x > 0, game.damage))))
     if not req or req == 3:
        prstat(_("Position"), "%s , %s" % (game.quadrant, game.sector))
     if not req or req == 4:
     if not req or req == 3:
        prstat(_("Position"), "%s , %s" % (game.quadrant, game.sector))
     if not req or req == 4:
@@ -5229,7 +5011,7 @@ def status(req=0):
        prstat(_("Shields"), s+data)
     if not req or req == 9:
         prstat(_("Klingons Left"), "%d" \
        prstat(_("Shields"), s+data)
     if not req or req == 9:
         prstat(_("Klingons Left"), "%d" \
-               % (game.state.remkl + len(game.state.kcmdr) + game.state.nscrem))
+               % (game.state.remkl+len(game.state.kcmdr)+game.state.nscrem))
     if not req or req == 10:
        if game.options & OPTION_WORLDS:
            plnet = game.state.galaxy[game.quadrant.i][game.quadrant.j].planet
     if not req or req == 10:
        if game.options & OPTION_WORLDS:
            plnet = game.state.galaxy[game.quadrant.i][game.quadrant.j].planet
@@ -5349,7 +5131,7 @@ def eta():
        prout(_("Captain, certainly you can give me one of these."))
     while True:
        scanner.chew()
        prout(_("Captain, certainly you can give me one of these."))
     while True:
        scanner.chew()
-       ttime = (10.0*game.dist)/twarp**2
+       ttime = (10.0*dist)/twarp**2
        tpower = dist*twarp*twarp*twarp*(game.shldup+1)
        if tpower >= game.energy:
            prout(_("Insufficient energy, sir."))
        tpower = dist*twarp*twarp*twarp*(game.shldup+1)
        if tpower >= game.energy:
            prout(_("Insufficient energy, sir."))