]> thyrsus.com Git - ski.git/commitdiff
Initial revision
authorEric S. Raymond <esr@thyrsus.com>
Wed, 31 Dec 2003 01:12:45 +0000 (01:12 +0000)
committerEric S. Raymond <esr@thyrsus.com>
Wed, 31 Dec 2003 01:12:45 +0000 (01:12 +0000)
ski [new file with mode: 0755]

diff --git a/ski b/ski
new file mode 100755 (executable)
index 0000000..428d384
--- /dev/null
+++ b/ski
@@ -0,0 +1,541 @@
+#!/usr/bin/env python
+#                                  S K I !
+#
+# Version 6.0 of October 14, 1990
+# Copyright 1983-1990 by Mark Stevans for Roy's Games, Inc.
+#
+# Version 6.1 of December 30 2003
+# Rewritten in Python
+# Copright 2003 by Eric S. Raymond
+#
+"""
+This is ski %s, designed by Mark Stevans, ported by Eric S. Raymond.
+You are hurtling down a ski slope in reverse, trying to evade the Yeti.
+Available commands are:
+
+ l = turn left            r = turn right
+ j = jump                 h = hop
+ t = teleport             Enter = keep skiing
+ i = launch ICBM          d = summon Fire Demon
+"""
+version='6.1'
+import time, random, curses, copy, sys
+
+REP_SNOW       = '.'
+REP_TREE       = 'Y'
+REP_GROUND     = ':'
+REP_ICE        = '#'
+REP_PLAYER     = 'I'
+REP_YETI       = 'A'
+REP_ICBM       = '*'
+REP_DEMON      = 'D'
+
+terrain_key = """
+Terrain types:   %s = snow      %s = tree  %s = bare ground     %s = ice
+Creatures:       %s = player    %s = yeti  %s = ICBM            %s = fire demon
+""" \
+% (REP_SNOW, REP_TREE, REP_GROUND, \
+   REP_ICE, REP_PLAYER, REP_YETI, \
+   REP_ICBM, REP_DEMON) 
+
+# Length of line
+LINE_LEN = 70
+
+# "MIN_YETI_APPEARANCE_DISTANCE" is the minimum distance from the player
+# at which a new Yeti may appear.
+MIN_YETI_APPEARANCE_DISTANCE = 3
+
+# "LEVEL_MULTIPLIER" is a constant multiplied into the first element of the
+# cellular growth automaton probability array with each passing level.
+LEVEL_MULTIPLIER = 1.01
+
+# "MAX_HORIZONTAL_PLAYER_SPEED" defines the absolute value of the maximum
+#horizontal player speed.
+MAX_HORIZONTAL_PLAYER_SPEED = 5
+
+# "PROB_SKIS_MELT_YETI" is the probability that the player's jet-powered
+# skis will melt the Yeti during each turn in which the player is jumping
+# over the Yeti.
+PROB_SKIS_MELT_YETI = 20.0
+
+# "PROB_SPONTANEOUS_MELT" is the probability that the Yeti melts
+# spontaneously during any given turn.
+PROB_SPONTANEOUS_MELT = 1.0
+
+# "ICBM_SPEED" is the horizontal speed of an ICBM.
+ICBM_SPEED = 3
+
+# "ICBM_RANGE" is the horizontal Yeti lethality range of the ICBM.
+ICBM_RANGE = 2
+
+# "DEMON_RANGE" is the horizontal Yeti lethality range of the demon.
+DEMON_RANGE = 1
+
+# "ICBM_SPEED" is the maximum horizontal speed of an ICBM.
+DEMON_SPEED = 1
+
+# "PROB_BAD_SPELL" is the probability that the "Incant Fire Demon" spell will
+# be bad during each incantation.
+PROB_BAD_SPELL = 10.0
+
+# "PROB_BAD_TELEPORT" is the probability that the teleportation device will
+# fail to safely teleport the player during each application.
+PROB_BAD_TELEPORT = 10.0
+
+# "PROB_BAD_ICBM" is the probability that a nuclear ICBM will detonate in
+# the player's backpack during each launch.
+PROB_BAD_ICBM = 30.0
+
+# "PROB_SLIP_ON_ICE" is the probability that the player will slip on the ice
+# and fall down for each turn during which the player is skiiing on ice.
+PROB_SLIP_ON_ICE = 2.0
+
+# "PROB_FALL_ON_GROUND" is the probability that the player will fall down
+# for each turn during which the player is skiiing on bare ground.
+PROB_FALL_ON_GROUND = 10.0
+
+# "PROB_HIT_TREE" is the probability that the player will hit a tree in
+# each turn during which the player is skiiing past a tree.
+PROB_HIT_TREE = 25.0
+
+# "PROB_BAD_LANDING" is the probability that the player will land badly and
+# fall down during a landing from a jump or hop.
+PROB_BAD_LANDING = 3.0
+
+# "POINTS_PER_JUMP" is the number of points awarded to the player for the
+# successful completion of one jump.  For scoring purposes, a hop is
+# considered to consist of exactly one half-jump.
+POINTS_PER_JUMP        = 20
+
+# "POINTS_PER_METER" is the number of points awarded to the player for each
+# meter of horizontal or vertical motion during each turn.
+POINTS_PER_METER = 1
+
+# "POINTS_PER_MELTED_YETI" is the number of points awarded to the player
+# for each Yeti that melts during the course of the game, regardless of
+# whether the player passively caused the Yeti to melt by luring him from a
+# snowbank, or actively melted the Yeti using his skis, an ICBM, or with
+# the assistance of the Fire Demon.
+POINTS_PER_MELTED_YETI = 100
+
+# "POINTS_PER_INJURY_DEGREE" is number of pints docked from your score
+# for each degree of injury.
+POINTS_PER_INJURY_DEGREE = -40
+
+# The injury categories.
+SLIGHT_INJURY          = 0
+MODERATE_INJURY                = 3
+SEVERE_INJURY          = 6
+
+# The randomness of injury degrees.
+INJURY_RANDOMNESS      = 6
+
+colordict = {
+    REP_SNOW   : curses.COLOR_WHITE,
+    REP_TREE   : curses.COLOR_GREEN,
+    REP_PLAYER : curses.COLOR_MAGENTA,
+    REP_GROUND : curses.COLOR_YELLOW,  # Brown on PC-compatibles
+    REP_ICE    : curses.COLOR_CYAN,
+    REP_YETI   : curses.COLOR_BLUE,
+    REP_ICBM   : curses.COLOR_MAGENTA,
+    REP_DEMON  : curses.COLOR_RED
+    }
+
+def prob(X):
+    return (random.randint(0, 9999) / 100.0) <= (X)
+def exists(X):
+    return X != None
+
+class SkiWorld:
+    def __init__(self):
+        # Constants controlling the multiple cellular growth
+        # automatons that are executed in parallel to generate
+        # hazards.
+        #
+        # Each cellular growth automaton probability element tends to control a
+        # different facet of hazard generation:
+        #
+        #    [0] appearance of new hazards in clear snow
+        #    [1] hazards edge growth
+        #    [2] hazards edge stability
+        #    [3] appearance of holes in solid hazards (this allows the Yeti
+        #        to work his way through forests)
+        self.prob_tree = [0.0, 30.0, 70.0, 90.0]
+        self.prob_ice = [0.0, 30.0, 70.0, 90.0]
+        self.prob_ground = [0.0, 30.0, 70.0, 90.0]
+
+       self.level_num = 0
+       self.player_pos = LINE_LEN / 2
+       self.yeti = None
+       self.icbm_pos = None
+       self.demon_pos = None
+       self.slope = [REP_SNOW] * LINE_LEN
+
+        # Initialize the player's position.
+        self.player_pos = random.choice(range(LINE_LEN))
+
+        # Randomize the appearance probabilities.
+        self.prob_tree[0] = (random.randint(0, 99)) / 500.0 + 0.05
+        self.prob_ice[0] = (random.randint(0, 99)) / 500.0 + 0.05
+        self.prob_ground[0] = (random.randint(0, 99)) / 500.0 + 0.05
+        self.prob_yeti_appearance = (random.randint(0, 99)) / 25.0 + 1.0
+
+    def terrain(self):
+        "What kind of terrain are we on?"
+        return self.slope[self.player_pos]
+
+    def nearby(self, pos, min=1):
+        "Is the specified position near enough to the player?"
+        return exists(pos) and abs(pos - self.player_pos) <= min        
+
+    def gen_next_slope(self, next_level):
+        # Generates the slope of the next level, dependent upon
+        # the characteristics of the current level, the probabilities, and the
+        # position of the player.
+
+        # Stash away old state so we don't step on it while generating new
+        current_slope = copy.copy(self.slope)
+
+        # Generate each character of the next level.
+        for i in range(LINE_LEN):
+            # Count the number of nearby trees, ice patches, and
+            # ground patches on the current level.
+            num_nearby_trees = 0
+            num_nearby_ice = 0
+            num_nearby_ground = 0
+
+            if current_slope[i] == REP_TREE:
+                num_nearby_trees += 1
+            if current_slope[i] == REP_ICE:
+                num_nearby_ice += 1
+            if current_slope[i] == REP_GROUND:
+                num_nearby_ground += 1
+
+            if i > 0:
+                if current_slope[i - 1] == REP_TREE:
+                    num_nearby_trees += 1
+                elif current_slope[i - 1] == REP_ICE:
+                    num_nearby_ice += 1
+                elif current_slope[i - 1] == REP_GROUND:
+                    num_nearby_ground += 1
+
+            if i < LINE_LEN - 1:
+                if current_slope[i + 1] == REP_TREE:
+                    num_nearby_trees += 1
+                elif current_slope[i + 1] == REP_ICE:
+                    num_nearby_ice += 1
+                elif current_slope[i + 1] == REP_GROUND:
+                    num_nearby_ground += 1
+
+            # Generate this character of the next level based upon
+            # the characteristics of the nearby characters on the
+            # current level.
+            if prob(self.prob_tree[num_nearby_trees]) and \
+                   ((i != self.player_pos) or (num_nearby_trees > 0)):
+                self.slope[i] = REP_TREE
+            elif prob(self.prob_ice[num_nearby_ice]) and \
+                 ((i != self.player_pos) or (num_nearby_ice > 0)):
+                self.slope[i] = REP_ICE
+            elif prob(self.prob_ground[num_nearby_ground]) and \
+                 ((i != self.player_pos) or (num_nearby_ground > 0)):
+                self.slope[i] = REP_GROUND
+            else:
+                self.slope[i] = REP_SNOW
+
+    def update_level(self, player):
+        # Go to the next level, and move the player.
+        self.level_num += 1
+
+        # Figure out the new player position based on a modulo
+        # addition.  Note that we must add the line length into the
+        # expression before taking the modulus to make sure that we
+        # are not taking the modulus of a negative integer.
+        self.player_pos = (self.player_pos +
+                        player.player_speed + LINE_LEN) % LINE_LEN
+
+        # Generate the updated slope.
+        self.gen_next_slope(self)
+
+        # If there is no Yeti, one might be created.
+        if not exists(self.yeti) and prob(self.prob_yeti_appearance):
+            # Make sure that the Yeti does not appear too
+            # close to the player.
+            while True:
+                self.yeti = random.randint(0, LINE_LEN-1)
+                if not self.nearby(self.yeti,MIN_YETI_APPEARANCE_DISTANCE):
+                    break
+
+        # Increase the initial appearance probabilities of all obstacles and
+        # the Yeti.
+        self.prob_tree[0] *= LEVEL_MULTIPLIER
+        self.prob_ice[0] *= LEVEL_MULTIPLIER
+        self.prob_ground[0] *= LEVEL_MULTIPLIER
+        self.prob_yeti_appearance *= LEVEL_MULTIPLIER
+
+    def manipulate_objects(self, player):
+       # If there is a Yeti, the player's jet-powered skis may melt him,
+       # or he may spontaneously melt. Otherwise move him towards the player.
+        # If there is a        tree in the way, the Yeti is blocked.
+        if exists(self.yeti):
+            if (self.nearby(self.yeti) and prob(PROB_SKIS_MELT_YETI)) \
+                       or prob(PROB_SPONTANEOUS_MELT):
+                self.yeti = None
+                player.num_snomen_melted += 1
+
+        if exists(self.yeti):
+            if self.yeti < self.player_pos:
+               if self.slope[self.yeti + 1] != REP_TREE:
+                    self.yeti += 1
+            else:
+                if self.slope[self.yeti - 1] != REP_TREE:
+                    self.yeti -= 1
+
+       # If there is an ICBM, handle it.
+        if exists(self.icbm_pos):
+            # If there is a Yeti, move the ICBM towards him.  Else,
+            # self-destruct the ICBM.
+            if exists(self.yeti):
+                if self.icbm_pos < self.yeti:
+                    self.icbm_pos += ICBM_SPEED
+                else:
+                    self.icbm_pos -= ICBM_SPEED
+            else:
+                self.icbm_pos = None
+
+       # If there is a fire demon on the level, handle it.
+        if exists(self.demon_pos):
+            # If there is a Yeti on the current level, move the demon
+            # towards him.  Else, the demon might decide to leave.
+            if exists(self.yeti):
+                if self.demon_pos < self.yeti:
+                    self.demon_pos += DEMON_SPEED
+                else:
+                    self.demon_pos -= DEMON_SPEED
+            else:
+                if prob(25.0):
+                    self.demon_pos = None
+
+       # If there is a Yeti and an ICBM on the slope, the Yeti
+       # might get melted.
+        if exists(self.yeti) and exists(self.icbm_pos):
+            if abs(self.yeti - self.icbm_pos) <= ICBM_RANGE:
+                self.icbm_pos = None
+                self.yeti = None
+                player.num_snomen_melted += 1
+
+       # If there is a Yeti and a fire demon, he might get melted.
+       if exists(self.yeti) and exists(self.demon_pos):
+            if abs(self.yeti - self.demon_pos) <= 1:
+                self.yeti = None
+                player.num_snomen_melted += 1
+
+    def __repr__(self):
+       "Create a picture of the current level."
+        picture = copy.copy(self.slope)
+        picture[self.player_pos] = REP_PLAYER
+        if exists(self.yeti):
+            picture[self.yeti] = REP_YETI
+        if exists(self.demon_pos):
+            picture[self.demon_pos] = REP_DEMON
+        if exists(self.icbm_pos):
+            picture[self.icbm_pos] = REP_ICBM
+        picture = colorize(picture)
+       return "%4d %s" % (self.level_num, "".join(picture))
+
+class SkiPlayer:
+    injuries = (
+       "However, you escaped injury!", 
+       "But you weren't hurt at all!", 
+       "But you only got a few scratches.", 
+       "You received some cuts and bruises.", 
+       "You wind up with a concussion and some contusions.", 
+       "You now have a broken rib.", 
+       "Your left arm has been fractured.",
+       "You suffered a broken ankle.",
+       "You have a broken arm and a broken leg.", 
+       "You have four broken limbs and a cut!", 
+       "You broke every bone in your body!", 
+       "I'm sorry to tell you that you have been killed....")
+
+    def __init__(self):
+       self.jump_count = -1
+       self.num_snomen_melted = 0
+       self.num_jumps_attempted = 0.0
+       self.player_speed = 0
+       self.meters_travelled = 0
+
+    def __accident(self, msg, severity):
+        # "__accident" is called when the player gets into an __accident,
+        # which ends the game.  "msg" is the description of the
+        # __accident type, and "severity" is the severity.  This
+        # function should never return.
+        # Compute the degree of the player's injuries.
+        degree = severity + random.randint(0, INJURY_RANDOMNESS - 1)
+
+        # Print a message indicating the termination of the game.
+        print "!\n\n%s  %s\n" % (msg, SkiPlayer.injuries[degree])
+
+        # Print the statistics of the game.
+        print "You skiied %d meters with %d jumps and melted %d %s."%(\
+                        self.meters_travelled,
+                        self.num_jumps_attempted,
+                        self.num_snomen_melted,
+                        ("Yeti", "Yetis")[self.num_snomen_melted != 1])
+
+        # Initially calculate the player's score based upon the number of
+        # meters travelled.
+        score = self.meters_travelled * POINTS_PER_METER
+
+        # Add bonus points for the number of jumps completed.
+        score += self.num_jumps_attempted * POINTS_PER_JUMP
+
+        # Add bonus points for each Yeti that melted during the course of
+        # the game.
+        score += self.num_snomen_melted * POINTS_PER_MELTED_YETI
+
+        # Subtract a penalty for the degree of injury experienced by the
+        # player.
+        score += degree * POINTS_PER_INJURY_DEGREE
+
+        # Print the player's score.
+        print "Your score for this run is %ld." % score
+
+        # Exit the game with a code indicating successful completion.
+        sys.exit(0)
+
+    def check_obstacles(self, world):
+       # If we are just landing after a jump, we might fall down.
+        if (self.jump_count == 0) and prob(PROB_BAD_LANDING):
+            self.__accident("Whoops!  A bad landing!", LIGHT_INJURY)
+
+       # If there is a tree in our position, we might hit it.
+        if (world.terrain() == REP_TREE) and prob(PROB_HIT_TREE):
+            self.__accident("Oh no!  You hit a tree!", SEVERE_INJURY)
+
+       # If there is bare ground under us, we might fall down.
+        if (world.terrain() == REP_GROUND) and prob(PROB_FALL_ON_GROUND):
+            self.__accident("You fell on the ground!", MODERATE_INJURY)
+
+       # If we are on ice, we might slip.
+        if (world.terrain() == REP_ICE) and prob(PROB_SLIP_ON_ICE):
+            self.__accident("Oops!  You slipped on the ice!",SLIGHT_INJURY)
+
+       # If there is a Yeti next to us, he may grab us.
+        if world.nearby(world.yeti):
+            self.__accident("Yikes!  The Yeti's got you!",MODERATE_INJURY)
+
+
+    def update_player(self):
+        "Update state of player for current move."
+        self.meters_travelled += abs(self.player_speed) + 1
+        # If the player was jumping, decrement the jump count.
+        if player.jump_count >= 0:
+            player.jump_count -= 1
+
+
+    def do_command(self, world):
+       # Print a prompt, and read a command.
+        while True:
+            cmdline = raw_input("? ")
+            if cmdline == "":
+                cmdline = "\n"
+            cmd = cmdline[0].upper()
+
+            if cmd == '?':
+                print __doc__ % version, terrain_key
+                sys.stdout.write(`world`)
+                continue
+            elif cmd == '!':
+                os.system(cmdline + 1)
+                sys.stdout.write(`world`)
+                continue
+            elif cmd == 'R':   # Move right
+               if ((world.terrain() != REP_ICE) and \
+                    (self.player_speed < MAX_HORIZONTAL_PLAYER_SPEED)):
+                       self.player_speed += 1
+               break
+            elif cmd == 'L':   # Move left
+               if ((world.terrain() != REP_ICE) and \
+                    (self.player_speed > -MAX_HORIZONTAL_PLAYER_SPEED)):
+                       self.player_speed -= 1
+               break
+            elif cmd == 'J':   # Jump
+               self.jump_count = random.randint(0, 5) + 4
+               self.num_jumps_attempted += 1.0
+               break
+            elif cmd == 'H':   # Do a hop
+               self.jump_count = random.randint(0, 2) + 2
+               self.num_jumps_attempted += 0.5
+               break
+            elif cmd == 'T':   # Attempt teleportation
+                if prob(PROB_BAD_TELEPORT):
+                    self.__accident("You materialized 25 feet in the air!",
+                                  SLIGHT_INJURY)
+               world.player_pos = random.randint(0, LINE_LEN-1)
+               break
+            elif cmd == 'I':   # Launch backpack ICBM
+                if prob(PROB_BAD_ICBM):
+                    self.__accident("Nuclear blast in your backpack!", SEVERE_INJURY)
+                world.icbm_pos = world.player_pos
+               break
+
+            elif cmd == 'D':   # Incant spell for fire demon
+                if prob(PROB_BAD_SPELL):
+                    self.__accident("A bad spell -- the demon grabs you!", MODERATE_INJURY)
+
+               world.demon_pos = random.randint(0, LINE_LEN-1)
+               break
+            else:
+               break
+
+    def do_user(self, world):
+       # If we are jumping, just finish the line.  Otherwise, check for
+       # obstacles, and do a command.
+        if self.jump_count >= 0:
+            sys.stdout.write('\n')
+       else:
+            self.check_obstacles(world)
+            self.do_command(world)
+
+def colorize(picture):
+    "Colorize specual characters in a string"
+    for i in range(len(picture)):
+        if (i == 0 or (picture[i] != picture[i-1][-1])) and picture[i] in colordict:
+            picture[i] = colordict[picture[i]] + picture[i]
+    picture += reset
+    return picture
+
+# Main sequence 
+if __name__ == "__main__":
+    # Initialize the random number generator.
+    random.seed(time.time())
+
+    # Arrange for color to be available
+    curses.setupterm()
+    color = curses.tigetstr("setaf")
+    for (ch, idx) in colordict.items():
+        if color:
+            colordict[ch] = curses.tparm(color, idx)
+        else:
+            colordict[ch] = ""
+    reset = curses.tigetstr("sgr0") or ""
+
+    print "SKI!  Version 6.1.  Type ? for help.\n"
+    world = SkiWorld()
+    player = SkiPlayer()
+
+    # Perform the game loop until the game is over.
+    try:
+        while True:
+            sys.stdout.write(`world`)
+            player.do_user(world)
+            world.manipulate_objects(player)
+            world.update_level(player)
+            player.update_player()
+    except (KeyboardInterrupt, EOFError):
+        print "\nBye!"
+
+# end
+
+
+