#!/usr/local/bin/python2.7
"""\
This is ski %s, designed by Mark Stevans, Python port 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

 ! = interpret line as shell command and execute.
 ? = print this help message.

"""
version = "6.9"
import time, random, curses, copy, sys, os

REP_SNOW	= '.'
REP_TREE	= 'Y'
REP_GROUND	= ':'
REP_ICE 	= '#'
REP_PLAYER	= 'I'
REP_YETI	= 'A'
REP_ICBM	= '*'
REP_DEMON	= 'D'

terrain_key = [
"Terrain types:   ",
REP_SNOW, " = snow      ",
REP_TREE, " = tree  ",
REP_GROUND, " = bare ground     ",
REP_ICE, " = ice\n\
Creatures:       ",
REP_PLAYER, " = player    ",
REP_YETI,   " = yeti  ",
REP_ICBM,   " = ICBM            ",
REP_DEMON,  " = fire demon"]

# Length of line
LINE_LEN = 70

# Minimum distance from the player at which a new Yeti may appear.
MIN_YETI_APPEARANCE_DISTANCE = 3

# Constant multiplied into the first element of the cellular growth
# automaton probability array with each passing level.
LEVEL_MULTIPLIER = 1.01

# Absolute value of maximum horizontal player speed.
MAX_HORIZONTAL_PLAYER_SPEED = 5

ICBM_SPEED	= 3	# Horizontal speed of an ICBM.
ICBM_RANGE	= 2	# Horizontal Yeti lethality range of the ICBM.
DEMON_RANGE	= 1	# Horizontal Yeti lethality range of the demon.
DEMON_SPEED	= 1	# Horizontal maximum speed of the Fire Demon.

# Per-turn probabilities
PROB_YETI_MELT  	= 1.0	# Of Yeti spontaneously melting.
PROB_SKIS_MELT_YETI	= 20.0	# Of melting Yeti by jumping over it.
PROB_BAD_SPELL  	= 10.0 	# Of "Summon Fire Demon" spell going awry.
PROB_BAD_TELEPORT	= 10.0	# Of a teleport going wrong.
PROB_BAD_ICBM   	= 30.0	# Of your ICBM exploding on launch.
PROB_SLIP_ON_ICE	= 2.0	# Of slipping when on ice.
PROB_FALL_ON_GROUND	= 10.0	# Of falling down on bare ground.
PROB_HIT_TREE   	= 25.0	# Of hitting tree, each turn in trees
PROB_BAD_LANDING	= 3.0	# Of land badly from jump or hop.

# 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

# Number of points awarded to the player for each meter of horizontal
# or vertical motion during each turn.
POINTS_PER_METER = 1

# 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

