Imprive documentation, fix a typo, add the hacking guide.
[super-star-trek.git] / src / sst.py
index a83d6caaf8a53118ef0d039a7c6e61445936d2ad..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
 
@@ -201,10 +33,6 @@ FOREVER     = 1e30
 MAXBURST       = 3
 MINCMDR        = 10
 
 MAXBURST       = 3
 MINCMDR        = 10
 
-# These functions hide the difference between 0-origin and 1-origin addressing.
-def VALID_QUADRANT(x, y):      return ((x)>=0 and (x)<GALSIZE and (y)>=0 and (y)<GALSIZE)
-def VALID_SECTOR(x, y):        return ((x)>=0 and (x)<QUADSIZE and (y)>=0 and (y)<QUADSIZE)
-
 # How to represent features
 IHR = 'R',
 IHK = 'K',
 # How to represent features
 IHR = 'R',
 IHK = 'K',
@@ -232,6 +60,10 @@ class coord:
     def __init__(self, x=None, y=None):
         self.i = x
         self.j = y
     def __init__(self, x=None, y=None):
         self.i = x
         self.j = y
+    def valid_quadrant(self):
+        return 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
     def invalidate(self):
         self.i = self.j = None
     def is_valid(self):
     def invalidate(self):
         self.i = self.j = None
     def is_valid(self):
@@ -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"
@@ -602,7 +436,7 @@ def randreal(*args):
 
 def welcoming(iq):
     "Would this quadrant welcome another Klingon?"
 
 def welcoming(iq):
     "Would this quadrant welcome another Klingon?"
-    return VALID_QUADRANT(iq.i,iq.j) and \
+    return iq.valid_quadrant() and \
        not game.state.galaxy[iq.i][iq.j].supernova and \
        game.state.galaxy[iq.i][iq.j].klingons < MAXKLQUAD
 
        not game.state.galaxy[iq.i][iq.j].supernova and \
        game.state.galaxy[iq.i][iq.j].klingons < MAXKLQUAD
 
@@ -1261,63 +1095,60 @@ def collision(rammed, enemy):
        finish(FWON)
     return
 
        finish(FWON)
     return
 
-def torpedo(origin, course, dispersion, number, nburst):
+def torpedo(origin, bearing, dispersion, number, nburst):
     "Let a photon torpedo fly" 
     if not damaged(DSRSENS) or game.condition=="docked":
        setwnd(srscan_window)
     else: 
        setwnd(message_window)
     "Let a photon torpedo fly" 
     if not damaged(DSRSENS) or game.condition=="docked":
        setwnd(srscan_window)
     else: 
        setwnd(message_window)
-    shoved = False
-    ac = course + 0.25*dispersion
-    angle = (15.0-ac)*0.5235988
-    bullseye = (15.0 - course)*0.5235988
-    delta = coord(-math.sin(angle), math.cos(angle))          
-    bigger = max(abs(delta.i), abs(delta.j))
-    delta /= bigger
-    w = coord(0, 0); jw = coord(0, 0)
-    ungridded = copy.copy(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)) 
+    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):
-       ungridded += delta
-       w = ungridded.roundtogrid()
-       if not VALID_SECTOR(w.i, w.j):
+        if not track.next(): break
+        w = track.sector()
+       if not w.valid_sector():
            break
        iquad=game.quad[w.i][w.j]
        tracktorpedo(origin, w, step, number, nburst, iquad)
        if iquad==IHDOT:
            continue
        # hit something 
            break
        iquad=game.quad[w.i][w.j]
        tracktorpedo(origin, w, step, number, nburst, iquad)
        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 = 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 VALID_SECTOR(jw.i, jw.j):
+            # 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):
@@ -1329,7 +1160,7 @@ def torpedo(origin, course, dispersion, number, nburst):
                    break
            kp = math.fabs(enemy.kpower)
            h1 = 700.0 + randrange(100) - \
                    break
            kp = math.fabs(enemy.kpower)
            h1 = 700.0 + randrange(100) - \
-               1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-angle))
+               1000.0 * (w-origin).distance() * math.fabs(math.sin(bullseye-track.angle))
            h1 = math.fabs(h1)
            if kp < h1:
                h1 = kp
            h1 = math.fabs(h1)
            if kp < h1:
                h1 = kp
