Parallel cleanups in the C and scratch Python code.
[super-star-trek.git] / src / sst.py
1 """
2 sst.py =-- Super Star Trek in Python
3
4 Control flow of this translation is pretty much identical to the C version
5 (and thus like the ancestral FORTRAN) but the data structures are
6 radically different -- the Python code makes heavy use of objects.
7
8 Note that the game.quad, game.snap.galaxy and game.snap.chart members
9 are not actually arrays but dictioaries indixed by coord tuples.  Be setting
10 the hash of a coord exual to the hash of a literal tuple containing its
11 coordinate data, we ensure these can be indexed both ways.
12
13 """
14 import math
15
16 PHASEFAC        = 2.0
17 GALSIZE         = 8
18 NINHAB          = GALSIZE * GALSIZE / 2
19 MAXUNINHAB      = 10
20 PLNETMAB        = NINHAB + MAXUNINHAB
21 QUADSIZE        = 10
22 BASEMAX         = 5
23 FULLCREW        = 428    # BSD Trek was 387, that's wrong
24 MAXKLGAME       = 127
25 MAXKLQUAD       = 9
26 FOREVER         = 1e30
27
28 # These macros hide the difference between 0-origin and 1-origin addressing.
29 # They're a step towards de-FORTRANizing the code.
30 def VALID_QUADRANT(x,y): ((x)>=1 and (x)<=GALSIZE and (y)>=1 and (y)<=GALSIZE)
31 def VALID_SECTOR(x, y): ((x)>=1 and (x)<=QUADSIZE and (y)>=1 and (y)<=QUADSIZE)
32
33 # These types have not been dealt with yet 
34 IHQUEST = '?',
35 IHWEB = '#',
36 IHMATER0 = '-',
37 IHMATER1 = 'o',
38 IHMATER2 = '0',
39
40 class coord:
41     def __init(self, x=None, y=None):
42         self.x = x
43         self.y = y
44     def invalidate(self):
45         self.x = self.y = None
46     def is_valid(self):
47         return self.x != None and self.y != None
48     def __eq__(self, other):
49         return self.x == other.y and self.x == other.y
50     def __add__(self, other):
51         return coord(self.x+self.x, self.y+self.y)
52     def __sub__(self, other):
53         return coord(self.x-self.x, self.y-self.y)
54     def distance(self, other):
55         return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
56     def sgn(self):
57         return coord(self.x / abs(x), self.y / abs(y));
58     def __hash__(self):
59         return hash((x, y))
60     def __str__(self):
61         return "%d - %d" % (self.x, self.y)
62
63 class feature:
64     "A feature in the current quadrant (ship, star, black hole, etc)." 
65     def __init__(self):
66         self.type = None        # name of feature type
67         self.sector = None      # sector location
68     def distance(self):
69         return self.sector.distance(game.sector)
70     def __str__(self):
71         return self.name[0]
72     def sectormove(self, dest):
73         "Move this feature within the current quadrant." 
74         if self.sector:
75             game.quad[self.sector] = None
76         game.quad[dest] = self
77         self.sector = dest
78
79 empty = None    # Value of empty space in game.quad
80
81 class ship(feature):
82     "An enemy ship in the current quadrant." 
83     def __init__(self):
84         feature.__init__(self)
85         self.type = None        # klingon, romulan, commander,
86                                 # supercommander, tholian
87         self.power = None       # power
88         if self.type in ("Klingon", "Commander", "Super-Commander"):
89             game.remkl += 1
90         elif self.type == "Romulan":
91             game.romrem += 1
92     def __del__(self):
93         if self.type in ("Klingon", "Commander", "Super-Commander"):
94             game.remkl -= 1
95         elif self.type == "Romulan":
96             game.romrem -= 1
97
98 class planet(feature):
99     "A planet.  May be inhabited or not, may hold dilithium crystals or not."
100     def __init(self):
101         feature.__init__(self)
102         self.name = None
103         self.crystals = None    # "absent", "present", or "mined"
104         self.inhabited = False
105         self.known = "unknown"  # Other values: "known" and "shuttle down"
106     def __str__(self):
107         if self.inhabited:
108             return '@'
109         else:
110             return 'P'
111
112 class star(feature):
113     "A star.  Has no state, just knows how to identify iself."
114     def __init(self):
115         feature.__init__(self)
116     def __str__(self):
117         return '*'
118
119 class web(feature):
120     "A bit of Tholian web.  Has no state, just knows how to identify iself."
121     def __init(self):
122         feature.__init__(self)
123     def __str__(self):
124         return '*'
125
126 class blackhole(feature):
127     "A black hole.  Has no hair, just knows how to identify iself."
128     def __init(self):
129         feature.__init__(self)
130     def __str__(self):
131         return '*'
132
133 class starbase(feature):
134     "Starbases also have no features, just a location."
135     def __init(self, quadrant):
136         feature.__init__(self)
137         self.quadrant = quadrant
138         game.state.bases.append(self)
139     def __del__(self):
140         game.state.bases.remove(self)
141     def __str__(self):
142         return 'B'
143     def __del__(self):
144         feature.__del__(self)
145
146 class quadrant:
147     def __init__(self):
148         self.stars = None
149         self.planet = None
150         self.starbase = None
151         self.klingons = None
152         self.romulans = None
153         self.supernova = None
154         self.charted = None
155         self.status = "secure"  # Other valuues: "distressed", "enslaved"
156     def enemies(self):
157         "List enemies in this quadrant."
158         lst = []
159         for feature in self.quad.values:
160             if not isinstance(feature, ship):
161                 continue
162             if feature.name not in ("Enterprise", "Faerie Queene"):
163                 lst.append(feature)
164         return lst
165
166 class page:
167     "A chart page.  The starchart is a 2D array of these."
168     def __init__(self):
169         self.stars = None       # Will hold a number
170         self.starbase = None    # Will hold a bool
171         self.klingons = None    # Will hold a number
172
173 class snapshot:
174     "State of the universe.  The galaxy is a 2D array of these."
175     def __init__(self):
176         self.crew = None        # crew complement
177         self.remkl = None       # remaining klingons
178         self.remcom = None      # remaining commanders
179         self.nscrem = None      # remaining super commanders
180         self.starkl = None      # destroyed stars
181         self.basekl = None      # destroyed bases
182         self.nromrem = None     # Romulans remaining
183         self.nplankl = None     # destroyed uninhabited planets
184         self.nworldkl = None    # destroyed inhabited planets
185         self.plnets = [];       # List of planets known
186         self.date = None        # stardate
187         self.remres = None      # remaining resources
188         self. remtime = None    # remaining time
189         self.bases = []         # Base quadrant coordinates
190         self.kcmdr = []         # Commander quadrant coordinates
191         self.kscmdr = None      # Supercommander quadrant coordinates
192         self.galaxy = {}        # Dictionary of quadrant objects
193         self.chart = {}         # Dictionary of page objects
194
195 def damaged(dev):
196     return game.damage[dev] != 0.0
197
198 class event:
199     def __init__(self):
200         self.date = None        # The only mandatory attribute
201
202 class game:
203     def __init__(self):
204         self.options = []               # List of option strings
205         self.state = snapshot()         # State of the universe
206         self.snapsht = snapshot()       # For backwards timetravel
207         self.quad = {}                  # contents of our quadrant
208         self.kpower = {}                # enemy energy levels
209         self.kdist = {}                 # enemy distances
210         self.kavgd = {}                 # average distances
211         self.damage = {}                # damage encountered
212         self.future = []                # future events
213         self.passwd = None              # Self Destruct password
214         # Coordinate members start here
215         self.enemies = {}                       # enemy sector locations
216         self.quadrant = None            # where we are
217         self.sector = None
218         self.tholian = None             # coordinates of Tholian
219         self.base = None                # position of base in current quadrant
220         self.battle = None              # base coordinates being attacked
221         self.plnet = None               # location of planet in quadrant
222         self.probec = None              # current probe quadrant
223         # Flag members start here
224         self.gamewon = None             # Finished!
225         self.ididit = None              # action taken -- allows enemy to attack
226         self.alive = None               # we are alive (not killed)
227         self.justin = None              # just entered quadrant
228         self.shldup = None              # shields are up
229         self.shldchg = None             # shield changing (affects efficiency)
230         self.comhere = None             # commander here
231         self.ishere = None              # super-commander in quadrant
232         self.iscate = None              # super commander is here
233         self.ientesc = None             # attempted escape from supercommander
234         self.ithere = None              # Tholian is here 
235         self.resting = None             # rest time
236         self.icraft = None              # Kirk in Galileo
237         self.landed = None              # party on planet or on ship
238         self.alldone = None             # game is now finished
239         self.neutz = None               # Romulan Neutral Zone
240         self.isarmed = None             # probe is armed
241         self.inorbit = None             # orbiting a planet
242         self.imine = None               # mining
243         self.icrystl = None             # dilithium crystals aboard
244         self.iseenit = None             # seen base attack report
245         self.thawed = None              # thawed game
246         # String members start here
247         self.condition = None           # green, yellow, red, docked, dead,
248         self.iscraft = None             # onship, offship, removed
249         self.skill = None               # levels: none, novice, fair, good,
250                                         # expert, emeritus
251         # Integer nembers sart here
252         self.inkling = None             # initial number of klingons
253         self.inbase = None              # initial number of bases
254         self.incom = None               # initial number of commanders
255         self.inscom = None              # initial number of commanders
256         self.inrom = None               # initial number of commanders
257         self.instar = None              # initial stars
258         self.intorps = None             # initial/max torpedoes
259         self.torps = None               # number of torpedoes
260         self.ship = None                # ship type -- 'E' is Enterprise
261         self.abandoned = None           # count of crew abandoned in space
262         self.length = None              # length of game
263         self.klhere = None              # klingons here
264         self.casual = None              # causalties
265         self.nhelp = None               # calls for help
266         self.nkinks = None              # count of energy-barrier crossings
267         self.iplnet = None              # planet # in quadrant
268         self.inplan = None              # initial planets
269         self.irhere = None              # Romulans in quadrant
270         self.isatb = None               # =1 if super commander is attacking base
271         self.tourn = None               # tournament number
272         self.proben = None              # number of moves for probe
273         self.nprobes = None             # number of probes available
274         # Float members start here
275         self.inresor = None             # initial resources
276         self.intime = None              # initial time
277         self.inenrg = None              # initial/max energy
278         self.inshld = None              # initial/max shield
279         self.inlsr = None               # initial life support resources
280         self.indate = None              # initial date
281         self.energy = None              # energy level
282         self.shield = None              # shield level
283         self.warpfac = None             # warp speed
284         self.wfacsq = None              # squared warp factor
285         self.lsupres = None             # life support reserves
286         self.dist = None                # movement distance
287         self.direc = None               # movement direction
288         self.optime = None              # time taken by current operation
289         self.docfac = None              # repair factor when docking (constant?)
290         self.damfac = None              # damage factor
291         self.lastchart = None           # time star chart was last updated
292         self.cryprob = None             # probability that crystal will work
293         self.probex = None              # location of probe
294         self.probey = None              #
295         self.probeinx = None            # probe x,y increment
296         self.probeiny = None            #
297         self.height = None              # height of orbit around planet
298
299 def communicating():
300     "Are we in communication with Starfleet Command?"
301     return (not damaged("DRADIO")) or game.condition == docked
302
303 # Code corresponding to ai.c begins here
304
305 def tryexit(look, ship, irun):
306     # a bad guy attempts to bug out of the quadrant
307     iq = coord()
308     iq.x = game.quadrant.x+(look.x+(QUADSIZE-1))/QUADSIZE - 1
309     iq.y = game.quadrant.y+(look.y+(QUADSIZE-1))/QUADSIZE - 1
310     if not valid_quadrant(iq) or \
311         game.state.galaxy[iq].supernova or \
312         game.state.galaxy[iq].klingons > MAXKLQUAD-1:
313         return False;   # no can do -- neg energy, supernovae, or >MAXKLQUAD-1 Klingons
314     if ship.type == "Romulan":
315         return False    # Romulans cannot escape
316     if not irun:
317         # avoid intruding on another commander's territory
318         if ship.type == "Commander":
319             if iq in gamestate.kcmdr:
320                 return False
321             # refuse to leave if currently attacking starbase:
322             if game.battle == game.quadrant:
323                 return False;
324         # don't leave if over 1000 units of energy
325         if ship.power > 1000.0:
326             return false;
327     # Print escape message and move out of quadrant.
328     # We know this if either short or long range sensors are working
329     if not damaged("DSRSENS") or not damaged("DLRSENS") or game.condition=="docked":
330         crmena(True, "sector", ship)
331         prout(" escapes to quadrant %s (and regains strength)." % iq)
332     # handle local matters related to escape
333     game.quad[ship.location] = None;
334     if game.condition != "docked":
335         newcnd()
336     # Handle global matters related to escape
337     game.state.galaxy[game.quadrant].klingons -= 1
338     game.state.galaxy[iq].klingons += 1
339     if ship.type == "Super-Commander":
340         game.ishere = False
341         game.iscate = False
342         game.ientesc = False
343         game.isatb = 0
344         schedule("FSCMOVE", 0.2777)
345         unschedule("FSCDBAS")
346         game.state.kscmdr = iq
347     else:
348         for (n, cmdr) in enumerate(game.state.kcmdr):
349             if cmdr == game.quadrant:
350                 game.state.kcmdr[n] = iq
351                 break
352         game.comhere = False
353     return True         # successful exit
354
355 def sgn(n): n / abs(n)
356
357 '''
358 Algorithm for moving bad guys:
359
360  * Enterprise has "force" based on condition of phaser and photon torpedoes.
361  If both are operating full strength, force is 1000. If both are damaged,
362  force is -1000. Having shields down subtracts an additional 1000.
363
364  * Enemy has forces equal to the energy of the attacker plus
365  100*(K+R) + 500*(C+S) - 400 for novice through good levels OR
366  346*K + 400*R + 500*(C+S) - 400 for expert and emeritus.
367
368  Attacker Initial energy levels (nominal):
369  Klingon   Romulan   Commander   Super-Commander
370  Novice    400        700        1200        
371  Fair      425        750        1250
372  Good      450        800        1300        1750
373  Expert    475        850        1350        1875
374  Emeritus  500        900        1400        2000
375  VARIANCE   75        200         200         200
376
377  Enemy vessels only move prior to their attack. In Novice - Good games
378  only commanders move. In Expert games, all enemy vessels move if there
379  is a commander present. In Emeritus games all enemy vessels move.
380
381  *  If Enterprise is not docked, an agressive action is taken if enemy
382  forces are 1000 greater than Enterprise.
383
384  Agressive action on average cuts the distance between the ship and
385  the enemy to 1/4 the original.
386
387  *  At lower energy advantage, movement units are proportional to the
388  advantage with a 650 advantage being to hold ground, 800 to move forward
389  1, 950 for two, 150 for back 4, etc. Variance of 100.
390
391  If docked, is reduced by roughly 1.75*game.skill, generally forcing a
392  retreat, especially at high skill levels.
393
394  *  Motion is limited to skill level, except for SC hi-tailing it out.
395 '''
396
397 def movebaddy(ship):
398     # tactical movement for the bad guys
399     bugout = False
400     # This should probably be just game.comhere + game.ishere
401     if game.skill >= SKILL_EXPERT:
402         nbaddys = int((game.comhere*2 + game.ishere*2+game.klhere*1.23+game.irhere*1.5)/2.0)
403     else:
404         nbaddys = game.comhere + game.ishere
405     dist1 = ship.distance()
406     mdist = round(dist1 + 0.5)          # Nearest integer distance
407     # If SC, check with spy to see if should high-tail it
408     if ship.type == "Super-Commander" and \
409            (ship.power <= 500.0 or (game.condition==docked and not damaged("DPHOTON"))):
410         bugout = True;
411         motion = -QUADSIZE;
412     else:
413         # decide whether to advance, retreat, or hold position
414         forces = ship.power + 100.0*len(game.quad.enemies()) + 400*(nbaddys-1)
415         if not game.shldup:
416             forces += 1000.0            # Good for enemy if shield is down!
417         if not damaged("DPHASER") or not damaged("DPHOTON"):
418             if damaged(DPHASER):
419                 forces += 300.0
420             else:
421                 forces -= 0.2*(game.energy - 2500.0);
422             if damaged("DPHOTON"):
423                 forces += 300.0
424             else:
425                 forces -= 50.0*game.torps
426         else:
427             # phasers and photon tubes both out!
428             forces += 1000.0
429         motion = 0;
430         if forces <= 1000.0 and game.condition != "docked":     # Typical case
431             motion = ((forces+200.0*Rand())/150.0) - 5.0
432         else:
433             if forces > 1000.0: # Very strong -- move in for kill
434                 motion = (1.0-square(Rand()))*dist1 + 1.0
435             if game.condition == "docked" and "base" in game.options:
436                  # protected by base -- back off !
437                 motion -= game.skill * (2.0-Rand()**2)
438         if idebug:
439             proutn("=== MOTION = %1.2f, FORCES = %1.2f, ", motion, forces)
440         # don't move if no motion
441         if motion == 0:
442             return
443         # Limit motion according to skill
444         if abs(motion) > game.skill:
445             if motion < 0:
446                 motion = -game.kill
447             else:
448                 motion = game.skill
449     # calculate preferred number of steps
450     nsteps = abs(motion)
451     if motion > 0 and nsteps > mdist: # don't overshoot
452         nsteps = mdist
453     if nsteps > QUADSIZE: # This shouldn't be necessary
454         nsteps = QUADSIZE
455     if nsteps < 1:  # This shouldn't be necessary
456         nsteps = 1
457     if idebug:
458         proutn("NSTEPS = %d:", nsteps)
459     # Compute preferred values of delta X and Y
460     me = game.sector - com;
461     if 2.0 * abs(me.x) < abs(me.y):
462         me.x = 0;
463     if 2.0 * abs(me.y) < abs(game.sector.x-com.x):
464         me.y = 0;
465     if me.x != 0: me.x = sgn(me.x*motion)
466     if me.y != 0: me.y = sgn(me.y*motion)
467     next = com;
468     # main move loop
469     for ll in range(nsteps):
470         if idebug:
471             proutn(" %d", ll+1)
472         # Check if preferred position available
473         look = next + me
474         krawl = me.sgn()
475         success = False
476         attempts = 0 # Settle meysterious hang problem
477         while attempts < 20 and not success:
478             attempts += 1
479             if look.x < 1 or look.x > QUADSIZE:
480                 if motion < 0 and tryexit(look, ship, bugout):
481                     return
482                 if krawl.x == me.x or me.y == 0:
483                     break
484                 look.x = next.x + krawl.x
485                 krawl.x = -krawl.x
486             elif look.y < 1 or look.y > QUADSIZE:
487                 if motion < 0 and tryexit(look, ship, bugout):
488                     return
489                 if krawl.y == me.y or me.x == 0:
490                     break
491                 look.y = next.y + krawl.y
492                 krawl.y = -krawl.y
493             elif "ramming" in game.options and game.quad[look] != IHDOT:
494                 # See if we should ram ship
495                 if game.quad[look] == game.ship and ienm in (IHC, IHS):
496                     ram(true, ienm, com)
497                     return
498                 if krawl.x != me.x and me.y != 0:
499                     look.x = next.x + krawlx
500                     krawl.x = -krawl.x
501                 elif krawly != me.y and me.x != 0:
502                     look.y = next.y + krawly
503                     krawl.y = -krawl.y
504                 else:
505                     break # we have failed
506             else:
507                 success = True
508         if success:
509             next = look
510             if idebug:
511                 proutn(str(next))
512         else:
513             break # done early
514     if idebug:
515         prout("")
516     # Put ship in place within same quadrant
517     if next != ship.location:
518         # it moved
519         if not damaged("DSRSENS") or game.condition == "docked":
520             proutn("*** %s from sector %s" % (ship, ship.location))
521             if ship.distance() < dist1:
522                 prout(" advances to sector %s" % ship.location)
523             else:
524                 prout(" retreats to sector %s" % ship.location)
525         ship.sectormove(next)
526
527 def movcom(): 
528     "Allow enemies to move."
529     for enemy in self.quad.enemies():
530         if enemy.type == "Commander":
531             movebaddy(enemy)
532             break
533     for enemy in self.quad.enemies():
534         if enemy.type == "Super-Commander":
535             movebaddy(enemy)
536             break
537     # Ff skill level is high, move other Klingons and Romulans too!
538     # Move these last so they can base their actions on what the
539     # commander(s) do.
540     if game.skill >= SKILL_EXPERT and "movebaddy" in game.options: 
541         for enemy in self.quad.enemies():
542             if enemy.type in ("Klingon", "Romulan"):
543                 movebaddy(enemy)
544                 break
545
546 def movescom(ship, avoid):
547     # commander movement helper
548     global ipage
549     if game.state.kscmdr == game.quadrant or \
550         game.state.galaxy[iq].supernova or \
551         game.state.galaxy[iq].klingons > MAXKLQUAD-1: 
552         return True
553     if avoid:
554         # Avoid quadrants with bases if we want to avoid Enterprise
555         for base in game.state.starbases:
556             if base.location == ship.location:
557                 return True
558     if game.justin and not game.iscate:
559         return True
560     # Super-Commander has scooted, Remove him from current quadrant.
561     if game.state.kscmdr == game.quadrant:
562         game.iscate = False
563         game.isatb = 0
564         game.ientesc = False
565         unschedule("FSCDBAS")
566         if game.condition != "docked":
567             newcnd()
568         ship.sectormove(None)
569     # do the actual move
570     game.state.galaxy[game.state.kscmdr].klingons -= 1
571     game.state.kscmdr = iq
572     game.state.galaxy[game.state.kscmdr].klingons += 1
573     # check for a helpful planet in the destination quadrant
574     for planet in game.state.plnets:
575         if planet.location == game.state.kscmdr and planet.crystals=="present":
576             # destroy the planet
577             game.state.plnets.remove(planet)
578             if communicating():
579                 if not ipage:
580                     pause_game(True)
581                 ipage = true
582                 prout("Lt. Uhura-  \"Captain, Starfleet Intelligence reports")
583                 proutn(_("   a planet in "))
584                 proutn(cramlc(quadrant, game.state.kscmdr))
585                 prout(" has been destroyed")
586                 prout("   by the Super-commander.\"")
587             break
588     return False # looks good!
589                         
590 def scom():
591     # move the Super Commander
592     if (idebug):
593         prout("== SCOM")
594
595     # Decide on being active or passive
596     passive = ((NKILLC+NKILLK)/(game.state.date+0.01-game.indate) < 0.1*game.skill*(game.skill+1.0) \
597                or (game.state.date-game.indate) < 3.0)
598     if not game.iscate and passive:
599         # compute move away from Enterprise
600         idelta = game.state.kscmdr - game.quadrant
601         if distance(game.state.kscmdr) > 2.0:
602             # circulate in space
603             idelta,x = game.state.kscmdr.y-game.quadrant.y
604             idelta,y = game.quadrant.x-game.state.kscmdr.x
605     else:
606         if len(game.state.bases):
607             unschedule("FSCMOVE")
608             return
609         sc = game.state.kscmdr
610         # compute distances to starbases
611         game.starbases.sort(lambda x, y: cmp(distance(x, game.quadrant), distance(y, game.quadrant)))
612         # look for nearest base without a commander, no Enterprise, and
613         # without too many Klingons, and not already under attack.
614         nearest = filter(game.starbases,
615                          lambda x: game.state.galaxy[x].supernova \
616                          and game.state.galaxy[x].klingons <= MAXKLQUAD-1)
617         if game.quadrant in nearest:
618             nearest.remove(game.quadrant)
619         if game.battle in nearest:
620             nearest.remove(game.battle)
621         # if there is a commander, and no other base is appropriate,
622         # we will take the one with the commander
623         nocmd = filter(lambda x: x.location not in game.state.kcmdr, nearest)
624         if len(nocmd):
625             nearest = nocmd
626         ibq = nearest[0]
627         if len(nearest) == 0:
628             return      # Nothing suitable -- wait until next time
629         # decide how to move toward base
630         idelta = ibq - game.state.kscmdr
631     # maximum movement is 1 quadrant in either or both axis
632     delta = delta.sgn()
633     # try moving in both x and y directions
634     iq = game.state.kscmdr + idelta
635     if movescom(iq, passive):
636         # failed -- try some other maneuvers
637         if ideltax==0 or ideltay==0:
638             # attempt angle move
639             if ideltax != 0:
640                 iq.y = game.state.kscmdr.y + 1
641                 if movescom(iq, passive):
642                     iq.y = game.state.kscmdr.y - 1
643                     movescom(iq, passive)
644             else:
645                 iq.x = game.state.kscmdr.x + 1
646                 if movescom(iq, passive):
647                     iq.x = game.state.kscmdr.x - 1
648                     movescom(iq, passive)
649         else:
650             # try moving just in x or y
651             iq.y = game.state.kscmdr.y
652             if movescom(iq, passive):
653                 iq.y = game.state.kscmdr.y + ideltay
654                 iq.x = game.state.kscmdr.x
655                 movescom(iq, passive)
656     # check for a base
657     if len(game.state.bases) == 0:
658         unschedule("FSCMOVE")
659     else:
660         for ibq in game.bases:
661             if ibq == game.state.kscmdr and game.state.kscmdr == game.battle:
662                 # attack the base
663                 if passive:
664                     return # no, don't attack base!
665                 game.iseenit = false
666                 game.isatb = 1
667                 schedule("FSCDBAS", 1.0 +2.0*Rand())
668                 if is_scheduled("FCDBAS"):
669                     postpone("FSCDBAS", scheduled("FCDBAS")-game.state.date)
670                 if not communicating():
671                     return # no warning
672                 game.iseenit = True
673                 if not ipage:
674                     pause_game(true)
675                 ipage = True
676                 proutn(_("Lt. Uhura-  \"Captain, the starbase in "))
677                 proutn(cramlc(quadrant, game.state.kscmdr))
678                 skip(1)
679                 prout("   reports that it is under attack from the Klingon Super-commander.")
680                 proutn("   It can survive until stardate %d.\"",
681                        int(scheduled(FSCDBAS)))
682                 if not game.resting:
683                     return
684                 prout("Mr. Spock-  \"Captain, shall we cancel the rest period?\"")
685                 if ja() == false:
686                     return
687                 game.resting = False
688                 game.optime = 0.0 # actually finished
689                 return
690     # Check for intelligence report
691     if (Rand() > 0.2 or not communicating() or
692         not game.state.galaxy[game.state.kscmdr].charted):
693         return
694     if ipage:
695         pause_game(true)
696         ipage = true
697     prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
698     proutn(_("   the Super-commander is in "))
699     proutn(cramlc(quadrant, game.state.kscmdr))
700     prout(".\"")
701     return
702
703 def movetho(void):
704     "Move the Tholian (an instance of ship type pointed at by game.tholian)." 
705     if not game.tholian or game.justin:
706         return
707     next = coord()
708     if game.tholian.location.x == 1 and game.tholian.location.y == 1:
709         next.x = 1
710         next.y = QUADSIZE
711     elif game.tholian.location.x == 1 and game.tholian.location.y == QUADSIZE:
712         next.x = next.y = QUADSIZE
713     elif game.tholian.location.x == QUADSIZE and game.tholian.location.y == QUADSIZE:
714         next.x = QUADSIZE
715         next.y = 1
716     elif game.tholian.location.x == QUADSIZE and game.tholian.location.y == 1:
717         next.x = next.y = 1
718     else:
719         # something is wrong!
720         game.tholian = None
721         return
722     # Do nothing if we are blocked
723     if game.quad[next] != empty and not isinstance(game.quad[next]. web):
724         return
725     # Now place some web
726     im = (next - game.tholian.location).sgn()
727     if game.tholian.x != next.x:
728         # move in x axis
729         while game.tholian.location.x != next.x:
730             game.tholian.location.x += im.x
731             if game.quad[game.tholian.location] == empty:
732                 game.quad[game.tholian.location] = web()
733     elif game.tholian.y != next.y:
734         # move in y axis
735         while game.tholian.y != next.y:
736             game.tholian.y += im.y
737             if game.quad[game.tholian.location] == empty:
738                 game.quad[game.tholian.location] = web()
739     # web is done, move ship
740     game.tholian.movesector(next)
741     # check to see if all holes plugged
742     for i in range(1, QUADSIZE+1):
743         if (not isinstance(game.quad[(1,i)],web)) and game.quad[(1,i)]!=game.tholian:
744             return
745         if (not isinstance(game.quad[(QUADSIZE,i)],web)) and game.quad[(QUADSIZE,i)]!=game.tholian:
746             return
747         if (not isinstance(game.quad[(i,1)],web)) and game.quad[(i,1)]!=game.tholian:
748             return
749         if (not isinstance(game.quad[(i.QUADSIZE)],web)) and game.quad[(i,QUADSIZE)]!=game.tholian:
750             return
751     # All plugged up -- Tholian splits
752     game.quad[game.tholian] = web()
753     ship.movesector(None)
754     crmena(True, IHT, sector, game.tholian)
755     prout(" completes web.")
756     game.tholian = None
757     return
758