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