#!/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 ############################################################################### # from http://www.geocities.com/anvrill/names/cc_goth.html PLACES = ['Adara', 'Adena', 'Adrianne', 'Alarice', 'Alvita', 'Amara', 'Ambika', 'Antonia', 'Araceli', 'Balandria', 'Basha', 'Beryl', 'Bryn', 'Callia', 'Caryssa', 'Cassandra', 'Casondrah', 'Chatha', 'Ciara', 'Cynara', 'Cytheria', 'Dabria', 'Darcei', 'Deandra', 'Deirdre', 'Delores', 'Desdomna', 'Devi', 'Dominique', 'Drucilla', 'Duvessa', 'Ebony', 'Fantine', 'Fuscienne', 'Gabi', 'Gallia', 'Hanna', 'Hedda', 'Jerica', 'Jetta', 'Joby', 'Kacila', 'Kagami', 'Kala', 'Kallie', 'Keelia', 'Kerry', 'Kerry-Ann', 'Kimberly', 'Killian', 'Kory', 'Lilith', 'Lucretia', 'Lysha', 'Mercedes', 'Mia', 'Maura', 'Perdita', 'Quella', 'Riona', 'Safiya', 'Salina', 'Severin', 'Sidonia', 'Sirena', 'Solita', 'Tempest', 'Thea', 'Treva', 'Trista', 'Vala', 'Winta'] 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_DIRS8 = {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") ############################################################################### # Markov Name model # A random name generator, by Peter Corbett # http://www.pick.ucam.org/~ptc24/mchain.html # This script is hereby entered into the public domain ############################################################################### class Mdict: def __init__(self): self.d = {} def __getitem__(self, key): if self.d.has_key(key): return self.d[key] else: raise KeyError(key) def add_key(self, prefix, suffix): if self.d.has_key(prefix): self.d[prefix].append(suffix) else: self.d[prefix] = [suffix] def get_suffix(self,prefix): l = self[prefix] return random.choice(l) class MName: """A name from a Markov chain""" def __init__(self, chainlen = 2): """ Building the dictionary """ if chainlen > 10 or chainlen < 1: print "Chain length must be between 1 and 10, inclusive" sys.exit(0) self.mcd = Mdict() oldnames = [] self.chainlen = chainlen for l in PLACES: l = l.strip() oldnames.append(l) s = " " * chainlen + l for n in range(0,len(l)): self.mcd.add_key(s[n:n+chainlen], s[n+chainlen]) self.mcd.add_key(s[len(l):len(l)+chainlen], "\n") def New(self): """ New name from the Markov chain """ random.seed(time.time()) prefix = " " * self.chainlen name = "" suffix = "" while True: suffix = self.mcd.get_suffix(prefix) if suffix == "\n" or len(name) > 9: break else: name = name + suffix prefix = prefix[1:] + suffix return name.capitalize() ############################################################################### # 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.mname = MName() if dungeon_name == "random" or dungeon_name == "Random": self.dname = MName().New() else: self.dname = dungeon_name self.dsize = size self.dname_full = "%s (%s, %s)" % (self.dname, self.dsize[0], self.dsize[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.deepness_max = 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 self.add_message("You enter the mines of %s." % self.dname, curses.color_pair(curses.COLOR_WHITE)) 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 help(self): """ Help in game """ help = ("General help :", "--------------", "You are a young hero and to prove your strength, you decide to explore the mines nearby your native village.", "But beware ! It is said that those mines are haunted by dangerous monsters.", "Only armed with your training sword and a health potion, you enter a maze of rooms and corridors.", "", "- 'F2' : how to play", "- 'F3' : the tiles on screen", "- 'F4' : the keys", "--------", "(More to read in the readme.txt file)") for mess in help: self.add_message(mess, curses.color_pair(curses.COLOR_CYAN)) def help_f2(self): """ Help in game """ help = ("How to play ?", "-------------", "This game is a roguelike. It is turn-based, and once your character dies there is no loading back.", "By moving in the four directions, you attack monsters, open doors, collect equipment or use stairs.", "Your goal is to survive long enough in the mines, progressing by killing monsters and descending as deep as you can.", "It is said that your true nemesis awaits for you at the bottom...", "--------", "(More to read in the readme.txt file)") for mess in help: self.add_message(mess, curses.color_pair(curses.COLOR_CYAN)) def help_f3(self): """ Help in game : the tiles """ help = ("The tiles in the game", "---------------------", "## : walls", ".. : floor you can see", "++ : a door; bump into it to open it", "<< : descending stairs", ">> : ascending stairs", "M~ : monster sleeping; it will awake at your approach", "Mn : monster level n", "! : a health potion; (d)rink it to recover all your Health Points", "* : a magic mushroom; when you (r)est, you can eat them to recover HP. Don't let monsters eat them !", "/n : a sword of level n", "Pn : an axe of level n", "|n : a spear of level n", "Tn : a warhammer of level n", "&/ : you with your weapon ! (a sword)", "--------", "(More to read in the readme.txt file)") for mess in help: self.add_message(mess, curses.color_pair(curses.COLOR_CYAN)) def help_f4(self): """ Help in game : the keys """ help = ("The keys of the game", "--------------------", "- h : general help", "- F2: how to play", "- F3: the keys in the game", "- F4: the tiles in the game", "- keyboard arrows to move and attack North, South, East and West", "- space : wait for a round", "- d : drink a health potion", "- r : rest to recover HP by eating mushrooms", "- 1 : equip your best sword", "- 2 : equip your best axe", "- 3 : equip your best spear", "- 4 : equip your best warhammer", "- i : list your weapons and their quality", "- esc : save and close the game", "--------", "(More to read in the readme.txt file)") for mess in help: self.add_message(mess, curses.color_pair(curses.COLOR_CYAN)) 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) | ! %s | * %s | Round %s | Hit 'h' for help" % \ (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 - Max deepness lvl %s - Player was level %s (%s)" \ % (self.round, self.deepness_max, self.level, time.strftime("%c")) else: print "You scored %s." % player.xp score = self.xp new_entry = "Score %s - Max deepness lvl %s - Player was level %s (%s)" \ % (score, self.deepness_max, self.level, time.strftime("%c")) print "------------------" file = open("%s_morgue_%sx%s.txt" % (self.dname, self.dsize[0], self.dsize[1]), "a") file.write(new_entry) file.close() 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, coef = 1): """ Move the player to the new pos coef is an XP multiplier when you combo monsters (1, 2, 4, 8, ...) """ 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) * coef if xp > 0: self.progress(xp) if self.active_weapon == 2: # Spear self.add_message("You charge %s for %s points of damage, killing him." % (tile, n), curses.color_pair(curses.COLOR_YELLOW)) 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, coef * 2) else: self.add_message("You hit %s for %s points of damage, killing him." % (tile, n), curses.color_pair(curses.COLOR_YELLOW)) n = self.dungeon.add_mushroom(pos, l + 1) self.add_message("%s drops %s mushroom(s) on the floor." % (tile, n)) 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.deepness_max = max(self.deepness_max, self.deepness) 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.") return False return True ############################################################################### # 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: move = False 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]) move = player.move(new_pos) player.do_fov(player.fov) elif k == ord('d'): player.drink() move = True elif k == ord('r'): player.rest() move = True elif k == ord('1'): player.change_active_weapon(WEAPON_SWORD) move = True elif k == ord('2'): player.change_active_weapon(WEAPON_AXE) move = True elif k == ord('3'): player.change_active_weapon(WEAPON_SPEAR) move = True elif k == ord('4'): player.change_active_weapon(WEAPON_WARHAMMER) move = True else: if k == ord('i'): player.inventory() elif k == ord('h'): player.help() elif k == curses.KEY_F2: player.help_f2() elif k == curses.KEY_F3: player.help_f3() elif k == curses.KEY_F4: player.help_f4() if move: 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)