#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
#
# Mines of Elderlore
# An Ascii roguelike with :
# * Permanent levels
# * Simple and easy gameplay
# * High scores that you can compare with others
# http://landsof.elderlore.com
#
# Released under the GNU General Public License
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""
Mines of Elderlore
"""

import random
import time
import Numeric as N
import curses
import ConfigParser
import pickle
import os

###############################################################################
# Functions
###############################################################################

def wrap(text, width):
    """
    Splits a string in strings with length <= width
    """
    list = []
    elt = ""
    words = text.split(' ')
    for word in words:
        if len(elt) + len(word) < width:
            elt += " " + word
        else:
            list.append(elt[1:])
            elt = " " + word
    list.append(elt[1:])
    return list

###############################################################################
# Constants
###############################################################################

CURSES_DIRS = {curses.KEY_UP : (0, -1),     # North
               curses.KEY_DOWN : (0, 1),    # South
               curses.KEY_LEFT : (-1, 0),   # West
               curses.KEY_RIGHT : (1, 0),   # East
               ord(' ') : (0, 0),           # to wait
               curses.KEY_B2 : (0, 0)}
               
"""
               curses.KEY_A1 : (-1, -1),    # North West
               curses.KEY_A3 : (1, -1),     # North East
               curses.KEY_C1 : (-1, 1),     # South West
               curses.KEY_C3 : (1, 1),      # South East
               
