c921a496098863855457b206a89607e40b35db9f
[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
369     iq.x = game.quadrant.x+(look.x+(QUADSIZE-1))/QUADSIZE - 1
370     iq.y = game.quadrant.y+(look.y+(QUADSIZE-1))/QUADSIZE - 1
371     if not VALID_QUADRANT(iq.x,iq.y) or \
372         game.state.galaxy[iq.x][iq.y].supernova or \
373         game.state.galaxy[iq.x][iq.y].klingons > MAXKLQUAD-1:
374         return False; # no can do -- neg energy, supernovae, or >MAXKLQUAD-1 Klingons 
375     if ienm == IHR:
376         return False; # Romulans cannot escape! 
377     if not irun:
378         # avoid intruding on another commander's territory 
379         if ienm == IHC:
380             for n in range(1, game.state.remcom+1):
381                 if same(game.state.kcmdr[n],iq):
382                     return False
383             # refuse to leave if currently attacking starbase 
384             if same(game.battle, game.quadrant):
385                 return False
386         # don't leave if over 1000 units of energy 
387         if game.kpower[loccom] > 1000.0:
388             return False
389     # print escape message and move out of quadrant.
390     # We know this if either short or long range sensors are working
391     if not damaged(DSRSENS) or not damaged(DLRSENS) or \
392         game.condition == docked:
393         crmena(True, ienm, sector, game.ks[loccom])
394         prout(_(" escapes to %s (and regains strength)."),
395               cramlc(quadrant, iq))
396     # handle local matters related to escape 
397     game.quad[game.ks[loccom].x][game.ks[loccom].y] = IHDOT
398     game.ks[loccom] = game.ks[game.nenhere]
399     game.kavgd[loccom] = game.kavgd[game.nenhere]
400     game.kpower[loccom] = game.kpower[game.nenhere]
401     game.kdist[loccom] = game.kdist[game.nenhere]
402     game.klhere -= 1
403     game.nenhere -= 1
404     if game.condition != docked:
405         newcnd()
406     # Handle global matters related to escape 
407     game.state.galaxy[game.quadrant.x][game.quadrant.y].klingons -= 1
408     game.state.galaxy[iq.x][iq.y].klingons += 1
409     if ienm==IHS:
410         game.ishere = False
411         game.iscate = False
412         game.ientesc = False
413         game.isatb = 0
414         schedule(FSCMOVE, 0.2777)
415         unschedule(FSCDBAS)
416         game.state.kscmdr=iq
417     else:
418         for n in range(1, game.state.remcom+1):
419             if same(game.state.kcmdr[n], game.quadrant):
420                 game.state.kcmdr[n]=iq
421                 break
422         game.comhere = False
423     return True; # success 
424
425 #
426 # The bad-guy movement algorithm:
427
428 # 1. Enterprise has "force" based on condition of phaser and photon torpedoes.
429 # If both are operating full strength, force is 1000. If both are damaged,
430 # force is -1000. Having shields down subtracts an additional 1000.
431
432 # 2. Enemy has forces equal to the energy of the attacker plus
433 # 100*(K+R) + 500*(C+S) - 400 for novice through good levels OR
434 # 346*K + 400*R + 500*(C+S) - 400 for expert and emeritus.
435
436 # Attacker Initial energy levels (nominal):
437 # Klingon   Romulan   Commander   Super-Commander
438 # Novice    400        700        1200        
439 # Fair      425        750        1250
440 # Good      450        800        1300        1750
441 # Expert    475        850        1350        1875
442 # Emeritus  500        900        1400        2000
443 # VARIANCE   75        200         200         200
444
445 # Enemy vessels only move prior to their attack. In Novice - Good games
446 # only commanders move. In Expert games, all enemy vessels move if there
447 # is a commander present. In Emeritus games all enemy vessels move.
448
449 # 3. If Enterprise is not docked, an agressive action is taken if enemy
450 # forces are 1000 greater than Enterprise.
451
452 # Agressive action on average cuts the distance between the ship and
453 # the enemy to 1/4 the original.
454
455 # 4.  At lower energy advantage, movement units are proportional to the
456 # advantage with a 650 advantage being to hold ground, 800 to move forward
457 # 1, 950 for two, 150 for back 4, etc. Variance of 100.
458
459 # If docked, is reduced by roughly 1.75*game.skill, generally forcing a
460 # retreat, especially at high skill levels.
461
462 # 5.  Motion is limited to skill level, except for SC hi-tailing it out.
463
464
465 def movebaddy(com, loccom, ienm):
466     # tactical movement for the bad guys 
467     next = coord(); look = coord()
468     irun = False
469     # This should probably be just game.comhere + game.ishere 
470     if game.skill >= SKILL_EXPERT:
471         nbaddys = ((game.comhere*2 + game.ishere*2+game.klhere*1.23+game.irhere*1.5)/2.0)
472     else:
473         nbaddys = game.comhere + game.ishere
474
475     dist1 = game.kdist[loccom]
476     mdist = dist1 + 0.5; # Nearest integer distance 
477
478     # If SC, check with spy to see if should hi-tail it 
479     if ienm==IHS and \
480         (game.kpower[loccom] <= 500.0 or (game.condition==docked and not damaged(DPHOTON))):
481         irun = True
482         motion = -QUADSIZE
483     else:
484         # decide whether to advance, retreat, or hold position 
485         forces = game.kpower[loccom]+100.0*game.nenhere+400*(nbaddys-1)
486         if not game.shldup:
487             forces += 1000; # Good for enemy if shield is down! 
488         if not damaged(DPHASER) or not damaged(DPHOTON):
489             if damaged(DPHASER): # phasers damaged 
490                 forces += 300.0
491             else:
492                 forces -= 0.2*(game.energy - 2500.0)
493             if damaged(DPHOTON): # photon torpedoes damaged 
494                 forces += 300.0
495             else:
496                 forces -= 50.0*game.torps
497         else:
498             # phasers and photon tubes both out! 
499             forces += 1000.0
500         motion = 0
501         if forces <= 1000.0 and game.condition != docked: # Typical situation 
502             motion = ((forces+200.0*Rand())/150.0) - 5.0
503         else:
504             if forces > 1000.0: # Very strong -- move in for kill 
505                 motion = (1.0-square(Rand()))*dist1 + 1.0
506             if game.condition=="docked" and (game.options & OPTION_BASE): # protected by base -- back off ! 
507                 motion -= game.skill*(2.0-square(Rand()))
508         if idebug:
509             proutn("=== MOTION = %d, FORCES = %1.2f, ", motion, forces)
510         # don't move if no motion 
511         if motion==0:
512             return
513         # Limit motion according to skill 
514         if abs(motion) > game.skill:
515             if motion < 0:
516                 motion = -game.skill
517             else:
518                 motion = game.skill
519     # calculate preferred number of steps 
520     if motion < 0:
521         msteps = -motion
522     else:
523         msteps = motion
524     if motion > 0 and nsteps > mdist:
525         nsteps = mdist; # don't overshoot 
526     if nsteps > QUADSIZE:
527         nsteps = QUADSIZE; # This shouldn't be necessary 
528     if nsteps < 1:
529         nsteps = 1; # This shouldn't be necessary 
530     if idebug:
531         proutn("NSTEPS = %d:", nsteps)
532     # Compute preferred values of delta X and Y 
533     mx = game.sector.x - com.x
534     my = game.sector.y - com.y
535     if 2.0 * abs(mx) < abs(my):
536         mx = 0
537     if 2.0 * abs(my) < abs(game.sector.x-com.x):
538         my = 0
539     if mx != 0:
540         if mx*motion < 0:
541             mx = -1
542         else:
543             mx = 1
544     if my != 0:
545         if my*motion < 0:
546             my = -1
547         else:
548             my = 1
549     next = com
550     # main move loop 
551     for ll in range(nsteps):
552         if idebug:
553             proutn(" %d", ll+1)
554         # Check if preferred position available 
555         look.x = next.x + mx
556         look.y = next.y + my
557         if mx < 0:
558             krawlx = 1
559         else:
560             krawlx = -1
561         if my < 0:
562             krawly = 1
563         else:
564             krawly = -1
565         success = False
566         attempts = 0; # Settle mysterious hang problem 
567         while attempts < 20 and not success:
568             attempts += 1
569             if look.x < 1 or look.x > QUADSIZE:
570                 if motion < 0 and tryexit(look, ienm, loccom, irun):
571                     return
572                 if krawlx == mx or my == 0:
573                     break
574                 look.x = next.x + krawlx
575                 krawlx = -krawlx
576             elif look.y < 1 or look.y > QUADSIZE:
577                 if motion < 0 and tryexit(look, ienm, loccom, irun):
578                     return
579                 if krawly == my or mx == 0:
580                     break
581                 look.y = next.y + krawly
582                 krawly = -krawly
583             elif (game.options & OPTION_RAMMING) and game.quad[look.x][look.y] != IHDOT:
584                 # See if we should ram ship 
585                 if game.quad[look.x][look.y] == game.ship and \
586                     (ienm == IHC or ienm == IHS):
587                     ram(True, ienm, com)
588                     return
589                 if krawlx != mx and my != 0:
590                     look.x = next.x + krawlx
591                     krawlx = -krawlx
592                 elif krawly != my and mx != 0:
593                     look.y = next.y + krawly
594                     krawly = -krawly
595                 else:
596                     break; # we have failed 
597             else:
598                 success = True
599         if success:
600             next = look
601             if idebug:
602                 proutn(cramlc(neither, next))
603         else:
604             break; # done early 
605         
606     if idebug:
607         skip(1)
608     # Put commander in place within same quadrant 
609     game.quad[com.x][com.y] = IHDOT
610     game.quad[next.x][next.y] = ienm
611     if not same(next, com):
612         # it moved 
613         game.ks[loccom] = next
614         game.kdist[loccom] = game.kavgd[loccom] = distance(game.sector, next)
615         if not damaged(DSRSENS) or game.condition == docked:
616             proutn("***")
617             cramen(ienm)
618             proutn(_(" from %s"), cramlc(2, com))
619             if game.kdist[loccom] < dist1:
620                 proutn(_(" advances to "))
621             else:
622                 proutn(_(" retreats to "))
623             prout(cramlc(sector, next))
624
625 def moveklings():
626     # Klingon tactical movement 
627     w = coord(); 
628
629     if idebug:
630         prout("== MOVCOM")
631
632     # Figure out which Klingon is the commander (or Supercommander)
633     #   and do move
634     if game.comhere:
635         for i in range(1, game.nenhere+1):
636             w = game.ks[i]
637             if game.quad[w.x][w.y] == IHC:
638                 movebaddy(w, i, IHC)
639                 break
640     if game.ishere:
641         for i in range(1, game.nenhere+1):
642             w = game.ks[i]
643             if game.quad[w.x][w.y] == IHS:
644                 movebaddy(w, i, IHS)
645                 break
646     # if skill level is high, move other Klingons and Romulans too!
647     # Move these last so they can base their actions on what the
648     # commander(s) do.
649     if game.skill >= SKILL_EXPERT and (game.options & OPTION_MVBADDY):
650         for i in range(1, game.nenhere+1):
651             w = game.ks[i]
652             if game.quad[w.x][w.y] == IHK or game.quad[w.x][w.y] == IHR:
653                 movebaddy(w, i, game.quad[w.x][w.y])
654     sortklings();
655
656 def movescom(iq, avoid):
657     # commander movement helper 
658
659     if same(iq, game.quadrant) or not VALID_QUADRANT(iq.x, iq.y) or \
660         game.state.galaxy[iq.x][iq.y].supernova or \
661         game.state.galaxy[iq.x][iq.y].klingons > MAXKLQUAD-1:
662         return 1
663     if avoid:
664         # Avoid quadrants with bases if we want to avoid Enterprise 
665         for i in range(1, game.state.rembase+1):
666             if same(game.state.baseq[i], iq):
667                 return True
668     if game.justin and not game.iscate:
669         return True
670     # do the move 
671     game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].klingons -= 1
672     game.state.kscmdr = iq
673     game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].klingons += 1
674     if game.ishere:
675         # SC has scooted, Remove him from current quadrant 
676         game.iscate=False
677         game.isatb=0
678         game.ishere = False
679         game.ientesc = False
680         unschedule(FSCDBAS)
681         for i in range(1, game.nenhere+1):
682             if game.quad[game.ks[i].x][game.ks[i].y] == IHS:
683                 break
684         game.quad[game.ks[i].x][game.ks[i].y] = IHDOT
685         game.ks[i] = game.ks[game.nenhere]
686         game.kdist[i] = game.kdist[game.nenhere]
687         game.kavgd[i] = game.kavgd[game.nenhere]
688         game.kpower[i] = game.kpower[game.nenhere]
689         game.klhere -= 1
690         game.nenhere -= 1
691         if game.condition!=docked:
692             newcnd()
693         sortklings()
694     # check for a helpful planet 
695     for i in range(game.inplan):
696         if same(game.state.planets[i].w, game.state.kscmdr) and \
697             game.state.planets[i].crystals == present:
698             # destroy the planet 
699             game.state.planets[i].pclass = destroyed
700             game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].planet = NOPLANET
701             if not damaged(DRADIO) or game.condition == docked:
702                 pause_game(True)
703                 prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
704                 proutn(_("   a planet in "))
705                 proutn(cramlc(quadrant, game.state.kscmdr))
706                 prout(_(" has been destroyed"))
707                 prout(_("   by the Super-commander.\""))
708             break
709     return False; # looks good! 
710                         
711 def supercommander():
712     # move the Super Commander 
713     iq = coord(); sc = coord(); ibq = coord()
714     basetbl = []
715
716     if idebug:
717         prout("== SUPERCOMMANDER")
718
719     # Decide on being active or passive 
720     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 \
721             (game.state.date-game.indate) < 3.0)
722     if not game.iscate and avoid:
723         # compute move away from Enterprise 
724         ideltax = game.state.kscmdr.x-game.quadrant.x
725         ideltay = game.state.kscmdr.y-game.quadrant.y
726         if math.sqrt(ideltax*ideltax+ideltay*ideltay) > 2.0:
727             # circulate in space 
728             ideltax = game.state.kscmdr.y-game.quadrant.y
729             ideltay = game.quadrant.x-game.state.kscmdr.x
730     else:
731         # compute distances to starbases 
732         if game.state.rembase <= 0:
733             # nothing left to do 
734             unschedule(FSCMOVE)
735             return
736         sc = game.state.kscmdr
737         for i in range(1, game.state.rembase+1):
738             basetbl.append((i, distance(game.state.baseq[i], sc)))
739         if game.state.rembase > 1:
740             basetbl.sort(lambda x, y: cmp(x[1]. y[1]))
741         # look for nearest base without a commander, no Enterprise, and
742         # without too many Klingons, and not already under attack. 
743         ifindit = iwhichb = 0
744
745         for i2 in range(1, game.state.rembase+1):
746             i = basetbl[i2][0]; # bug in original had it not finding nearest
747             ibq = game.state.baseq[i]
748             if same(ibq, game.quadrant) or same(ibq, game.battle) or \
749                 game.state.galaxy[ibq.x][ibq.y].supernova or \
750                 game.state.galaxy[ibq.x][ibq.y].klingons > MAXKLQUAD-1:
751                 continue
752             # if there is a commander, and no other base is appropriate,
753             #   we will take the one with the commander
754             for j in range(1, game.state.remcom+1):
755                 if same(ibq, game.state.kcmdr[j]) and ifindit!= 2:
756                     ifindit = 2
757                     iwhichb = i
758                     break
759             if j > game.state.remcom: # no commander -- use this one 
760                 ifindit = 1
761                 iwhichb = i
762                 break
763         if ifindit==0:
764             return; # Nothing suitable -- wait until next time
765         ibq = game.state.baseq[iwhichb]
766         # decide how to move toward base 
767         ideltax = ibq.x - game.state.kscmdr.x
768         ideltay = ibq.y - game.state.kscmdr.y
769     # Maximum movement is 1 quadrant in either or both axis 
770     if ideltax > 1:
771         ideltax = 1
772     if ideltax < -1:
773         ideltax = -1
774     if ideltay > 1:
775         ideltay = 1
776     if ideltay < -1:
777         ideltay = -1
778
779     # try moving in both x and y directions 
780     iq.x = game.state.kscmdr.x + ideltax
781     iq.y = game.state.kscmdr.y + ideltax
782     if movescom(iq, avoid):
783         # failed -- try some other maneuvers 
784         if ideltax==0 or ideltay==0:
785             # attempt angle move 
786             if ideltax != 0:
787                 iq.y = game.state.kscmdr.y + 1
788                 if movescom(iq, avoid):
789                     iq.y = game.state.kscmdr.y - 1
790                     movescom(iq, avoid)
791             else:
792                 iq.x = game.state.kscmdr.x + 1
793                 if movescom(iq, avoid):
794                     iq.x = game.state.kscmdr.x - 1
795                     movescom(iq, avoid)
796         else:
797             # try moving just in x or y 
798             iq.y = game.state.kscmdr.y
799             if movescom(iq, avoid):
800                 iq.y = game.state.kscmdr.y + ideltay
801                 iq.x = game.state.kscmdr.x
802                 movescom(iq, avoid)
803     # check for a base 
804     if game.state.rembase == 0:
805         unschedule(FSCMOVE)
806     else:
807         for i in range(1, game.state.rembase+1):
808             ibq = game.state.baseq[i]
809             if same(ibq, game.state.kscmdr) and same(game.state.kscmdr, game.battle):
810                 # attack the base 
811                 if avoid:
812                     return; # no, don't attack base! 
813                 game.iseenit = False
814                 game.isatb = 1
815                 schedule(FSCDBAS, 1.0 +2.0*Rand())
816                 if is_scheduled(FCDBAS):
817                     postpone(FSCDBAS, scheduled(FCDBAS)-game.state.date)
818                 if damaged(DRADIO) and game.condition != docked:
819                     return; # no warning 
820                 game.iseenit = True
821                 pause_game(True)
822                 proutn(_("Lt. Uhura-  \"Captain, the starbase in "))
823                 proutn(cramlc(quadrant, game.state.kscmdr))
824                 skip(1)
825                 prout(_("   reports that it is under attack from the Klingon Super-commander."))
826                 proutn(_("   It can survive until stardate %d.\""),
827                        int(scheduled(FSCDBAS)))
828                 if not game.resting:
829                     return
830                 prout(_("Mr. Spock-  \"Captain, shall we cancel the rest period?\""))
831                 if ja() == False:
832                     return
833                 game.resting = False
834                 game.optime = 0.0; # actually finished 
835                 return
836     # Check for intelligence report 
837     if not idebug and \
838         (Rand() > 0.2 or \
839          (damaged(DRADIO) and game.condition != docked) or \
840          not game.state.galaxy[game.state.kscmdr.x][game.state.kscmdr.y].charted):
841         return
842     pause_game(True)
843     prout(_("Lt. Uhura-  \"Captain, Starfleet Intelligence reports"))
844     proutn(_("   the Super-commander is in "))
845     proutn(cramlc(quadrant, game.state.kscmdr))
846     prout(".\"")
847     return;
848
849 def movetholian():
850     # move the Tholian 
851     if not game.ithere or game.justin:
852         return
853
854     if game.tholian.x == 1 and game.tholian.y == 1:
855         idx = 1; idy = QUADSIZE
856     elif game.tholian.x == 1 and game.tholian.y == QUADSIZE:
857         idx = QUADSIZE; idy = QUADSIZE
858     elif game.tholian.x == QUADSIZE and game.tholian.y == QUADSIZE:
859         idx = QUADSIZE; idy = 1
860     elif game.tholian.x == QUADSIZE and game.tholian.y == 1:
861         idx = 1; idy = 1
862     else:
863         # something is wrong! 
864         game.ithere = False
865         return
866
867     # do nothing if we are blocked 
868     if game.quad[idx][idy]!= IHDOT and game.quad[idx][idy]!= IHWEB:
869         return
870     game.quad[game.tholian.x][game.tholian.y] = IHWEB
871
872     if game.tholian.x != idx:
873         # move in x axis 
874         im = math.fabs(idx - game.tholian.x)*1.0/(idx - game.tholian.x)
875         while game.tholian.x != idx:
876             game.tholian.x += im
877             if game.quad[game.tholian.x][game.tholian.y]==IHDOT:
878                 game.quad[game.tholian.x][game.tholian.y] = IHWEB
879     elif game.tholian.y != idy:
880         # move in y axis 
881         im = math.fabs(idy - game.tholian.y)*1.0/(idy - game.tholian.y)
882         while game.tholian.y != idy:
883             game.tholian.y += im
884             if game.quad[game.tholian.x][game.tholian.y]==IHDOT:
885                 game.quad[game.tholian.x][game.tholian.y] = IHWEB
886     game.quad[game.tholian.x][game.tholian.y] = IHT
887     game.ks[game.nenhere] = game.tholian
888
889     # check to see if all holes plugged 
890     for i in range(1, QUADSIZE+1):
891         if game.quad[1][i]!=IHWEB and game.quad[1][i]!=IHT:
892             return
893         if game.quad[QUADSIZE][i]!=IHWEB and game.quad[QUADSIZE][i]!=IHT:
894             return
895         if game.quad[i][1]!=IHWEB and game.quad[i][1]!=IHT:
896             return
897         if game.quad[i][QUADSIZE]!=IHWEB and game.quad[i][QUADSIZE]!=IHT:
898             return
899     # All plugged up -- Tholian splits 
900     game.quad[game.tholian.x][game.tholian.y]=IHWEB
901     dropin(IHBLANK)
902     crmena(True, IHT, sector, game.tholian)
903     prout(_(" completes web."))
904     game.ithere = False
905     game.nenhere -= 1
906     return