@@ -1341,30 +1172,26 @@ def torpedo(origin, course, 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 = 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 VALID_SECTOR(jw.i, jw.j):
+            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.."))
@@ -1399,8 +1226,7 @@ def torpedo(origin, course, 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):
@@ -1460,16 +1286,6 @@ def torpedo(origin, course, 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;
@@ -1701,7 +1517,7 @@ def deadkl(w, type, mv):
 
 def targetcheck(w):
     "Return None if target is invalid, otherwise return a course angle."
 
 def targetcheck(w):
     "Return None if target is invalid, otherwise return a course angle."
-    if not VALID_SECTOR(w.i, w.j):
+    if not w.valid_sector():
        huh()
        return None
     delta = coord()
        huh()
        return None
     delta = coord()
@@ -2138,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!"))
@@ -2456,36 +2272,36 @@ 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.nextquad():
-               if not VALID_QUADRANT(game.probe.loc.i, game.probe.loc.j) or \
-                   game.state.galaxy[game.probe.loc.i][game.probe.loc.j].supernova:
+            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 communicating():
                        announce()
                        skip(1)
                        proutn(_("Lt. Uhura-  \"The deep space probe "))
                    # Left galaxy or ran into supernova
                     if communicating():
                        announce()
                        skip(1)
                        proutn(_("Lt. Uhura-  \"The deep space probe "))
-                       if not VALID_QUADRANT(game.probe.loc.i, game.probe.loc.j):
-                           proutn(_("has left the galaxy.\""))
+                       if not game.probe.quadrant().valid_quadrant():
+                           prout(_("has left the galaxy.\""))
                        else:
                        else:
-                           proutn(_("is no longer transmitting.\""))
+                           prout(_("is no longer transmitting.\""))
                    unschedule(FDSPROB)
                    continue
                 if communicating():
                    #announce()
                    skip(1)
                    unschedule(FDSPROB)
                    continue
                 if communicating():
                    #announce()
                    skip(1)
-                   prout(_("Lt. Uhura-  \"The deep space probe is now in Quadrant %s.\"") % game.probe.loc)
-           pdest = game.state.galaxy[game.probe.loc.i][game.probe.loc.j]
+                   prout(_("Lt. Uhura-  \"The deep space probe is now in Quadrant %s.\"") % game.probe.quadrant())
+           pdest = game.state.galaxy[game.probe.quadrant().i][game.probe.quadrant().j]
            if communicating():
            if communicating():
-               chp = game.state.chart[game.probe.loc.i][game.probe.loc.j]
+               chp = game.state.chart[game.probe.quadrant().i][game.probe.quadrant().j]
                chp.klingons = pdest.klingons
                chp.starbase = pdest.starbase
                chp.stars = pdest.stars
                pdest.charted = True
            game.probe.moves -= 1 # One less to travel
                chp.klingons = pdest.klingons
                chp.starbase = pdest.starbase
                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)
                supernova(game.probe)           # fire in the hole!
                unschedule(FDSPROB)
-               if game.state.galaxy[game.quadrant.i][game.quadrant.j].supernova: 
+               if game.state.galaxy[game.quadrant().i][game.quadrant().j].supernova: 
                    return
        elif evcode == FDISTR: # inhabited system issues distress call 
            unschedule(FDISTR)
                    return
        elif evcode == FDISTR: # inhabited system issues distress call 
            unschedule(FDISTR)
@@ -2548,14 +2364,15 @@ def events():
                continue                # full right now 
            # reproduce one Klingon 
            w = ev.quadrant
                continue                # full right now 
            # reproduce one Klingon 
            w = ev.quadrant
+            m = coord()
            if game.klhere >= MAXKLQUAD:
                 try:
                     # this quadrant not ok, pick an adjacent one 
            if game.klhere >= MAXKLQUAD:
                 try:
                     # this quadrant not ok, pick an adjacent one 
-                    for i in range(w.i - 1, w.i + 2):
-                        for j in range(w.j - 1, w.j + 2):
-                            if not VALID_QUADRANT(i, j):
+                    for m.i in range(w.i - 1, w.i + 2):
+                        for m.j in range(w.j - 1, w.j + 2):
+                            if not m.valid_quadrant():
                                 continue
                                 continue
-                            q = game.state.galaxy[w.i][w.j]
+                            q = game.state.galaxy[m.i][m.j]
                             # check for this quad ok (not full & no snova) 
                             if q.klingons >= MAXKLQUAD or q.supernova:
                                 continue
                             # check for this quad ok (not full & no snova) 
                             if q.klingons >= MAXKLQUAD or q.supernova:
                                 continue
@@ -2563,7 +2380,7 @@ def events():
                     else:
                         continue       # search for eligible quadrant failed
                 except "FOUNDIT":
                     else:
                         continue       # search for eligible quadrant failed
                 except "FOUNDIT":
-                    w.i = i; w.j = j
+                    w = m
            # deliver the child 
            game.state.remkl += 1
            q.klingons += 1
            # deliver the child 
            game.state.remkl += 1
            q.klingons += 1
@@ -2572,7 +2389,6 @@ def events():
                game.enemies.append(newkling())
            # recompute time left
             game.recompute()
                game.enemies.append(newkling())
            # recompute time left
             game.recompute()
-           # report the disaster if we can 
            if communicating():
                if game.quadrant == w:
                    prout(_("Spock- sensors indicate the Klingons have"))
            if communicating():
                if game.quadrant == w:
                    prout(_("Spock- sensors indicate the Klingons have"))
@@ -2658,7 +2474,7 @@ def nova(nov):
                 if offset.j==0 and offset.i==0:
                     continue
                 neighbor = start + offset
                 if offset.j==0 and offset.i==0:
                     continue
                 neighbor = start + offset
-                if not VALID_SECTOR(neighbor.j, neighbor.i):
+                if not neighbor.valid_sector():
                     continue
                 iquad = game.quad[neighbor.i][neighbor.j]
                 # Empty space ends reaction
                     continue
                 iquad = game.quad[neighbor.i][neighbor.j]
                 # Empty space ends reaction
@@ -2730,7 +2546,7 @@ def nova(nov):
                         break
                     newc = neighbor + neighbor - hits[mm]
                     proutn(crmena(True, iquad, "sector", neighbor) + _(" damaged"))
                         break
                     newc = neighbor + neighbor - hits[mm]
                     proutn(crmena(True, iquad, "sector", neighbor) + _(" damaged"))
-                    if not VALID_SECTOR(newc.i, newc.j):
+                    if not newc.valid_sector():
                         # can't leave quadrant 
                         skip(1)
                         break
                         # can't leave quadrant 
                         skip(1)
                         break
@@ -2755,12 +2571,12 @@ def nova(nov):
        dist = 0.0
     if dist == 0.0:
        return
        dist = 0.0
     if dist == 0.0:
        return
-    course = course(distance=dist, bearing=direc, warp=4)
-    game.optime = course.time()
+    course = course(bearing=direc, distance=dist)
+    game.optime = course.time(warp=4)
     skip(1)
     prout(_("Force of nova displaces starship."))
     skip(1)
     prout(_("Force of nova displaces starship."))
-    imove(course, novapush=True)
-    game.optime = course.time()
+    imove(course, noattack=True)
+    game.optime = course.time(warp=4)
     return
        
 def supernova(w):
     return
        
 def supernova(w):
@@ -3529,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- "))
@@ -3583,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 VALID_SECTOR(w.i, w.j):
-               # 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):
@@ -3794,6 +3583,14 @@ def dock(verbose):
 # because it involves giving x and y motions, yet the coordinates
 # are always displayed y - x, where +y is downward!
 
 # because it involves giving x and y motions, yet the coordinates
 # are always displayed y - x, where +y is downward!
 
+def cartesian(loc1=None, loc2=None):
+    if loc1 is None:
+        return game.quadrant * QUADSIZE + game.sector
+    elif loc2 is None:
+        return game.quadrant * QUADSIZE + loc1
+    else:
+        return loc1 * QUADSIZE + loc2
+
 def getcourse(isprobe):
     "Get a course and distance from the user."
     key = 0
 def getcourse(isprobe):
     "Get a course and distance from the user."
     key = 0
@@ -3842,6 +3639,7 @@ def getcourse(isprobe):
                prout(_("(Manual movement assumed.)"))
            navmode = "manual"
            break
                prout(_("(Manual movement assumed.)"))
            navmode = "manual"
            break
+    delta = coord()
     if navmode == "automatic":
        while key == "IHEOL":
            if isprobe:
     if navmode == "automatic":
        while key == "IHEOL":
            if isprobe:
@@ -3885,7 +3683,7 @@ def getcourse(isprobe):
                dsect.i = xi
                dsect.j = xj
            itemp = "normal"
                dsect.i = xi
                dsect.j = xj
            itemp = "normal"
-       if not VALID_QUADRANT(dquad.i,dquad.j) or not VALID_SECTOR(dsect.i,dsect.j):
+       if not dquad.valid_quadrant() or not dsect.valid_sector():
            huh()
            raise TrekError
        skip(1)
            huh()
            raise TrekError
        skip(1)
@@ -3896,7 +3694,6 @@ def getcourse(isprobe):
            else:
                prout(_("Ensign Chekov- \"Course laid in, Captain.\""))
         # the actual deltas get computed here
            else:
                prout(_("Ensign Chekov- \"Course laid in, Captain.\""))
         # the actual deltas get computed here
-        delta = coord()
        delta.j = dquad.j-game.quadrant.j + (dsect.j-game.sector.j)/(QUADSIZE*1.0)
        delta.i = game.quadrant.i-dquad.i + (game.sector.i-dsect.i)/(QUADSIZE*1.0)
     else: # manual 
        delta.j = dquad.j-game.quadrant.j + (dsect.j-game.sector.j)/(QUADSIZE*1.0)
        delta.i = game.quadrant.i-dquad.i + (game.sector.i-dsect.i)/(QUADSIZE*1.0)
     else: # manual 
@@ -3923,44 +3720,51 @@ def getcourse(isprobe):
        skip(1)
        prout(_("Helmsman Sulu- \"Aye, Sir.\""))
     scanner.chew()
        skip(1)
        prout(_("Helmsman Sulu- \"Aye, Sir.\""))
     scanner.chew()
-    return course(delta.distance(), delta.bearing(), isprobe=isprobe)
+    return course(bearing=delta.bearing(), distance=delta.distance())
 
 class course:
 
 class course:
-    # Eventually, we want to consolidate all course compuation here,
-    # including for torpedos and the Enterprise.
-    def __init__(self, distance, bearing, warp=None, isprobe=False): 
-        # Course actually laid in -- thisis straight from the old getcd().
+    def __init__(self, bearing, distance, origin=None): 
         self.distance = distance
         self.bearing = bearing
         self.distance = distance
         self.bearing = bearing
-        self.warp = warp or game.warpfac
-        self.isprobe = isprobe
-        # This odd relic suggests that the bearing() code we inherited from
-        # FORTRAN is actually computing clockface directions.
+        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
         if self.bearing < 0.0:
             self.bearing += 12.0
-        # This code was moved from the probe() routine
-        if isprobe:
-            angle = ((15.0 - self.bearing) * 0.5235988)
-            self.increment = coord(-math.sin(angle), math.cos(angle))
-            bigger = max(abs(self.increment.i), abs(self.increment.j))
-            self.increment /= bigger
-            self.location = coord(game.quadrant.i*QUADSIZE + game.sector.i, 
-                               game.quadrant.j*QUADSIZE + game.sector.j)
-            self.loc = copy.copy(game.quadrant)
-            self.moves = 10.0*self.distance*bigger +0.5
-    def power(self):
-       return self.distance*(self.warp**3)*(game.shldup+1)
-    def time(self):
-        return 10.0*self.distance/self.warp**2
-    def nextquad(self):
-        "Next location on course, at quadrant granularity."
-        self.location += self.increment
-        newloc = (self.location / float(QUADSIZE)).trunctogrid()
-        if not newloc == self.loc:
-            self.loc = newloc
-            return True
+        self.angle = ((15.0 - self.bearing) * 0.5235988)
+        if origin is None:
+            self.origin = cartesian(game.quadrant, game.sector)
         else:
         else:
-            return False
+            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.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."
+        self.step += 1
+        self.nextlocation = self.location + self.increment
+        samequad = (self.location.quadrant() == self.nextlocation.quadrant())
+        self.location = self.nextlocation
+        return samequad
+    def quadrant(self):
+        return self.location.quadrant()
+    def sector(self):
+        return self.location.sector()
+    def power(self, warp):
+       return self.distance*(warp**3)*(game.shldup+1)
+    def time(self, warp):
+        return 10.0*self.distance/warp**2
 
 def impulse():
     "Move under impulse power."
 
 def impulse():
     "Move under impulse power."
@@ -4000,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
@@ -4036,7 +3840,7 @@ def warp(course, involuntary):
        # Make sure starship has enough energy for the trip
         # Note: this formula is slightly different from the C version,
         # and lets you skate a bit closer to the edge.
        # Make sure starship has enough energy for the trip
         # Note: this formula is slightly different from the C version,
         # and lets you skate a bit closer to the edge.
-       if course.power() >= game.energy:
+       if course.power(game.warpfac) >= game.energy:
            # Insufficient power for trip 
            game.ididit = False
            skip(1)
            # Insufficient power for trip 
            game.ididit = False
            skip(1)
@@ -4056,7 +3860,7 @@ def warp(course, involuntary):
                prout(_("We haven't the energy to go that far with the shields up."))
            return                              
        # Make sure enough time is left for the trip 
                prout(_("We haven't the energy to go that far with the shields up."))
            return                              
        # Make sure enough time is left for the trip 
-       game.optime = course.time()
+       game.optime = course.time(game.warpfac)
        if game.optime >= 0.8*game.state.remtime:
            skip(1)
            prout(_("First Officer Spock- \"Captain, I compute that such"))
        if game.optime >= 0.8*game.state.remtime:
            skip(1)
            prout(_("First Officer Spock- \"Captain, I compute that such"))
@@ -4072,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
@@ -4087,36 +3891,23 @@ 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 VALID_SECTOR(ix, iy):
-                   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
     if game.alldone:
        return
-    game.energy -= course.power()
+    game.energy -= course.power(game.warpfac)
     if game.energy <= 0:
        finish(FNRG)
     if game.energy <= 0:
        finish(FNRG)
-    game.optime = course.time()
+    game.optime = course.time(game.warpfac)
     if twarp:
        timwrp()
     if blooey:
     if twarp:
        timwrp()
     if blooey:
@@ -4210,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
@@ -4224,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(distance=dist, bearing=randreal(12))   # 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:
@@ -4386,7 +4175,7 @@ def mayday():
     game.sector.invalidate()
     for m in range(1, 5+1):
         w = game.base.scatter() 
     game.sector.invalidate()
     for m in range(1, 5+1):
         w = game.base.scatter() 
-       if VALID_SECTOR(w.i,w.j) and game.quad[w.i][w.j]==IHDOT:
+       if w.valid_sector() and game.quad[w.i][w.j]==IHDOT:
            # found one -- finish up 
             game.sector = w
            break
            # found one -- finish up 
             game.sector = w
            break
@@ -4489,7 +4278,7 @@ def abandon():
            game.quad[game.sector.i][game.sector.j] = IHDOT
            for l in range(QUADSIZE):
                game.sector = game.base.scatter()
            game.quad[game.sector.i][game.sector.j] = IHDOT
            for l in range(QUADSIZE):
                game.sector = game.base.scatter()
-               if VALID_SECTOR(game.sector.i, game.sector.j) and \
+               if game.sector.valid_sector() and \
                        game.quad[game.sector.i][game.sector.j] == IHDOT:
                     break
            if l < QUADSIZE+1:
                        game.quad[game.sector.i][game.sector.j] == IHDOT:
                     break
            if l < QUADSIZE+1:
@@ -5085,7 +4874,7 @@ def lrscan(silent):
         if not silent:
             proutn(" ")
         for y in range(game.quadrant.j-1, game.quadrant.j+2):
         if not silent:
             proutn(" ")
         for y in range(game.quadrant.j-1, game.quadrant.j+2):
-           if not VALID_QUADRANT(x, y):
+           if not coord(x, y).valid_quadrant():
                 if not silent:
                     proutn("  -1")
            else:
                 if not silent:
                     proutn("  -1")
            else:
@@ -5104,7 +4893,6 @@ def damagereport():
     "Damage report."
     jdam = False
     scanner.chew()
     "Damage report."
     jdam = False
     scanner.chew()
-
     for i in range(NDEVICES):
        if damaged(i):
            if not jdam:
     for i in range(NDEVICES):
        if damaged(i):
            if not jdam:
@@ -5189,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:
@@ -5226,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
@@ -5311,7 +5096,7 @@ def eta():
            w2.j = 0
        else:
            w2.j=QUADSIZE-1
            w2.j = 0
        else:
            w2.j=QUADSIZE-1
-    if not VALID_QUADRANT(w1.i, w1.j) or not VALID_SECTOR(w2.i, w2.j):
+    if not w1.valid_quadrant() or not w2.valid_sector():
        huh()
        return
     dist = math.sqrt((w1.j-game.quadrant.j+(w2.j-game.sector.j)/(QUADSIZE*1.0))**2+
        huh()
        return
     dist = math.sqrt((w1.j-game.quadrant.j+(w2.j-game.sector.j)/(QUADSIZE*1.0))**2+
@@ -5346,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."))