"""
# Multipliers for transforming coordinates to other octants:
MULT = [[1,  0,  0, -1, -1,  0,  0,  1],
        [0,  1, -1,  0,  0, -1,  1,  0],
        [0,  1,  1,  0,  0, -1, -1,  0],
        [1,  0,  0,  1, -1,  0,  0, -1]]

DIRS8 = ((-1,0), (-1,-1), (0,-1), (1,-1), (1,0), (1,1), (0,1), (-1,1))
DIRS = [(-1,0), (0,-1), (1,0), (0,1)]

SPECIAL_NONE        = 0
SPECIAL_DOOR        = 1
SPECIAL_UPSTAIRS    = 2
SPECIAL_DOWNSTAIRS1 = 3
SPECIAL_DOWNSTAIRS2 = 4
SPECIAL_MUSHROOM    = 5
SPECIAL_POTION      = 6

DUNGEON_FLOOR       = 0
DUNGEON_WALL        = 1

WEAPON_SWORD        = 10
WEAPON_AXE          = 20
WEAPON_SPEAR        = 30
WEAPON_WARHAMMER    = 40

WEAPON_NONE         = -1
WEAPON_FLAVOR0      = 0
WEAPON_FLAVOR1      = 1
WEAPON_FLAVOR2      = 2
WEAPON_FLAVOR3      = 3
WEAPON_FLAVOR4      = 4
WEAPON_FLAVOR5      = 5

WEAPON_NAME = ("sword", "axe", "spear", "warhammer")
WEAPON_TILE = ("/", "P", "|", "T")
WEAPON_FLAVOR = ("training", "iron", "steel", "hardenned steel", "crystal", "vorpal")

    
###############################################################################
# Monster class
###############################################################################

class Monster:
    ASLEEP = 0
    AWAKEN = 1
    def __init__(self, pos, level):
        self.pos = pos
        self.level = level
        if 1 < self.level < 10:
            while random.randint(1, 5) == 1:
                self.level +=1
            self.level = min(self.level, 9)
        if self.level < 10:
            self.hp = 1 + self.level * (self.level + 1)
            self.state = Monster.ASLEEP
        else:
            self.hp = 150
            self.state = Monster.AWAKEN
        self.hpmax = self.hp
            
        
    def is_awake(self):
        return self.state == Monster.AWAKEN
    
    def awake(self):
        """
        Change state to aggressive
        """
        self.state = min(self.state + 1, Monster.AWAKEN)
        
    def tile(self):
        """
        Monster tile on two characters
        """
        if self.is_awake():
            if self.level < 10:
                return "M%s" % self.level
            else:
                return "ME"
        else:
            return "M~"
    
    def move(self, pos):
        """
        Moves to a new pos
        """
        self.pos = pos
        
    def upgrade(self):
        """
        The monster gains a level
        """
        self.level = min(self.level+1, 9)
        self.hpmax = 4 + self.level * (self.level - 1)
        self.hp = self.hpmax
        
    def damage(self):
        """
        How much damage the monster inflicts
        """
        l2 = self.level
        l1 = max(0, l2 - 3)
        return 1 + random.randint(l1, l2) + random.randint(l1, l2) + random.randint(l1, l2)
    
    def stun(self, n):
        """
        Stun the monster
        """
        self.state = -n
        

###############################################################################
# Dungeon class
###############################################################################

class Dungeon:
    def __init__(self, size = (32, 32), ratio = 60, name = None, level = 1):
        """
        Initialization of a dungeon floor
        size   : (width, height)
        ratio  : ratio of rooms compared to the full surface (in %)
                 from 10 to 60
        name   : random seed in a string
        """
        self.size = size
        self.name = name
        self.level = level
        # Set the seed
        if name == "random" or name is None:
            random.seed(time.time())
        else:
            random.seed(name)
            
        # Creating the floor array
        # 0: the floor
        # 1: the walls
        self.floor = N.ones(size) * DUNGEON_WALL
        # rooms array
        self.rooms = N.ones(size)
        # number of cells rooms occupy
        self.surf_rooms = 0
        # doors, stairs, ... array
        self.special = N.ones(size) * SPECIAL_NONE
        # Dead end list
        self.list_de = []
        # Monsters dictionary
        self.monster = {}
        
        self.corridor_v(False)
        
        r = self.size[0] * self.size[1] * ratio / 100
        n = 0
        nmax = r * 10
        
        while n < r:
            n += 1
            self.corridor_h()
            self.corridor_v()
            if self.surf_rooms < r:
                self.room()
        # Addition of doors to rooms
        self.special *= (1 - self.floor) * SPECIAL_DOOR
        # Addition of stairs and treasures to dead ends
        self.dead_end()
        # Back to a true random seed
        random.seed(time.time())
    
    def corridor_h(self, test = True):
        """
        A horizontal corridor
        """
        l = random.randint(2, 6) * 2 + 1
        x = (random.randint(2, self.size[0] - l - 2) / 2) * 2 + 1
        y = (random.randint(6, self.size[1] - 6) / 2) * 2 + 1
        if l / 5 > N.sum(N.sum(1 - self.floor[x:x+l, y])) > 0 or not test:
            self.floor[x:x+l, y] = DUNGEON_FLOOR
            self.list_de.append((x, y))
            self.list_de.append((x+l-1, y))
            return True
        return False
        
        
    def corridor_v(self, test = True):
        """
        A vertical corridor
        """
        l = random.randint(2, 6) * 2 + 1
        x = (random.randint(6, self.size[0] - 6) / 2) * 2 + 1
        y = (random.randint(2, self.size[1] - l - 2) / 2) * 2 + 1
        if l / 5 > N.sum(N.sum(1 - self.floor[x, y:y+l])) > 0 or not test:
            self.floor[x, y:y+l] = DUNGEON_FLOOR
            self.list_de.append((x, y))
            self.list_de.append((x, y+l-1))
            return True
        return False
        
    def room(self, pos = None, test = True):
        """
        A room
        """
        lx = random.randint(2, 5) * 2 + 1
        ly = random.randint(2, 5) * 2 + 1
        if pos is None:
            x = (random.randint(0, self.size[0] - lx - 2) / 2) * 2 + 1
            y = (random.randint(0, self.size[1] - ly - 2) / 2) * 2 + 1
        else:
            x, y = pos
        if (N.sum(N.sum(1 - self.floor[x:x+lx, y:y+ly])) > 0 \
            and N.sum(N.sum(1 - self.rooms[x:x+lx, y:y+ly])) == 0) \
            or not test:  
            # The room is above one corridor or more
            # but not above another room
            self.surf_rooms += lx * ly
            self.floor[x:x+lx, y:y+ly] = DUNGEON_FLOOR
            self.rooms[x:x+lx, y:y+ly] = 0
            self.special[x-1:x+lx+1, y-1:y+ly+1] = SPECIAL_DOOR
            self.special[x:x+lx, y:y+ly] = SPECIAL_NONE
            n = random.randint(1, 5 + self.level)
            posn = (x+random.randint(1, lx-1), y+random.randint(1, ly-1))
            if n == 1:
                self.add_special(posn, SPECIAL_POTION)
            elif n < 4:
                self.add_special(posn, SPECIAL_MUSHROOM)
            else:
                self.add_monster(posn, self.level)
            return True
        return False
    
    
    def weapon_params(self, pos):
        """
        Returns flavor and num of a weapon at pos in the dungeon
        """
        weap_flav = self.special[pos] % 10
        weap_type = (self.special[pos] - weap_flav) / 10 - 1
        return weap_flav, weap_type
    
    
    def add_mushroom(self, pos, nb):
        """
        add mushrooms around pos after a monster is killed
        """
        l = list(DIRS8)
        random.shuffle(l)
        n = 0
        for i in range(len(l)):
            p = (pos[0] + l[i][0], pos[1] + l[i][1])
            if self.is_reachable(p) and not self.has_special(p, SPECIAL_MUSHROOM):
                n += 1
                self.add_special(p, SPECIAL_MUSHROOM)
                if n == nb:
                    break
        return n
                
    
    def add_special(self, pos, type):
        """
        Add something special to pos
        """
        if self.special[pos] == SPECIAL_NONE:
            self.special[pos] = type
            
    def has_special(self, pos, type):
        """
        True is there is a special type at pos, False either
        """
        return self.special[pos] == type
    
    def grab_special(self, pos, type):
        """
        Gets the special type at pos
        """
        if self.has_special(pos, type):
            self.special[pos] = SPECIAL_NONE
            return 1
        return 0

    def add_monster(self, pos, level):
        """
        Add a monster at pos
        """
        if self.is_reachable(pos):
            self.monster[pos] = Monster(pos, level)
    
    def has_monster(self, pos):
        """
        True is there is a monster at pos, False either
        """
        return self.monster.has_key(pos)
    
    def bash_monster(self, pos, n):
        """
        Remove n points to monster health points
        """
        if self.has_monster(pos):
            if self.monster[pos].hp <= n:
                xp = self.monster[pos].hpmax
                self.monster.pop(pos)
                return xp
            else:
                self.monster[pos].hp -= n
                return 0
        
    def dead_end(self):
        """
        Dealing dead ends
        """
        nb_sp = 1
        for i in range(len(self.list_de)):
            x, y = self.list_de[i]
            if N.sum(N.sum(self.floor[x-1:x+2, y-1:y+2])) <> 7:
                # Add one potion every 3 monster
                if i % 3 == 0:
                    self.add_special((x, y), SPECIAL_POTION)
                else:
                    self.add_monster((x, y), self.level)
            else:
                # It is a real dead end
                nb_sp += 1
                if nb_sp < 5:
                    if nb_sp not in (SPECIAL_DOWNSTAIRS1, SPECIAL_DOWNSTAIRS2) or self.level < 9:
                        # Down stairs only before level 9
                        self.special[x, y] = nb_sp
                    # Record starting position
                    if nb_sp == 2:
                        self.pos_start = self.list_de[i]
                else:
                    # Add a weapon
                    if self.level == 9:
                        weap_type = WEAPON_SWORD
                        weap_flav = WEAPON_FLAVOR5
                    else:
                        weap_type = random.choice((WEAPON_SWORD, WEAPON_AXE, WEAPON_SPEAR, WEAPON_WARHAMMER))
                        weap_flav = 1 + random.randint(max(0, self.level / 2 - 2), self.level / 2)
                    self.special[x, y] = weap_type + weap_flav
                    self.add_monster((x-1, y), self.level + 1)
                    self.add_monster((x+1, y), self.level + 1)
                    self.add_monster((x, y-1), self.level + 1)
                    self.add_monster((x, y+1), self.level + 1)
                        
        if nb_sp == 1:
            # No dead ends found
            # We add stairs manually
            self.special[self.list_de[0]] = SPECIAL_UPSTAIRS
            # Record starting position
            self.pos_start = self.list_de[0]
            if self.level < 9:
                self.special[self.list_de[1]] = SPECIAL_DOWNSTAIRS1
        elif nb_sp == 2:
            #print "Bottom reached !"
            pass

        
    def is_reachable(self, pos):
        """
        Return True is pos can be accessed
        False either
        """
        if self.has_special(pos, SPECIAL_DOOR):
            return False
        else:
            return self.floor[pos[0], pos[1]] == DUNGEON_FLOOR and \
                   not self.monster.has_key(pos)
 
###############################################################################
# Player class
###############################################################################

class Player:
    def __init__(self, s, size = (32, 32), dungeon_name = "Moria", cols = 30, scores = {}):
        
        self.fov = max(size[0], size[1])
        
        self.dname = dungeon_name
        self.dsize = size
        self.dname_full = "%s (%s, %s)" % (dungeon_name, size[0], size[1])
        self.cols = cols
        
        # Dictionary of visited floors
        self.dict_dungeon = {}
        
        self.xp = 0
        self.level = 1
        self.levels = self.levels()
        self.hp, self.hpmax = 10, 10
        self.deepness = 1
        
        self.mushroom = 0
        # List of mushroom pos seen by the player
        self.mush_seen = []
        
        self.potion = 1
        # List of owned weapons
        self.weapon_flavor = [WEAPON_FLAVOR0, WEAPON_NONE, WEAPON_NONE, WEAPON_NONE]
        self.active_weapon = 0
        # Flag for axe
        self.hit_by_monster = False
        
        # List of starting pos in levels
        self.start_pos = [(0, 0)]
        # Loading the first floor
        self.load_dungeon_floor(self.start_pos[-1], False)
        self.pos = self.dungeon.pos_start
        self.known[self.pos] = 1
        # Messages of (text, color)
        self.message = []
        # Flag to test the end of the program
        # 0 : in game
        # 1 : killed
        # 2 : exited the mines
        self.exit = 0
        
        self.line = " " * s.getmaxyx()[1] #curses.tigetnum('cols')
        
        # A dictionary where key is dungeon name, and value is a tuple
        # each tuple contains all scores for one dungeon
        self.scores = scores
        
        # Number of rounds (for the WINNER case)
        self.round = 0
        self.winner = False
        
    
    def levels(self):
        """
        Compute xp amounts to increase player level
        for level 2 : player must kill 10 monster level 1
        for level 3 : player must kill 12 monster level 2
        for level 4 : player must kill 14 monster level 3
        ...
        """
        l = [0,]
        for level in range(1, 11):
            l.append(l[-1] + (8 + 2 * level) * (1 +  level * (level + 1)))
        return l
    
    def increase_round(self):
        """
        One more round
        """
        self.round += 1
        
        
    def change_active_weapon(self, type):
        """
        change the active weapon of the player
        """
        n = type / 10 - 1
        if self.weapon_flavor[n] <> WEAPON_NONE:
            if self.active_weapon == n:
                self.add_message("You already hold your %s %s." % (WEAPON_FLAVOR[self.weapon_flavor[n]], WEAPON_NAME[n]),
                                 curses.color_pair(curses.COLOR_BLUE))
            else:
                self.active_weapon = n
                self.add_message("You take your %s %s." % (WEAPON_FLAVOR[self.weapon_flavor[n]], WEAPON_NAME[n]),
                                 curses.color_pair(curses.COLOR_BLUE))
        else:
            self.add_message("You have no %s in your equipment." % WEAPON_NAME[n],
                             curses.color_pair(curses.COLOR_BLUE))
            
            
    def weapon_name(self, weap_num, weap_flav):
        """
        Returns the name of the weapon
        """
        if WEAPON_FLAVOR[weap_flav][0] in "aeiou":
            return "an %s %s" % (WEAPON_FLAVOR[weap_flav], WEAPON_NAME[weap_num])
        else:
            return "a %s %s" % (WEAPON_FLAVOR[weap_flav], WEAPON_NAME[weap_num])
        
        
    def inventory(self):
        """
        List all the player belongings
        """
        mess = "You have"
        for n in range(len(self.weapon_flavor)):
            if self.weapon_flavor[n] <> WEAPON_NONE:
                mess = "%s %s," % (mess, self.weapon_name(n, self.weapon_flavor[n]))
        self.add_message("%s." % mess[:-1], curses.color_pair(curses.COLOR_BLUE))
        
    
    def dist(self, pos1, pos2):
        """
        calculate the distance between pos1 and pos2
        """
        dx = abs(pos1[0] - pos2[0])
        dy = abs(pos1[1] - pos2[1])
        return dx + dy
    
    
    def blocked(self, pos):
        """
        True is (x, y) cannot be accessed, False either
        """
        if self.dsize[0] >= pos[0] >= 0 and self.dsize[1] >= pos[1] >= 0:
            return self.dungeon.floor[pos] == DUNGEON_WALL \
                or self.dungeon.has_special(pos, SPECIAL_DOOR)
        return False
                
                
    def lit(self, pos):
        """
        True if cell at pos is lit, False either
        """
        return self.seen[pos] == 1
        
        
    def set_lit(self, pos):
        """
        Light the cell at pos
        """
        if 0 <= pos[0] <= self.dsize[0] and 0 <= pos[1] <= self.dsize[1]:
            self.seen[pos] = 1
            self.known[pos] = 1
    
    
    def _cast_light(self, cx, cy, row, start, end, radius, xx, xy, yx, yy, id):
        """
        Recursive lightcasting function
        """
        if start < end:
            return
        radius_squared = radius*radius
        for j in range(row, radius+1):
            dx, dy = -j-1, -j
            blocked = False
            while dx <= 0:
                dx += 1
                # Translate the dx, dy coordinates into map coordinates:
                X, Y = cx + dx * xx + dy * xy, cy + dx * yx + dy * yy
                # l_slope and r_slope store the slopes of the left and right
                # extremities of the square we're considering:
                l_slope, r_slope = (dx-0.5)/(dy+0.5), (dx+0.5)/(dy-0.5)
                if start < r_slope:
                    continue
                elif end > l_slope:
                    break
                else:
                    # Our light beam is touching this square; light it:                    
                    if dx*dx + dy*dy < radius_squared:
                        self.set_lit((X, Y))
                    if blocked:
                        # we're scanning a row of blocked squares:
                        if self.blocked((X, Y)):
                            new_start = r_slope
                            continue
                        else:
                            blocked = False
                            start = new_start
                    else:
                        if self.blocked((X, Y)) and j < radius:
                            # This is a blocking square, start a child scan:
                            blocked = True
                            self._cast_light(cx, cy, j+1, start, l_slope,
                                             radius, xx, xy, yx, yy, id+1)
                            new_start = r_slope
            # Row is scanned; do next row unless last square was blocked:
            if blocked:
                break
    
    
    def do_fov(self, radius):
        """
        Calculate lit squares from the given location and radius
        """
        self.seen = N.zeros(self.dsize)
        x, y = self.pos
        for oct in range(8):
            self._cast_light(x, y, 1, 1.0, 0.0, radius,
                             MULT[0][oct], MULT[1][oct],
                             MULT[2][oct], MULT[3][oct], 0)
        
                
    def deal_damage(self, tile, n):
        """
        The player suffers n HPs of damage
        """
        self.hp -= n
        hit = random.choice(("bites", "claws", "slashes", "scratches", "chomps", \
                             "gnaws", "gnarls", "kicks", "tears", "rips", "stings"))
        if self.hp > 0:
            self.add_message("%s %s you for %s point(s) of damage !" % (tile, hit, n),
                             curses.color_pair(curses.COLOR_MAGENTA))
        else:
            self.add_message("%s %s you for %s point(s) of damage ! Maybe next time..." % (tile, hit, n),
                             curses.color_pair(curses.COLOR_MAGENTA))
            self.exit = 1  
            
    
    def add_mushroom_seen(self, pos):
        """
        Remembers a mushroom seen by the player
        """
        try:
            n = self.mush_seen.index(pos)
        except ValueError:
            self.mush_seen.append(pos)
            
            
    def remove_mushroom_seen(self, pos):
        """
        Removes a mushroom from the list self.mush_seen
        """
        try:
            n = self.mush_seen.index(pos)
            self.mush_seen.pop(n)
        except ValueError:
            pass
        
    
    def closest_mushroom(self, pos):
        """
        Finds the closest mushroom to the pos of a monster
        """
        if len(self.mush_seen) == 0:
            # No mushroom seen
            return (0, 0), 1000
        else:
            d_mush = 1000
            for pos_found in self.mush_seen:
                d = self.dist(pos_found, pos)
                if d < d_mush:
                    d_mush = d
                    pos_mush = pos_found
            return pos_mush, d_mush
        
    
    def move_monsters(self):
        """
        Browse all monsters and move awaken ones
        """
        # Temporary dictionary
        monsters = {}
        # Flag for axe fight
        flag_hit = False
        
        for pos, monster in self.dungeon.monster.items():
            move = False
            if monster.is_awake():
                # Choose the closest between closest mushroom and player
                pos_mush, d_mush = self.closest_mushroom(pos)
                d_player = self.dist(pos, self.pos)
                if d_player < d_mush:
                    spos, d = self.pos, d_player
                else:
                    spos, d = pos_mush, d_mush
                
                if d == 1 and d_player < d_mush:
                    flag_hit = True
                    self.deal_damage(monster.tile(), monster.damage())
                    if self.exit > 0:
                        break
                elif d == 0 and not d_player < d_mush:
                    self.dungeon.grab_special(pos_mush, SPECIAL_MUSHROOM)
                    monster.upgrade()
                    self.remove_mushroom_seen(pos_mush)
                    self.add_message("The monster eats a mushroom and progress to level %s." % monster.level,
                                     curses.color_pair(curses.COLOR_MAGENTA))
                    
                else:
                    # The monster moves to the target
                    d_new = d
                    random.shuffle(DIRS)
                    for dir in DIRS:
                        pos_temp = (pos[0] + dir[0], pos[1] + dir[1])
                        if self.dungeon.is_reachable(pos_temp) and \
                        not monsters.has_key(pos_temp):
                            d_temp = self.dist(pos_temp, spos)
                            if d_temp <= d_new:
                                d_new, pos_new = d_temp, pos_temp
                    move = d_new < d
            if move:
                monsters[pos_new] = monster
            else:
                monsters[pos] = monster
        # Hack (cannot change dict while browsing it)        
        self.dungeon.monster = monsters
        
        if flag_hit:
            self.hit_by_monster = True
        else:
            self.hit_by_monster = False
            
    
    def tile(self):
        """
        Player tile on the screen
        """
        return "&%s" % WEAPON_TILE[self.active_weapon]
        
    def display(self, s):
        """
        Display the dungeon floor on the given curses
        """
        # Screen init
        h, w = s.getmaxyx()
        n = self.cols + 1
        """
        s.addstr(1, 0, '_' * (w - n - 1))
        s.vline(2, w - n - 1, '|', h - 1)
        """
        # Clear the screen
        for i in range(2, h - 1):
            s.addstr(i, 0, " " * (w - n - 1))
        
        for x in range(self.dsize[0]):
            xx = (x - self.pos[0] + (w - n)/ 4) * 2 + 1
            if 0 <= xx < w - n:
                for y in range(self.dsize[1]):
                    yy = y - self.pos[1] + h / 2 - 2
                    if 1 < yy < h - 1:
                        color = curses.color_pair(curses.COLOR_WHITE)
                        ch = "  "
                        if self.known[x, y] == 1:    
                            if x == self.pos[0] and y == self.pos[1]:
                                # Display the player
                                ch = self.tile()
                                if self.hp < self.hpmax / 3:
                                    color = curses.color_pair(curses.COLOR_RED)
                                elif self.hp < self.hpmax * 2 / 3:
                                    color = curses.color_pair(curses.COLOR_YELLOW)                                
                                else:
                                    color = curses.color_pair(curses.COLOR_WHITE)
                                colorhp = color
                            else:
                                if self.lit((x, y)) and self.dungeon.monster.has_key((x, y)):
                                    # Display a monster
                                    ch = self.dungeon.monster[(x, y)].tile()
                                    color = curses.color_pair(curses.COLOR_MAGENTA)
                                    self.dungeon.monster[(x, y)].awake()
                                    
                                elif self.dungeon.special[x, y] >= WEAPON_SWORD:
                                    # Display a weapon
                                    weap_flav, weap_type = self.dungeon.weapon_params((x, y))
                                    ch = "%s%s" % (WEAPON_TILE[weap_type], weap_flav)
                                    color = curses.color_pair(curses.COLOR_BLUE)
                                    if weap_flav > self.weapon_flavor[weap_type]:
                                        color = color | curses.A_BOLD 
                                    
                                elif self.dungeon.has_special((x, y), SPECIAL_DOOR):
                                    ch = "++"
                                    
                                elif self.dungeon.has_special((x, y), SPECIAL_UPSTAIRS):
                                    ch = "<<"
                                    color = curses.color_pair(curses.COLOR_YELLOW)
                                    
                                elif self.dungeon.has_special((x, y), SPECIAL_DOWNSTAIRS1) or \
                                     self.dungeon.has_special((x, y), SPECIAL_DOWNSTAIRS2):
                                    ch = ">>"
                                    color = curses.color_pair(curses.COLOR_YELLOW)
                                    
                                elif self.dungeon.has_special((x, y), SPECIAL_MUSHROOM):
                                    ch = "* "
                                    color = curses.color_pair(curses.COLOR_GREEN)
                                    self.add_mushroom_seen((x, y))            
                                    
                                elif self.dungeon.has_special((x, y), SPECIAL_POTION):
                                    ch = "! "
                                    color = curses.color_pair(curses.COLOR_GREEN)   
                                                                     
                                elif self.dungeon.floor[x, y] == DUNGEON_WALL:
                                    ch = "##"
                                    
                                else:
                                    if self.lit((x, y)):
                                        ch = ". "
                        
                        s.addstr(yy, xx, ch, color)
        
        info = "Level %s | HP %s / %s | XP %s (%s for lvl %s) | Potion %s | Mushroom %s | Round %s" % \
               (self.deepness, self.hp, self.hpmax, 
                self.xp, self.levels[self.level], self.level+1,
                self.potion, self.mushroom, self.round)
        s.addstr(0, 0, self.line)
        s.addstr(0, 0, info, curses.color_pair(curses.COLOR_WHITE))
        
        nb = min(h-4, len(self.message))
        for i in range(nb):
            s.addstr(i+3, w - n + 1, " " * n)
            s.addstr(i+3, w - n + 1, self.message[nb-i-1][0], self.message[nb-i-1][1])
            
        s.refresh()
        
        
    def load_dungeon_floor(self, pos, save = True):
        """
        Loads a dungeon from its name, its level and a position
        """
        self.mush_seen = []
        if save:
            # We save the information of the dungeon
            self.dict_dungeon[self.dungeon.name] = (self.dungeon, self.known)
            
        name = "%s-%s (%s, %s)" % (self.dname, self.deepness, pos[0], pos[1])
        
        if self.dict_dungeon.has_key(name):
            # This floor has already been visited
            self.dungeon, self.known = self.dict_dungeon[name]
        else:
            self.dungeon = Dungeon(self.dsize, 45, name, self.deepness)
            self.known = N.zeros(self.dsize)
            
        
    def add_message(self, message, color = None):
        """
        Add a message
        """
        if color is None:
            color = curses.color_pair(curses.COLOR_WHITE)
        list = []
        elt = ""
        for word in message.split(' '):
            if len(elt) + len(word) < self.cols:
                elt += " " + word
            else:
                list.append(elt[1:])
                elt = " " + word
        list.append(elt[1:])
        for mess in list:
            self.message.insert(0, (mess, color))
            if len(self.message) > 100:
                self.message.pop()
        
    
    def next_level(self):
        """
        Returns the next level cap
        self.level * (self.level + 1) * (self.level + 2)
        """
        return 10 * self.level * (self.level + 1)
    
    def progress(self, xp):
        """
        Player gets xp
        """
        self.xp += xp
        if self.xp >= self.levels[self.level]: #self.next_level():
            self.level += 1
            self.hpmax += self.hpmax / 2
            self.hp = self.hpmax
            self.add_message("You gain %s XP points and progress to level %s !" % (xp, self.level),
                             curses.color_pair(curses.COLOR_YELLOW) | curses.A_BOLD)
        else:
            self.add_message("You gain %s XP." % xp)
            
        if xp == 150:
            self.winner = True
            self.add_message("Congratulations ! You have slayed ME. You can now retire to your native village.",
                             curses.color_pair(curses.COLOR_YELLOW) | curses.A_BOLD)
            self.exit = 2

        
    def drink(self):
        """
        drinks a potion
        """
        if self.potion > 0:
            self.potion -= 1
            self.add_message("You drink a health potion and recover all your health points !",
                             curses.color_pair(curses.COLOR_GREEN))
            self.hp = self.hpmax
            
    
    def rest(self):
        """
        rest to recover HP by eating mushrooms
        """
        for pos, monster in self.dungeon.monster.items():
            if monster.is_awake():
                self.add_message("You cannot rest while there are awaken monsters around.")
                return
        if self.hp == self.hpmax: 
            self.add_message("You are already fully rested.")
            return
        hp_rest = min(self.mushroom, self.hpmax - self.hp)
        self.hp += hp_rest
        self.mushroom -= hp_rest
        if hp_rest == 0:
            self.add_message("You rest for a while but recover no health points. You need mushrooms !")
        elif self.hp == self.hpmax: 
            self.add_message("You rest for a while and eat enough mushrooms to recover all your health points.",
                             curses.color_pair(curses.COLOR_GREEN))
        else:
            self.add_message("You rest for a while and eat enough mushrooms to recover %s health point(s)." % hp_rest,
                             curses.color_pair(curses.COLOR_GREEN))

    
    def damage(self):
        """
        How much damage the player inflicts
        """
        l1 = self.weapon_flavor[self.active_weapon]
        if self.active_weapon == WEAPON_AXE or l1 == WEAPON_FLAVOR5:
            l1 += self.level
        l2 = l1 + self.level
        return 1 + random.randint(l1, l2) + random.randint(l1, l2)
        
    
    def save_score(self):
        """
        Save score in dictionary
        """
        if self.winner:
            print "You are a winner !"
            # Hack : when winner, the least rounds the better (for sorting ranks)
            score = 1000000 - self.round
            new_entry = "** WINNER ** in %s rounds." % self.round
        else:
            print "You scored %s." % player.xp
            score = self.xp
            new_entry = "Score %s - %s" % (score, time.strftime("%c"))
        print "------------------"
        
        if not self.scores.has_key(self.dname_full):
            # This is the first score in this dungeon
            self.scores[self.dname_full] = []
            
        self.scores[self.dname_full].append((score, new_entry))
        self.scores[self.dname_full].sort(lambda x, y: cmp(y[0],x[0]))
        
        print "%s ranks :" % self.dname_full
        print "-" * (len(self.dname_full) + 8)
        for i in range(len(self.scores[self.dname_full])):
            print "%s : %s" % (i+1, self.scores[self.dname_full][i][1])
            
            
    def grab_mushroom(self, pos):
        """
        Grab a mushroom when you to it
        or when you stay over one
        """ 
        self.add_message("You grab a mushroom.")
        self.mushroom += self.dungeon.grab_special(pos, SPECIAL_MUSHROOM)
        self.remove_mushroom_seen(pos)
                    
    
    def move(self, pos):
        """
        Move the player to the new pos
        """
        if pos[0] == self.pos[0] and pos[1] == self.pos[1]:
            if self.dungeon.has_special(pos, SPECIAL_MUSHROOM):
                # mushroom
                self.grab_mushroom(pos)
            else:
                player.add_message("You wait...")
            
        elif self.dungeon.has_monster(pos):
            tile = self.dungeon.monster[pos].tile()
            if self.hit_by_monster and self.active_weapon == 1:
                # Axe
                self.add_message("You are too fuzzy to use your axe.")
            elif self.active_weapon == 3:
                # Warhammer
                n = self.weapon_flavor[self.active_weapon] + 1
                if random.randint(0, n + self.dungeon.monster[pos].level) <= n:
                    self.dungeon.monster[pos].stun(n)
                    self.add_message("You stun %s for %s rounds." % (tile, n),
                                     curses.color_pair(curses.COLOR_YELLOW))
                else:
                    self.add_message("You try to stun %s, but fail." % tile)
                
            else:
                n = self.damage()
                l = self.dungeon.monster[pos].level
                xp = self.dungeon.bash_monster(pos, n)
                if xp > 0:
                    self.add_message("You hit %s for %s points of damage, killing him." % (tile, n),
                                     curses.color_pair(curses.COLOR_YELLOW))
                    self.progress(xp)
                    n = self.dungeon.add_mushroom(pos, l + 1)
                    self.add_message("%s drops %s mushroom(s) on the floor." % (tile, n)) 
                    if self.active_weapon == 2:
                        # Spear
                        pos_next = (2 * (pos[0] - self.pos[0]) + self.pos[0],
                                    2 * (pos[1] - self.pos[1]) + self.pos[1])
                        self.pos = (pos[0], pos[1])
                        self.move(pos_next)
                else:
                    self.add_message("You hit %s for %s points of damage." % (tile, n),
                                     curses.color_pair(curses.COLOR_YELLOW))
                         
        elif self.dungeon.has_special(pos, SPECIAL_DOOR):
            self.add_message("You open the door.")
            self.dungeon.grab_special(pos, SPECIAL_DOOR)
            
        elif self.dungeon.is_reachable(pos):
            self.pos = (pos[0], pos[1])
            
            if self.dungeon.special[pos] >= WEAPON_SWORD:
                weap_flav = self.dungeon.special[pos] % 10
                weap_type = self.dungeon.special[pos] - weap_flav
                weap_num = weap_type / 10 - 1
                if weap_flav > self.weapon_flavor[weap_num]:
                    self.add_message("You grab %s." % self.weapon_name(weap_num, weap_flav),
                                     curses.color_pair(curses.COLOR_BLUE))
                    self.weapon_flavor[weap_num] = weap_flav
                    self.dungeon.special[pos] = 0
                elif weap_flav < self.weapon_flavor[weap_num]:
                    self.add_message("You already have a better %s." % WEAPON_NAME[weap_num])
                else:                    
                    self.add_message("You already have %s." % self.weapon_name(weap_num, weap_flav))
            
            if self.dungeon.has_special(pos, SPECIAL_MUSHROOM):
                # mushroom
                self.grab_mushroom(pos)
                
            
            elif self.dungeon.has_special(pos, SPECIAL_POTION):
                # potion
                self.add_message("You grab a health potion.")
                self.potion += self.dungeon.grab_special(pos, SPECIAL_POTION)

            elif self.dungeon.has_special(pos, SPECIAL_DOWNSTAIRS1) or \
                 self.dungeon.has_special(pos, SPECIAL_DOWNSTAIRS2):
                # Stairs down
                self.add_message("You go down the stairs.")
                self.deepness += 1
                self.load_dungeon_floor(pos)
                self.pos = self.dungeon.pos_start
                self.start_pos.append(pos)
                self.known[self.pos] = 1
                
            elif self.dungeon.has_special(pos, SPECIAL_UPSTAIRS):
                # Stairs up
                if self.deepness == 1:
                    self.add_message("You exit the mines.")
                    self.exit = 2
                else:
                    self.add_message("You go up the stairs.")
                    self.deepness -= 1
                    self.pos = self.start_pos[-1]
                    self.start_pos.pop()
                    self.load_dungeon_floor(self.start_pos[-1])
                    self.known[self.pos] = 1                
        else:
            self.add_message("You cannot go there.")

            
###############################################################################
# Main
###############################################################################
def load_config():
    """
    Loads moe.ini file
    """
    conf = ConfigParser.ConfigParser()
    try:
        conf.read('moe.ini')
        name = conf.get("Game", "Name")
        dirs = conf.get("Game", "Dirs")
        width = conf.getint("Game", "Width")
        height = conf.getint("Game", "Height")
        cols = conf.getint("Game", "Cols")
        return name, eval(dirs), width, height, cols
    except:
        try:
            os.remove('moe.ini')
        except:
            pass
        conf.add_section('Game')
        conf.set('Game', 'Name', 'Moria')
        conf.set('Game', 'Width', 40)
        conf.set('Game', 'Height', 40)
        conf.set('Game', 'Cols', 30)
        conf.set('Game', 'Dirs', CURSES_DIRS)
        # Save the config file
        conf.write(open('moe.ini', 'w'))
        return "Moria", CURSES_DIRS, 40, 40, 30
    

def save(player):
    """
    save to a file
    """
    output = open('moe.sav', 'wb')
    # Pickle the player
    pickle.dump(player, output, pickle.HIGHEST_PROTOCOL)
    output.close()
    

if __name__ == "__main__":
    try:
        import psyco
        psyco.full()
    except ImportError:
        pass
    
    # Curses init
    s = curses.initscr()
    curses.start_color()
    curses.noecho()
    curses.cbreak()
    c = []
    for i in range(1, 16):
        curses.init_pair(i, i % 8, 0)
        if i < 8:
            c.append(curses.color_pair(i))
        else:
            c.append(curses.color_pair(i) | curses.A_BOLD)
    s.keypad(1)
    curses.curs_set(0)
    
    # Player init
    mine_name, dirs, w, h, cols = load_config()
    try:
        pkl_file = open('moe.sav', 'rb')
        player = pickle.load(pkl_file)
        pkl_file.close()
        if player.hp <= 0 or player.exit == 2:
            player = Player(s, (w, h), mine_name, cols, player.scores)
    except:
        player = Player(s, (w, h), mine_name, cols)
    player.do_fov(player.fov)
    player.display(s)
    try:
        while True:
            k = s.getch()
            if k == 27:
                break
            elif CURSES_DIRS.has_key(k):
                new_pos = (player.pos[0] + dirs[k][0], player.pos[1] + dirs[k][1])
                player.move(new_pos)
                player.do_fov(player.fov)
            elif k == ord('d'):
                player.drink()
            elif k == ord('r'):
                player.rest()
            elif k == ord('1'):
                player.change_active_weapon(WEAPON_SWORD)
            elif k == ord('2'):
                player.change_active_weapon(WEAPON_AXE)
            elif k == ord('3'):
                player.change_active_weapon(WEAPON_SPEAR)
            elif k == ord('4'):
                player.change_active_weapon(WEAPON_WARHAMMER)
            elif k == ord('i'):    
                player.inventory()
            player.increase_round()
            player.move_monsters()       
            player.display(s)
            if player.exit > 0:
                break        
    finally:
        # Curses ending
        s.keypad(0)
        curses.echo()
        curses.nocbreak()
        curses.endwin()
    
    # Exit with some words
    print "Last messages :"
    print "---------------"
    n = min(10, len(player.message))
    for i in range(n):
        print player.message[n-i-1][0]
    print "---------------"
    
    if player.hp <= 0 or player.exit == 2:
        player.save_score()
        
    save(player)

        
    
