758fc9e24f20f1a3aef9fa2445ca0da7d6f39e4a
[super-star-trek.git] / src / sst.py
1 """
2 sst.py =-- Super Star Trek in Python
3
4 """
5 import os, sys, math, curses
6
7 SSTDOC = "/usr/share/doc/sst/sst.doc"
8
9 # Stub to be replaced
10 def _(str): return str
11
12 PHASEFAC        = 2.0
13 GALSIZE         = 8
14 NINHAB          = (GALSIZE * GALSIZE / 2)
15 MAXUNINHAB      = 10
16 PLNETMAX        = (NINHAB + MAXUNINHAB)
17 QUADSIZE        = 10
18 BASEMAX         = (GALSIZE * GALSIZE / 12)
19 MAXKLGAME       = 127
20 MAXKLQUAD       = 9
21 FULLCREW        = 428   # BSD Trek was 387, that's wrong 
22 FOREVER         = 1e30
23
24 # These functions hide the difference between 0-origin and 1-origin addressing.
25 def VALID_QUADRANT(x, y):       return ((x)>=1 and (x)<=GALSIZE and (y)>=1 and (y)<=GALSIZE)
26 def VALID_SECTOR(x, y): return ((x)>=1 and (x)<=QUADSIZE and (y)>=1 and (y)<=QUADSIZE)
27
28 def square(i):          return ((i)*(i))
29 def distance(c1, c2):   return math.sqrt(square(c1.x - c2.x) + square(c1.y - c2.y))
30 def invalidate(w):      w.x = w.y = 0
31 def is_valid(w):        return (w.x != 0 and w.y != 0)
32
33 class coord:
34     def __init(self, x=None, y=None):
35         self.x = x
36         self.y = y
37     def invalidate(self):
38         self.x = self.y = None
39     def is_valid(self):
40         return self.x != None and self.y != None
41     def __eq__(self, other):
42         return self.x == other.y and self.x == other.y
43     def __add__(self, other):
44         return coord(self.x+self.x, self.y+self.y)
45     def __sub__(self, other):
46         return coord(self.x-self.x, self.y-self.y)
47     def distance(self, other):
48         return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
49     def sgn(self):
50         return coord(self.x / abs(x), self.y / abs(y));
51     def __hash__(self):
52         return hash((x, y))
53     def __str__(self):
54         return "%d - %d" % (self.x, self.y)
55
56 class planet:
57     def __init(self):
58         self.name = None        # string-valued if inhabited
59         self.w = coord()        # quadrant located
60         self.pclass = None      # could be ""M", "N", "O", or "destroyed"
61         self.crystals = None    # could be "mined", "present", "absent"
62         self.known = None       # could be "unknown", "known", "shuttle_down"
63
64 # How to represent features
65 IHR = 'R',
66 IHK = 'K',
67 IHC = 'C',
68 IHS = 'S',
69 IHSTAR = '*',
70 IHP = 'P',
71 IHW = '@',
72 IHB = 'B',
73 IHBLANK = ' ',
74 IHDOT = '.',
75 IHQUEST = '?',
76 IHE = 'E',
77 IHF = 'F',
78 IHT = 'T',
79 IHWEB = '#',
80 IHMATER0 = '-',
81 IHMATER1 = 'o',
82 IHMATER2 = '0'
83
84 NOPLANET = None
85 class quadrant:
86     def __init(self):
87         self.stars = None
88         self.planet = None
89         self.starbase = None
90         self.klingons = None
91         self.romulans = None
92         self.supernova = None
93         self.charted = None
94         self.status = None      # Could be "secure", "distressed", "enslaved"
95
96 class page:
97     def __init(self):
98         self.stars = None
99         self.starbase = None
100         self.klingons = None
101
102 class snapshot:
103     def __init(self):
104         self.snap = False       # snapshot taken
105         self.crew = None        # crew complement
106         self.remkl = None       # remaining klingons
107         self.remcom = None      # remaining commanders
108         self.nscrem = None      # remaining super commanders
109         self.rembase = None     # remaining bases
110         self.starkl = None      # destroyed stars
111         self.basekl = None      # destroyed bases
112         self.nromrem = None     # Romulans remaining
113         self.nplankl = None     # destroyed uninhabited planets
114         self.nworldkl = None    # destroyed inhabited planets
115         self.planets = []       # Planet information
116         for i in range(PLNETMAX):
117             self.planets.append(planet())
118         self.date = None        # stardate
119         self.remres = None      # remaining resources
120         self.remtime = None     # remaining time
121         self.baseq = []         # Base quadrant coordinates
122         for i in range(BASEMAX+1):
123             self.baseq.append(coord())
124         self.kcmdr = []         # Commander quadrant coordinates
125         for i in range(QUADSIZE+1):
126             self.kcmdr.append(coord())
127         self.kscmdr = coord()   # Supercommander quadrant coordinates
128         self.galaxy = []        # The Galaxy (subscript 0 not used)
129         for i in range(GALSIZE+1):
130             self.chart.append([])
131             for j in range(GALSIZE+1):
132                 self.galaxy[i].append(quadrant())
133         self.chart = []         # the starchart (subscript 0 not used)
134         for i in range(GALSIZE+1):
135             self.chart.append([])
136             for j in range(GALSIZE+1):
137                 self.chart[i].append(page())
138
139 class event:
140     def __init__(self):
141         self.date = None        # A real number
142         self.quadrant = None    # A coord structure
143
144 # game options 
145 OPTION_ALL      = 0xffffffff
146 OPTION_TTY      = 0x00000001    # old interface 
147 OPTION_CURSES   = 0x00000002    # new interface 
148 OPTION_IOMODES  = 0x00000003    # cover both interfaces 
149 OPTION_PLANETS  = 0x00000004    # planets and mining 
150 OPTION_THOLIAN  = 0x00000008    # Tholians and their webs 
151 OPTION_THINGY   = 0x00000010    # Space Thingy can shoot back 
152 OPTION_PROBE    = 0x00000020    # deep-space probes 
153 OPTION_SHOWME   = 0x00000040    # bracket Enterprise in chart 
154 OPTION_RAMMING  = 0x00000080    # enemies may ram Enterprise 
155 OPTION_MVBADDY  = 0x00000100    # more enemies can move 
156 OPTION_BLKHOLE  = 0x00000200    # black hole may timewarp you 
157 OPTION_BASE     = 0x00000400    # bases have good shields 
158 OPTION_WORLDS   = 0x00000800    # logic for inhabited worlds 
159 OPTION_PLAIN    = 0x01000000    # user chose plain game 
160 OPTION_ALMY     = 0x02000000    # user chose Almy variant 
161
162 # Define devices 
163 DSRSENS = 0
164 DLRSENS = 1
165 DPHASER = 2
166 DPHOTON = 3
167 DLIFSUP = 4
168 DWARPEN = 5
169 DIMPULS = 6
170 DSHIELD = 7
171 DRADIO  = 0
172 DSHUTTL = 9
173 DCOMPTR = 10
174 DNAVSYS = 11
175 DTRANSP = 12
176 DSHCTRL = 13
177 DDRAY   = 14
178 DDSP    = 15
179 NDEVICES= 16    # Number of devices
180
181 def damaged(dev):       return (game.damage[dev] != 0.0)
182
183 # Define future events 
184 FSPY    = 0     # Spy event happens always (no future[] entry)
185                 # can cause SC to tractor beam Enterprise
186 FSNOVA  = 1     # Supernova
187 FTBEAM  = 2     # Commander tractor beams Enterprise
188 FSNAP   = 3     # Snapshot for time warp
189 FBATTAK = 4     # Commander attacks base
190 FCDBAS  = 5     # Commander destroys base
191 FSCMOVE = 6     # Supercommander moves (might attack base)
192 FSCDBAS = 7     # Supercommander destroys base
193 FDSPROB = 8     # Move deep space probe
194 FDISTR  = 9     # Emit distress call from an inhabited world 
195 FENSLV  = 10    # Inhabited word is enslaved */
196 FREPRO  = 11    # Klingons build a ship in an enslaved system
197 NEVENTS = 12
198
199 #
200 # abstract out the event handling -- underlying data structures will change
201 # when we implement stateful events
202
203 def findevent(evtype):  return game.future[evtype]
204
205 class gamestate:
206     def __init__(self):
207         self.options = None     # Game options
208         self.state = None       # A snapshot structure
209         self.snapsht = None     # Last snapshot taken for time-travel purposes
210         self.quad = [[IHDOT * (QUADSIZE+1)] * (QUADSIZE+1)]     # contents of our quadrant
211         self.kpower = [[0 * (QUADSIZE+1)] * (QUADSIZE+1)]       # enemy energy levels
212         self.kdist = [[0 * (QUADSIZE+1)] * (QUADSIZE+1)]        # enemy distances
213         self.kavgd = [[0 * (QUADSIZE+1)] * (QUADSIZE+1)]        # average distances
214         self.damage = [0] * NDEVICES    # damage encountered
215         self.future = [0.0] * NEVENTS   # future events
216         for i in range(NEVENTS):
217             self.future.append(event())
218         self.passwd  = None;            # Self Destruct password
219         self.ks = [[None * (QUADSIZE+1)] * (QUADSIZE+1)]        # enemy sector locations
220         self.quadrant = None    # where we are in the large
221         self.sector = None      # where we are in the small
222         self.tholian = None     # coordinates of Tholian
223         self.base = None        # position of base in current quadrant
224         self.battle = None      # base coordinates being attacked
225         self.plnet = None       # location of planet in quadrant
226         self.probec = None      # current probe quadrant
227         self.gamewon = False    # Finished!
228         self.ididit = False     # action taken -- allows enemy to attack
229         self.alive = False      # we are alive (not killed)
230         self.justin = False     # just entered quadrant
231         self.shldup = False     # shields are up
232         self.shldchg = False    # shield is changing (affects efficiency)
233         self.comhere = False    # commander here
234         self.ishere = False     # super-commander in quadrant
235         self.iscate = False     # super commander is here
236         self.ientesc = False    # attempted escape from supercommander
237         self.ithere = False     # Tholian is here 
238         self.resting = False    # rest time
239         self.icraft = False     # Kirk in Galileo
240         self.landed = False     # party on planet (true), on ship (false)
241         self.alldone = False    # game is now finished
242         self.neutz = False      # Romulan Neutral Zone
243         self.isarmed = False    # probe is armed
244         self.inorbit = False    # orbiting a planet
245         self.imine = False      # mining
246         self.icrystl = False    # dilithium crystals aboard
247         self.iseenit = False    # seen base attack report
248         self.thawed = False     # thawed game
249         self.condition = None   # "green", "yellow", "red", "docked", "dead"
250         self.iscraft = None     # "onship", "offship", "removed"
251         self.skill = None       # Player skill level
252         self.inkling = 0        # initial number of klingons
253         self.inbase = 0         # initial number of bases
254         self.incom = 0          # initial number of commanders
255         self.inscom = 0         # initial number of commanders
256         self.inrom = 0          # initial number of commanders
257         self.instar = 0         # initial stars
258         self.intorps = 0        # initial/max torpedoes
259         self.torps = 0          # number of torpedoes
260         self.ship = 0           # ship type -- 'E' is Enterprise
261         self.abandoned = 0      # count of crew abandoned in space
262         self.length = 0         # length of game
263         self.klhere = 0         # klingons here
264         self.casual = 0         # causalties
265         self.nhelp = 0          # calls for help
266         self.nkinks = 0         # count of energy-barrier crossings
267         self.iplnet = 0         # planet # in quadrant
268         self.inplan = 0         # initial planets
269         self.nenhere = 0        # number of enemies in quadrant
270         self.irhere = 0         # Romulans in quadrant
271         self.isatb = 0          # =1 if super commander is attacking base
272         self.tourn = 0          # tournament number
273         self.proben = 0         # number of moves for probe
274         self.nprobes = 0        # number of probes available
275         self.inresor = 0.0      # initial resources
276         self.intime = 0.0       # initial time
277         self.inenrg = 0.0       # initial/max energy
278         self.inshld = 0.0       # initial/max shield
279         self.inlsr = 0.0        # initial life support resources
280         self.indate = 0.0       # initial date
281         self.energy = 0.0       # energy level
282         self.shield = 0.0       # shield level
283         self.warpfac = 0.0      # warp speed
284         self.wfacsq = 0.0       # squared warp factor
285         self.lsupres = 0.0      # life support reserves
286         self.dist = 0.0         # movement distance
287         self.direc = 0.0        # movement direction
288         self.optime = 0.0       # time taken by current operation
289         self.docfac = 0.0       # repair factor when docking (constant?)
290         self.damfac = 0.0       # damage factor
291         self.lastchart = 0.0    # time star chart was last updated
292         self.cryprob = 0.0      # probability that crystal will work
293         self.probex = 0.0       # location of probe
294         self.probey = 0.0       #
295         self.probeinx = 0.0     # probe x,y increment
296         self.probeiny = 0.0     #
297         self.height = 0.0       # height of orbit around planet
298
299 # From enumerated type 'feature'
300 IHR = 'R'
301 IHK = 'K'
302 IHC = 'C'
303 IHS = 'S'
304 IHSTAR = '*'
305 IHP = 'P'
306 IHW = '@'
307 IHB = 'B'
308 IHBLANK = ' '
309 IHDOT = '.'
310 IHQUEST = '?'
311 IHE = 'E'
312 IHF = 'F'
313 IHT = 'T'
314 IHWEB = '#'
315 IHMATER0 = '-'
316 IHMATER1 = 'o'
317 IHMATER2 = '0'
318
319
320 # From enumerated type 'FINTYPE'
321 FWON = 0
322 FDEPLETE = 1
323 FLIFESUP = 2
324 FNRG = 3
325 FBATTLE = 4
326 FNEG3 = 5
327 FNOVA = 6
328 FSNOVAED = 7
329 FABANDN = 8
330 FDILITHIUM = 9
331 FMATERIALIZE = 10
332 FPHASER = 11
333 FLOST = 12
334 FMINING = 13
335 FDPLANET = 14
336 FPNOVA = 15
337 FSSC = 16
338 FSTRACTOR = 17
339 FDRAY = 18
340 FTRIBBLE = 19
341 FHOLE = 20
342 FCREW = 21
343
344 # From enumerated type 'COLORS'
345 DEFAULT = 0
346 BLACK = 1
347 BLUE = 2
348 GREEN = 3
349 CYAN = 4
350 RED = 5
351 MAGENTA = 6
352 BROWN = 7
353 LIGHTGRAY = 8
354 DARKGRAY = 9
355 LIGHTBLUE = 10
356 LIGHTGREEN = 11
357 LIGHTCYAN = 12
358 LIGHTRED = 13
359 LIGHTMAGENTA = 14
360 YELLOW = 15
361 WHITE = 16
362
363 # Code from ai.c begins here
364
365 def tryexit(look, ienm, loccom, irun):
366     # a bad guy attempts to bug out 
367     iq = coord()
368     iq.x = game.quadrant.x+(look.x+(QUADSIZE-1))/QUADSIZE - 1
369     iq.y = game.quadrant.y+(look.y+(QUADSIZE-1))/QUADSIZE - 1
370     if not VALID_QUADRANT(iq.x,iq.y) or \
371         game.state.galaxy[iq.x][iq.y].supernova or \
372         game.state.galaxy[iq.x][iq.y].klingons > MAXKLQUAD-1:
373         return False; # no can do -- neg energy, supernovae, or >MAXKLQUAD-1 Klingons 
374     if ienm == IHR:
375         return False; # Romulans cannot escape! 
376     if not irun:
377         # avoid intruding on another commander's territory 
378         if ienm == IHC:
379             for n in range(1, game.state.remcom+1):
380                 if same(game.state.kcmdr[n],iq):
381                     return False
382             # refuse to leave if currently attacking starbase 
383             if same(game.battle, game.quadrant):
384                 return False
385         # don't leave if over 1000 units of energy 
386         if game.kpower[loccom] > 1000.0:
387             return False
388     # print escape message and move out of quadrant.
389     # we know this if either short or long range sensors are working
390     if not damaged(DSRSENS) or not damaged(DLRSENS) or \
391         game.condition == docked:
392         crmena(True, ienm, "sector", game.ks[loccom])
393         prout(_(" escapes to Quadrant %s (and regains strength).") % q)
394     # handle local matters related to escape 
395     game.quad[game.ks[loccom].x][game.ks[loccom].y] = IHDOT
396     game.ks[loccom] = game.ks[game.nenhere]
397     game.kavgd[loccom] = game.kavgd[game.nenhere]
398     game.kpower[loccom] = game.kpower[game.nenhere]
399     game.kdist[loccom] = game.kdist[game.nenhere]
400     game.klhere -= 1
401     game.nenhere -= 1
402     if game.condition != docked:
403         newcnd()
404     # Handle global matters related to escape 
405     game.state.galaxy[game.quadrant.x][game.quadrant.y].klingons -= 1
406     game.state.galaxy[iq.x][iq.y].klingons += 1
407     if ienm==IHS:
408         game.ishere = False
409         game.iscate = False
410         game.ientesc = False
411         game.isatb = 0
412         schedule(FSCMOVE, 0.2777)
413         unschedule(FSCDBAS)
414         game.state.kscmdr=iq
415     else:
416         for n in range(1, game.state.remcom+1):
417             if same(game.state.kcmdr[n], game.quadrant):
418                 game.state.kcmdr[n]=iq
419                 break
420         game.comhere = False
421     return True; # success 
422
423 #
424 # The bad-guy movement algorithm:
425
426 # 1. Enterprise has "force" based on condition of phaser and photon torpedoes.
427 # If both are operating full strength, force is 1000. If both are damaged,
428 # force is -1000. Having shields down subtracts an additional 1000.
429
430 # 2. Enemy has forces equal to the energy of the attacker plus
431 # 100*(K+R) + 500*(C+S) - 400 for novice through good levels OR
432 # 346*K + 400*R + 500*(C+S) - 400 for expert and emeritus.
433
434 # Attacker Initial energy levels (nominal):
435 # Klingon   Romulan   Commander   Super-Commander
436 # Novice    400        700        1200        
437 # Fair      425        750        1250
438 # Good      450        800        1300        1750
439 # Expert    475        850        1350        1875
440 # Emeritus  500        900        1400        2000
441 # VARIANCE   75        200         200         200
442
443 # Enemy vessels only move prior to their attack. In Novice - Good games
444 # only commanders move. In Expert games, all enemy vessels move if there
445 # is a commander present. In Emeritus games all enemy vessels move.
446
447 # 3. If Enterprise is not docked, an aggressive action is taken if enemy
448 # forces are 1000 greater than Enterprise.
449
450 # Agressive action on average cuts the distance between the ship and
451 # the enemy to 1/4 the original.
452
453 # 4.  At lower energy advantage, movement units are proportional to the
454 # advantage with a 650 advantage being to hold ground, 800 to move forward
455 # 1, 950 for two, 150 for back 4, etc. Variance of 100.
456
457 # If docked, is reduced by roughly 1.75*game.skill, generally forcing a
458 # retreat, especially at high skill levels.
459
460 # 5.  Motion is limited to skill level, except for SC hi-tailing it out.
461
462
463 def movebaddy(com, loccom, ienm):
464     # tactical movement for the bad guys 
465     next = coord(); look = coord()
466     irun = False
467     # This should probably be just game.comhere + game.ishere 
468     if game.skill >= SKILL_EXPERT:
469         nbaddys = ((game.comhere*2 + game.ishere*2+game.klhere*1.23+game.irhere*1.5)/2.0)
470     else:
471         nbaddys = game.comhere + game.ishere
472
473     dist1 = game.kdist[loccom]
474     mdist = int(dist1 + 0.5); # Nearest integer distance 
475
476     # If SC, check with spy to see if should hi-tail it 
477     if ienm==IHS and \
478         (game.kpower[loccom] <= 500.0 or (game.condition=="docked" and not damaged(DPHOTON))):
479         irun = True
480         motion = -QUADSIZE
481     else:
482         # decide whether to advance, retreat, or hold position 
483         forces = game.kpower[loccom]+100.0*game.nenhere+400*(nbaddys-1)
484         if not game.shldup:
485             forces += 1000; # Good for enemy if shield is down! 
486         if not damaged(DPHASER) or not damaged(DPHOTON):
487             if damaged(DPHASER): # phasers damaged 
488                 forces += 300.0
489             else:
490                 forces -= 0.2*(game.energy - 2500.0)
491             if damaged(DPHOTON): # photon torpedoes damaged 
492                 forces += 300.0
493             else:
494                 forces -= 50.0*game.torps
495         else:
496             # phasers and photon tubes both out! 
497             forces += 1000.0
498         motion = 0
499         if forces <= 1000.0 and game.condition != "docked": # Typical situation 
500             motion = ((forces+200.0*Rand())/150.0) - 5.0
501         else:
502             if forces > 1000.0: # Very strong -- move in for kill 
503                 motion = (1.0-square(Rand()))*dist1 + 1.0
504             if game.condition=="docked" and (game.options & OPTION_BASE): # protected by base -- back off ! 
505                 motion -= game.skill*(2.0-square(Rand()))
506         if idebug:
507             proutn("=== MOTION = %d, FORCES = %1.2f, " % (motion, forces))
508         # don't move if no motion 
509         if motion==0:
510             return
511         # Limit motion according to skill 
512         if abs(motion) > game.skill:
513             if motion < 0:
514                 motion = -game.skill
515             else:
516                 motion = game.skill
517     # calculate preferred number of steps 
518     if motion < 0:
519         msteps = -motion
520     else:
521         msteps = motion
522     if motion > 0 and nsteps > mdist:
523         nsteps = mdist; # don't overshoot 
524     if nsteps > QUADSIZE:
525         nsteps = QUADSIZE; # This shouldn't be necessary 
526     if nsteps < 1:
527         nsteps = 1; # This shouldn't be necessary 
528     if idebug:
529         proutn("NSTEPS = %d:" % nsteps)
530     # Compute preferred values of delta X and Y 
531     mx = game.sector.x - com.x
532     my = game.sector.y - com.y
533     if 2.0 * abs(mx) < abs(my):
534         mx = 0
535     if 2.0 * abs(my) < abs(game.sector.x-com.x):
536         my = 0
537     if mx != 0:
538         if mx*motion < 0:
539             mx = -1
540         else:
541             mx = 1
542     if my != 0:
543         if my*motion < 0:
544             my = -1
545         else:
546             my = 1
547     next = com
548     # main move loop 
549     for ll in range(nsteps):
550         if idebug:
551             proutn(" %d" % (ll+1))
552         # Check if preferred position available 
553         look.x = next.x + mx
554         look.y = next.y + my
555         if mx < 0:
556             krawlx = 1
557         else:
558             krawlx = -1
559         if my < 0:
560             krawly = 1
561         else:
562             krawly = -1
563         success = False
564         attempts = 0; # Settle mysterious hang problem 
565         while attempts < 20 and not success:
566             attempts += 1
567             if look.x < 1 or look.x > QUADSIZE:
568                 if motion < 0 and tryexit(look, ienm, loccom, irun):
569                     return
570                 if krawlx == mx or my == 0:
571                     break
572                 look.x = next.x + krawlx
573                 krawlx = -krawlx
574             elif look.y < 1 or look.y > QUADSIZE:
575                 if motion < 0 and tryexit(look, ienm, loccom, irun):
576                     return
577                 if krawly == my or mx == 0:
578                     break
579                 look.y = next.y + krawly
580                 krawly = -krawly
581             elif (game.options & OPTION_RAMMING) and game.quad[look.x][look.y] != IHDOT:
582                 # See if we should ram ship 
583                 if game.quad[look.x][look.y] == game.ship and \
584                     (ienm == IHC or ienm == IHS):
585                     ram(True, ienm, com)
586                     return
587                 if krawlx != mx and my != 0:
588                     look.x = next.x + krawlx
589                     krawlx = -krawlx
590                 elif krawly != my and mx != 0:
591                     look.y = next.y + krawly
592                     krawly = -krawly
593                 else:
594                     break; # we have failed 
595             else:
596                 success = True
597         if success:
598             next = look
599             if idebug:
600                 proutn(`next`)
601         else:
602             break; # done early 
603         
604     if idebug:
605         skip(1)
606     # Put commander in place within same quadrant 
607     game.quad[com.x][com.y] = IHDOT
608     game.quad[next.x][next.y] = ienm
609     if not same(next, com):
610         # it moved 
611         game.ks[loccom] = next
612         game.kdist[loccom] = game.kavgd[loccom] = distance(game.sector, next)
613         if not damaged(DSRSENS) or game.condition == docked:
614             proutn("***")
615             cramen(ienm)
616             proutn(_(" from Sector %s") % com)
617             if game.kdist[loccom] < dist1:
618                 proutn(_(" advances to "))
619             else:
620                 proutn(_(" retreats to "))
621             prout("Sector %s." % next)
622
623 def moveklings():
624     # Klingon tactical movement 
625     if idebug:
626         prout("== MOVCOM")
627     # Figure out which Klingon is the commander (or Supercommander)
628     # and do move
629     if game.comhere:
630         for i in range(1, game.nenhere+1):
631             w = game.ks[i]
632             if game.quad[w.x][w.y] == IHC:
633                 movebaddy(w, i, IHC)
634                 break
635     if game.ishere:
636         for i in range(1, game.nenhere+1):
637             w = game.ks[i]
638             if game.quad[w.x][w.y] == IHS:
639                 movebaddy(w, i, IHS)
640                 break
641     # If skill level is high, move other Klingons and Romulans too!
642     # Move these last so they can base their actions on what the
643     # commander(s) do.
644     if game.skill >= SKILL_EXPERT and (game.options & OPTION_MVBADDY):
645         for i in range(1, game.nenhere+1):
646             w = game.ks[i]
647             if game.quad[w.x][w.y] == IHK or game.quad[w.x][w.y] == IHR:
648                 movebaddy(w, i, game.quad[w.x][w.y])
649     sortklings();
650
651 def movescom(iq, avoid):
652     # commander movement helper 
653     if same(iq, game.quadrant) or not VALID_QUADRANT(iq.x, iq.y) or \
654         game.state.galaxy[iq.x][iq.y].supernova or \
655         game.state.galaxy[iq.x][iq.y].klingons > MAXKLQUAD-1:
656         return 1
657     if avoid:
658         # Avoid quadrants with bases if we want to avoid Enterprise 
659         for i in range(1, game.state.rembase+1):
660             if same(game.state.baseq[i], iq):
661                 return True
662     if game.justin and not game.iscate:
663         return True
664     # do the move 
665     game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].klingons -= 1
666     game.state.kscmdr = iq
667     game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].klingons += 1
668     if game.ishere:
669         # SC has scooted, Remove him from current quadrant 
670         game.iscate=False
671         game.isatb=0
672         game.ishere = False
673         game.ientesc = False
674         unschedule(FSCDBAS)
675         for i in range(1, game.nenhere+1):
676             if game.quad[game.ks[i].x][game.ks[i].y] == IHS:
677                 break
678         game.quad[game.ks[i].x][game.ks[i].y] = IHDOT
679         game.ks[i] = game.ks[game.nenhere]
680         game.kdist[i] = game.kdist[game.nenhere]
681         game.kavgd[i] = game.kavgd[game.nenhere]
682         game.kpower[i] = game.kpower[game.nenhere]
683         game.klhere -= 1
684         game.nenhere -= 1
685         if game.condition!=docked:
686             newcnd()
687         sortklings()
688     # check for a helpful planet 
689     for i in range(game.inplan):
690         if same(game.state.planets[i].w, game.state.kscmdr) and \
691             game.state.planets[i].crystals == present:
692             # destroy the planet 
693             game.state.planets[i].pclass = destroyed
694             game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].planet = NOPLANET
695             if not damaged(DRADIO) or game.condition == docked:
696                 pause_game(True)
697                 prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
698                 proutn(_("   a planet in Quadrant %s has been destroyed") % game.state.kscmdr)
699                 prout(_("   by the Super-commander.\""))
700             break
701     return False; # looks good! 
702                         
703 def supercommander():
704     # move the Super Commander 
705     iq = coord(); sc = coord(); ibq = coord(); idelta = coord()
706     basetbl = []
707     if idebug:
708         prout("== SUPERCOMMANDER")
709     # Decide on being active or passive 
710     avoid = ((game.incom - game.state.remcom + game.inkling - game.state.remkl)/(game.state.date+0.01-game.indate) < 0.1*game.skill*(game.skill+1.0) or \
711             (game.state.date-game.indate) < 3.0)
712     if not game.iscate and avoid:
713         # compute move away from Enterprise 
714         idelta = game.state.kscmdr-game.quadrant
715         if math.sqrt(idelta.x*idelta.x+idelta.y*idelta.y) > 2.0:
716             # circulate in space 
717             idelta.x = game.state.kscmdr.y-game.quadrant.y
718             idelta.y = game.quadrant.x-game.state.kscmdr.x
719     else:
720         # compute distances to starbases 
721         if game.state.rembase <= 0:
722             # nothing left to do 
723             unschedule(FSCMOVE)
724             return
725         sc = game.state.kscmdr
726         for i in range(1, game.state.rembase+1):
727             basetbl.append((i, distance(game.state.baseq[i], sc)))
728         if game.state.rembase > 1:
729             basetbl.sort(lambda x, y: cmp(x[1]. y[1]))
730         # look for nearest base without a commander, no Enterprise, and
731         # without too many Klingons, and not already under attack. 
732         ifindit = iwhichb = 0
733         for i2 in range(1, game.state.rembase+1):
734             i = basetbl[i2][0]; # bug in original had it not finding nearest
735             ibq = game.state.baseq[i]
736             if same(ibq, game.quadrant) or same(ibq, game.battle) or \
737                 game.state.galaxy[ibq.x][ibq.y].supernova or \
738                 game.state.galaxy[ibq.x][ibq.y].klingons > MAXKLQUAD-1:
739                 continue
740             # if there is a commander, and no other base is appropriate,
741             #   we will take the one with the commander
742             for j in range(1, game.state.remcom+1):
743                 if same(ibq, game.state.kcmdr[j]) and ifindit!= 2:
744                     ifindit = 2
745                     iwhichb = i
746                     break
747             if j > game.state.remcom: # no commander -- use this one 
748                 ifindit = 1
749                 iwhichb = i
750                 break
751         if ifindit==0:
752             return; # Nothing suitable -- wait until next time
753         ibq = game.state.baseq[iwhichb]
754         # decide how to move toward base 
755         idelta = ibq - game.state.kscmdr
756     # Maximum movement is 1 quadrant in either or both axes 
757     idelta = idelta.sgn()
758     # try moving in both x and y directions
759     # there was what looked like a bug in the Almy C code here,
760     # but it might be this translation is just wrong.
761     iq = game.state.kscmdr + idelta
762     if movescom(iq, avoid):
763         # failed -- try some other maneuvers 
764         if idelta.x==0 or idelta.y==0:
765             # attempt angle move 
766             if idelta.x != 0:
767                 iq.y = game.state.kscmdr.y + 1
768                 if movescom(iq, avoid):
769                     iq.y = game.state.kscmdr.y - 1
770                     movescom(iq, avoid)
771             else:
772                 iq.x = game.state.kscmdr.x + 1
773                 if movescom(iq, avoid):
774                     iq.x = game.state.kscmdr.x - 1
775                     movescom(iq, avoid)
776         else:
777             # try moving just in x or y 
778             iq.y = game.state.kscmdr.y
779             if movescom(iq, avoid):
780                 iq.y = game.state.kscmdr.y + idelta.y
781                 iq.x = game.state.kscmdr.x
782                 movescom(iq, avoid)
783     # check for a base 
784     if game.state.rembase == 0:
785         unschedule(FSCMOVE)
786     else:
787         for i in range(1, game.state.rembase+1):
788             ibq = game.state.baseq[i]
789             if same(ibq, game.state.kscmdr) and same(game.state.kscmdr, game.battle):
790                 # attack the base 
791                 if avoid:
792                     return; # no, don't attack base! 
793                 game.iseenit = False
794                 game.isatb = 1
795                 schedule(FSCDBAS, 1.0 +2.0*Rand())
796                 if is_scheduled(FCDBAS):
797                     postpone(FSCDBAS, scheduled(FCDBAS)-game.state.date)
798                 if damaged(DRADIO) and game.condition != docked:
799                     return; # no warning 
800                 game.iseenit = True
801                 pause_game(True)
802                 prout(_("Lt. Uhura-  \"Captain, the starbase in Quadrant %s") \
803                       % game.state.kscmdr)
804                 prout(_("   reports that it is under attack from the Klingon Super-commander."))
805                 proutn(_("   It can survive until stardate %d.\"") \
806                        % int(scheduled(FSCDBAS)))
807                 if not game.resting:
808                     return
809                 prout(_("Mr. Spock-  \"Captain, shall we cancel the rest period?\""))
810                 if ja() == False:
811                     return
812                 game.resting = False
813                 game.optime = 0.0; # actually finished 
814                 return
815     # Check for intelligence report 
816     if not idebug and \
817         (Rand() > 0.2 or \
818          (damaged(DRADIO) and game.condition != docked) or \
819          not game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].charted):
820         return
821     pause_game(True)
822     prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
823     proutn(_("   the Super-commander is in Quadrant %s,") % game.state.kscmdr)
824     return;
825
826 def movetholian():
827     # move the Tholian 
828     if not game.ithere or game.justin:
829         return
830
831     if game.tholian.x == 1 and game.tholian.y == 1:
832         idx = 1; idy = QUADSIZE
833     elif game.tholian.x == 1 and game.tholian.y == QUADSIZE:
834         idx = QUADSIZE; idy = QUADSIZE
835     elif game.tholian.x == QUADSIZE and game.tholian.y == QUADSIZE:
836         idx = QUADSIZE; idy = 1
837     elif game.tholian.x == QUADSIZE and game.tholian.y == 1:
838         idx = 1; idy = 1
839     else:
840         # something is wrong! 
841         game.ithere = False
842         return
843
844     # do nothing if we are blocked 
845     if game.quad[idx][idy]!= IHDOT and game.quad[idx][idy]!= IHWEB:
846         return
847     game.quad[game.tholian.x][game.tholian.y] = IHWEB
848
849     if game.tholian.x != idx:
850         # move in x axis 
851         im = math.fabs(idx - game.tholian.x)*1.0/(idx - game.tholian.x)
852         while game.tholian.x != idx:
853             game.tholian.x += im
854             if game.quad[game.tholian.x][game.tholian.y]==IHDOT:
855                 game.quad[game.tholian.x][game.tholian.y] = IHWEB
856     elif game.tholian.y != idy:
857         # move in y axis 
858         im = math.fabs(idy - game.tholian.y)*1.0/(idy - game.tholian.y)
859         while game.tholian.y != idy:
860             game.tholian.y += im
861             if game.quad[game.tholian.x][game.tholian.y]==IHDOT:
862                 game.quad[game.tholian.x][game.tholian.y] = IHWEB
863     game.quad[game.tholian.x][game.tholian.y] = IHT
864     game.ks[game.nenhere] = game.tholian
865
866     # check to see if all holes plugged 
867     for i in range(1, QUADSIZE+1):
868         if game.quad[1][i]!=IHWEB and game.quad[1][i]!=IHT:
869             return
870         if game.quad[QUADSIZE][i]!=IHWEB and game.quad[QUADSIZE][i]!=IHT:
871             return
872         if game.quad[i][1]!=IHWEB and game.quad[i][1]!=IHT:
873             return
874         if game.quad[i][QUADSIZE]!=IHWEB and game.quad[i][QUADSIZE]!=IHT:
875             return
876     # All plugged up -- Tholian splits 
877     game.quad[game.tholian.x][game.tholian.y]=IHWEB
878     dropin(IHBLANK)
879     crmena(True, IHT, "sector", game.tholian)
880     prout(_(" completes web."))
881     game.ithere = False
882     game.nenhere -= 1
883     return