# Number of points 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 percent(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.slope = [REP_SNOW] * LINE_LEN
        self.player_pos = self.teleport()
        self.yeti = None
        self.icbm_pos = None
        self.demon_pos = None

        # 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, minval=1):
        "Is the specified position near enough to the player?"
        return exists(pos) and abs(pos - self.player_pos) <= minval

    def teleport(self):
        "Return a random location"
        return random.choice(range(len(self.slope)))

    def gen_next_slope(self):
        # 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(len(self.slope)):
            # 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 < len(self.slope) - 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 percent(self.prob_tree[num_nearby_trees]) and \
                   ((i != self.player_pos) or (num_nearby_trees > 0)):
                self.slope[i] = REP_TREE
            elif percent(self.prob_ice[num_nearby_ice]) and \
                 ((i != self.player_pos) or (num_nearby_ice > 0)):
                self.slope[i] = REP_ICE
            elif percent(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 + len(self.slope)) % len(self.slope)

        # Generate the updated slope.
        self.gen_next_slope()

        # If there is no Yeti, one might be created.
        if not exists(self.yeti) and percent(self.prob_yeti_appearance):
            # Make sure that the Yeti does not appear too
            # close to the player.
            while True:
                self.yeti = self.teleport()
                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 percent(PROB_SKIS_MELT_YETI)) \
                   or percent(PROB_YETI_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 percent(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, 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

        # Negative scores are just too silly.
        if score < 0:
            score = 0

        # 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 percent(PROB_BAD_LANDING):
            self.__accident("Whoops!  A bad landing!", SLIGHT_INJURY)

        # If there is a tree in our position, we might hit it.
        if (world.terrain() == REP_TREE) and percent(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 percent(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 percent(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, cmdline):
        # Print a prompt, and read a command.  Return True to advance game.
        cmd = cmdline[0].upper()
        if cmd == '?':
            print __doc__ % version, terrain_key
            return False
        elif cmd == '!':
            os.system(cmdline[1:])
            return False
        elif cmd == 'R': 	# Move right
            if ((world.terrain() != REP_ICE) and \
                        (self.player_speed < MAX_HORIZONTAL_PLAYER_SPEED)):
                self.player_speed += 1
            return True
        elif cmd == 'L':	# Move left
            if ((world.terrain() != REP_ICE) and \
                        (self.player_speed > -MAX_HORIZONTAL_PLAYER_SPEED)):
                self.player_speed -= 1
            return True
        elif cmd == 'J':	# Jump
            self.jump_count = random.randint(0, 5) + 4
            self.num_jumps_attempted += 1.0
            return True
        elif cmd == 'H':	# Do a hop
            self.jump_count = random.randint(0, 2) + 2
            self.num_jumps_attempted += 0.5
            return True
        elif cmd == 'T':	# Attempt teleportation
            if percent(PROB_BAD_TELEPORT):
                self.__accident("You materialized 25 feet in the air!",
                              SLIGHT_INJURY)
            world.player_pos = world.teleport()
            return True
        elif cmd == 'I':	# Launch backpack ICBM
            if percent(PROB_BAD_ICBM):
                self.__accident("Nuclear blast in your backpack!", SEVERE_INJURY)
            world.icbm_pos = world.player_pos
            return True
        elif cmd == 'D':	# Incant spell for fire demon
            if percent(PROB_BAD_SPELL):
                self.__accident("A bad spell -- the demon grabs you!", MODERATE_INJURY)
            world.demon_pos = world.teleport()
            return True
        else:
            # Any other command just advances
            return True

def colorize(picture):
    "Colorize special characters in a display list."
    for i in range(len(picture)):
        if (i == 0 or (picture[i] != picture[i-1][-1])):
            if picture[i] in colordict:
                picture[i] = colordict[picture[i]] + picture[i]
            else:
                picture[i] = reset + picture[i]
    picture += reset
    return "".join(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 ""
    terrain_key = colorize(terrain_key)

    print "SKI!  Version %s.  Type ? for help." % version
    world = SkiWorld()
    player = SkiPlayer()

    # Perform the game loop until the game is over.
    try:
        repeat = 1
        while True:
            sys.stdout.write(repr(world))
            # If we are jumping, just finish the line.  Otherwise, check for
            # obstacles, and do a command.
            if player.jump_count >= 0:
                sys.stdout.write('\n')
            else:
                player.check_obstacles(world)
                if repeat:
                    repeat -= 1
                if repeat:
                    sys.stdout.write('\n')
                else:
                    cmd = raw_input("? ")
                    if cmd == '':
                        cmd = '\n'
                    elif cmd[0] in "0123456789":
                        if cmd[-1] in "0123456789":
                            repeat = int(cmd)
                            cmd = '\n'
                        else:
                            repeat = int(cmd[:-1])
                            cmd = cmd[-1]
                if not player.do_command(world, cmd):
                    continue
            world.manipulate_objects(player)
            world.update_level(player)
            player.update_player()
    except (KeyboardInterrupt, EOFError):
        sys.stdout.write(reset)
        print "\nBye!"

# end
