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