2 sst.py =-- Super Star Trek in Python
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.
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.
18 NINHAB = GALSIZE * GALSIZE / 2
20 PLNETMAB = NINHAB + MAXUNINHAB
23 FULLCREW = 428 # BSD Trek was 387, that's wrong
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)
33 # These types have not been dealt with yet
41 def __init(self, x=None, y=None):
45 self.x = self.y = None
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)
57 return coord(self.x / abs(x), self.y / abs(y));
61 return "%d - %d" % (self.x, self.y)
64 "A feature in the current quadrant (ship, star, black hole, base, etc)."
66 self.type = None # name of feature type
67 self.sector = None # sector location
69 return self.sector.distance(game.sector)
71 "This will be overridden by subclasses."
73 def sectormove(self, dest):
74 "Move this feature within the current quadrant."
76 game.quad[self.sector] = None
77 game.quad[dest] = self
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"):
90 elif self.type == "Romulan":
93 if self.type in ("Klingon", "Commander", "Super-Commander"):
95 elif self.type == "Romulan":
99 "Empty space. Has no state, just knows how to identify iself."
104 "A star. Has no state, just knows how to identify iself."
108 class planet(feature):
109 "A planet. May be inhabited or not, may hold dilithium crystals or not."
111 feature.__init__(self)
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)
118 game.state.planets.remove(self)
126 "A bit of Tholian web. Has no state, just knows how to identify iself."
130 class blackhole(feature):
131 "A black hole. Has no hair, just knows how to identify iself."
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)
142 game.state.bases.remove(self)
153 self.supernova = None
155 self.status = "secure" # Other valuues: "distressed", "enslaved"
157 "List enemies in this quadrant."
159 for feature in self.quad.values:
160 if not isinstance(feature, ship):
162 if feature.name not in ("Enterprise", "Faerie Queene"):
167 "A chart page. The starchart is a 2D array of these."
169 self.stars = None # Will hold a number
170 self.starbase = None # Will hold a bool
171 self.klingons = None # Will hold a number
174 "State of the universe. The galaxy is a 2D array of these."
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
195 return game.damage[dev] != 0.0
199 self.date = None # The only mandatory attribute
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
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,
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
294 self.probeinx = None # probe x,y increment
295 self.probeiny = None #
296 self.height = None # height of orbit around planet
299 "Are we in communication with Starfleet Command?"
300 return (not damaged("DRADIO")) or game.condition == "docked"
302 # Code corresponding to ai.c begins here
304 def tryexit(look, ship, irun):
305 # a bad guy attempts to bug out of the quadrant
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
316 # avoid intruding on another commander's territory
317 if ship.type == "Commander":
318 if iq in gamestate.kcmdr:
320 # refuse to leave if currently attacking starbase:
321 if game.battle == game.quadrant:
323 # don't leave if over 1000 units of energy
324 if ship.power > 1000.0:
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":
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":
343 schedule("FSCMOVE", 0.2777)
344 unschedule("FSCDBAS")
345 game.state.kscmdr = iq
347 for (n, cmdr) in enumerate(game.state.kcmdr):
348 if cmdr == game.quadrant:
349 game.state.kcmdr[n] = iq
352 return True # successful exit
354 def sgn(n): n / abs(n)
357 Algorithm for moving bad guys:
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.
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.
367 Attacker Initial energy levels (nominal):
368 Klingon Romulan Commander Super-Commander
371 Good 450 800 1300 1750
372 Expert 475 850 1350 1875
373 Emeritus 500 900 1400 2000
374 VARIANCE 75 200 200 200
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.
380 * If Enterprise is not docked, an agressive action is taken if enemy
381 forces are 1000 greater than Enterprise.
383 Agressive action on average cuts the distance between the ship and
384 the enemy to 1/4 the original.
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.
390 If docked, is reduced by roughly 1.75*game.skill, generally forcing a
391 retreat, especially at high skill levels.
393 * Motion is limited to skill level, except for SC hi-tailing it out.
397 # tactical movement for the bad guys
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)
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"))):
412 # decide whether to advance, retreat, or hold position
413 forces = ship.power + 100.0*len(game.quad.enemies()) + 400*(nbaddys-1)
415 forces += 1000.0 # Good for enemy if shield is down!
416 if not damaged("DPHASER") or not damaged("DPHOTON"):
420 forces -= 0.2*(game.energy - 2500.0);
421 if damaged("DPHOTON"):
424 forces -= 50.0*game.torps
426 # phasers and photon tubes both out!
429 if forces <= 1000.0 and game.condition != "docked": # Typical case
430 motion = ((forces+200.0*Rand())/150.0) - 5.0
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)
438 proutn("=== MOTION = %1.2f, FORCES = %1.2f, ", motion, forces)
439 # don't move if no motion
442 # Limit motion according to skill
443 if abs(motion) > game.skill:
448 # calculate preferred number of steps
450 if motion > 0 and nsteps > mdist: # don't overshoot
452 if nsteps > QUADSIZE: # This shouldn't be necessary
454 if nsteps < 1: # This shouldn't be necessary
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):
462 if 2.0 * abs(me.y) < abs(game.sector.x-com.x):
464 if me.x != 0: me.x = sgn(me.x*motion)
465 if me.y != 0: me.y = sgn(me.y*motion)
468 for ll in range(nsteps):
471 # Check if preferred position available
475 attempts = 0 # Settle meysterious hang problem
476 while attempts < 20 and not success:
478 if look.x < 1 or look.x > QUADSIZE:
479 if motion < 0 and tryexit(look, ship, bugout):
481 if krawl.x == me.x or me.y == 0:
483 look.x = next.x + krawl.x
485 elif look.y < 1 or look.y > QUADSIZE:
486 if motion < 0 and tryexit(look, ship, bugout):
488 if krawl.y == me.y or me.x == 0:
490 look.y = next.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):
497 if krawl.x != me.x and me.y != 0:
498 look.x = next.x + krawlx
500 elif krawly != me.y and me.x != 0:
501 look.y = next.y + krawly
504 break # we have failed
515 # Put ship in place within same quadrant
516 if next != ship.location:
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)
523 prout(" retreats to sector %s" % ship.location)
524 ship.sectormove(next)
527 "Allow enemies to move."
528 for enemy in self.quad.enemies():
529 if enemy.type == "Commander":
532 for enemy in self.quad.enemies():
533 if enemy.type == "Super-Commander":
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
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"):
545 def movescom(ship, avoid):
546 # commander movement helper
548 if game.state.kscmdr == game.quadrant or \
549 game.state.galaxy[iq].supernova or \
550 game.state.galaxy[iq].klingons > MAXKLQUAD-1:
553 # Avoid quadrants with bases if we want to avoid Enterprise
554 for base in game.state.starbases:
555 if base.location == ship.location:
557 if game.justin and not game.iscate:
559 # Super-Commander has scooted, Remove him from current quadrant.
560 if game.state.kscmdr == game.quadrant:
564 unschedule("FSCDBAS")
565 if game.condition != "docked":
567 ship.sectormove(None)
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":
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.\"")
587 return False # looks good!
590 # move the Super Commander
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:
602 delta.x = game.state.kscmdr.y-game.quadrant.y
603 delta.y = game.quadrant.x-game.state.kscmdr.x
605 if len(game.state.bases):
606 unschedule("FSCMOVE")
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)
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
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:
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)
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)
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)
656 if len(game.state.bases) == 0:
657 unschedule("FSCMOVE")
659 for ibq in game.bases:
660 if ibq == game.state.kscmdr and game.state.kscmdr == game.battle:
663 return # no, don't attack base!
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():
675 proutn(_("Lt. Uhura- \"Captain, the starbase in "))
676 proutn(cramlc(quadrant, game.state.kscmdr))
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)))
683 prout("Mr. Spock- \"Captain, shall we cancel the rest period?\"")
687 game.optime = 0.0 # actually finished
689 # Check for intelligence report
690 if (Rand() > 0.2 or not communicating() or
691 not game.state.galaxy[game.state.kscmdr].charted):
696 prout(_("Lt. Uhura- \"Captain, Starfleet Intelligence reports"))
697 proutn(_(" the Super-commander is in "))
698 proutn(cramlc(quadrant, game.state.kscmdr))
703 "Move the Tholian (an instance of ship type pointed at by game.tholian)."
704 if not game.tholian or game.justin:
707 if game.tholian.location.x == 1 and game.tholian.location.y == 1:
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:
715 elif game.tholian.location.x == QUADSIZE and game.tholian.location.y == 1:
718 # something is wrong!
721 # Do nothing if we are blocked
722 if not (isinstance(game.quad[next], space) or isinstance(game.quad[next], web)):
725 im = (next - game.tholian.location).sgn()
726 if game.tholian.x != next.x:
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:
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:
744 if (not isinstance(game.quad[(QUADSIZE,i)],web)) and game.quad[(QUADSIZE,i)]!=game.tholian:
746 if (not isinstance(game.quad[(i,1)],web)) and game.quad[(i,1)]!=game.tholian:
748 if (not isinstance(game.quad[(i.QUADSIZE)],web)) and game.quad[(i,QUADSIZE)]!=game.tholian:
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.")