💾 Archived View for gemini.ctrl-c.club › ~nttp › games › tots3.py captured on 2024-03-21 at 15:43:25.

View Raw

More Information

⬅️ Previous capture (2021-12-03)

-=-=-=-=-=-=-

#!/usr/bin/env python3
# coding=utf-8

# Tomb of the Snake -- a coffeebreak roguelike for the Linux console
# 2021-10-25 Felix Pleșoianu <https://felix.plesoianu.ro/>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import division

import random

import curses
from curses.textpad import rectangle

WHITE = 0
RED = 1
GREEN = 2
BLUE = 3
YELLOW = 4

dirkeys = {
	ord('h'): 'w', ord('j'): 's', ord('k'): 'n', ord('l'): 'e',
	curses.KEY_LEFT: 'w', curses.KEY_DOWN: 's',
	curses.KEY_UP: 'n', curses.KEY_RIGHT: 'e',
}

fxdescs = {"fire": "on fire", "poison": "poisoned"}

traitadjs = {"muscle": "stronger", "stamina": "sturdier",
	"agility": "nimbler", "speed": "faster", "senses": "sharper"}

blurb = [
	"You are lost in the woods.",
	"Dark clouds hang in the sky.",
	"The day has turned to night.",
	"Now you will have to brave the..."
]

title_banner = [
" ______           __          ___  __  __         ____          __      ",
"/_  __/__  __ _  / /    ___  / _/ / /_/ /  ___   / __/__  ___ _/ /_____ ",
" / / / _ \/  ' \/ _ \  / _ \/ _/ / __/ _ \/ -_) _\ \/ _ \/ _ `/  '_/ -_)",
"/_/  \___/_/_/_/_.__/  \___/_/   \__/_//_/\__/ /___/_//_/\_,_/_/\_\\__/ "
]

help_text = [
	"Dive to level 10 of the dungeon",
	"and bring the idol back above the ground.",
	"",
	"Arrow keys or h-j-k-l to move; . to wait.",
	"< and > to go up/down a level.",
	"'r' and a direction to run until stopped.",
	"",
	"? to show instructions during the game.",
	"@ to show the character sheet.",
	"",
	"Click a map square to look at it.",
	"Double-click to shoot."
]

ending_text = [
	"As fresh surface air touches the idol,",
	"its golden shine quickly becomes tarnished.",
	"Vile gasses spew out of the idol. You drop it,",
	"and it tumbles back down the stairs.",
	"The entrance to the catacomb collapses, then...",
	"...the clouds part and the sun comes out!",
	"",
	"You have won!"
]

death_banner = [
"__  __               __                            ___          ____",
"\ \/ /___  __  __   / /_  ____ __   _____     ____/ (_)__  ____/ / /",
" \  / __ \/ / / /  / __ \/ __ `/ | / / _ \   / __  / / _ \/ __  / / ",
" / / /_/ / /_/ /  / / / / /_/ /| |/ /  __/  / /_/ / /  __/ /_/ /_/  ",
"/_/\____/\__,_/  /_/ /_/\__,_/ |___/\___/   \__,_/_/\___/\__,_(_)   "
]

terrain = {
	"village": {
		ord('.'): {"name": "grass", "color": WHITE},
		ord(','): {"name": "a dirt road", "color": YELLOW},
		ord('#'): {"name": "a cabin wall", "color": RED},
		ord('='): {"name": "a wooden plank", "color": YELLOW},
		ord('|'): {"name": "a tree", "color": RED},
		ord('^'): {"name": "a tree", "color": GREEN},
		ord('~'): {"name": "water", "color": BLUE},
		ord('>'): {"name": "a way down", "color": WHITE}
	},
	
	"cave": {
		ord('.'): {"name": "the cave floor", "color": WHITE},
		ord('#'): {"name": "the cave wall", "color": WHITE},
		ord('~'): {"name": "water", "color": BLUE},
		ord('>'): {"name": "a way down", "color": WHITE},
		ord('<'): {"name": "a way up", "color": WHITE}
	},
	
	"tomb": {
		ord('.'): {"name": "the catacomb floor", "color": WHITE},
		ord('#'): {"name": "the catacomb wall", "color": RED},
		ord('|'): {"name": "a pillar", "color": YELLOW},
		ord('+'): {"name": "a stone coffin", "color": WHITE},
		ord('~'): {"name": "water", "color": BLUE},
		ord('>'): {"name": "a way down", "color": WHITE},
		ord('<'): {"name": "a way up", "color": WHITE}
	}
}

weapons = {
	"stone": {
		"char": "o",
		"color": RED,
		"name": "stone",
		"ammo": 1,
		"weight": 1,
		"damage": 2
	},
	
	"pickaxe": {
		"char": "/",
		"color": BLUE,
		"name": "pickaxe",
		"attack": 1,
		"damage": 4,
		"weight": 6
	},

	"bullet": {
		"char": "o",
		"color": BLUE,
		"name": "sling bullet",
		"ammo": 2,
		"weight": 2,
		"damage": 6
	},
	
	"sword": {
		"char": ")",
		"color": BLUE,
		"name": "short sword",
		"attack": 4,
		"damage": 4,
		"weight": 4
	},
	
	"mace": {
		"char": ")",
		"color": YELLOW,
		"name": "mace",
		"attack": 4,
		"damage": 6,
		"weight": 4
	},
	
	"staff": {
		"char": "/",
		"color": RED,
		"name": "staff",
		"attack": 8,
		"damage": 4,
		"weight": 8
	},
	
	"spear": {
		"char": "/",
		"color": YELLOW,
		"name": "spear",
		"attack": 8,
		"damage": 6,
		"weight": 8
	}
}

armor = {
	"cloth": {
		"char": "[",
		"color": WHITE,
		"name": "thick cloak",
		"armor": 4,
		"weight": 6
	},
	
	"leather": {
		"char": "[",
		"color": RED,
		"name": "leather coat",
		"armor": 6,
		"weight": 8
	},
	
	"chainmail": {
		"char": "]",
		"color": BLUE,
		"name": "chainmail",
		"armor": 8,
		"weight": 10
	},
	
	"scalemail": {
		"char": "]",
		"color": YELLOW,
		"name": "scale mail",
		"armor": 10,
		"weight": 12
	},
	
	"splintmail": {
		"char": "]",
		"color": RED,
		"name": "splint mail",
		"armor": 12,
		"weight": 20
	}
}

shields = {
	"leather": {
		"char": "(",
		"color": RED,
		"name": "leather shield",
		"defense": 4,
		"weight": 4
	},
	
	"wood": {
		"char": "(",
		"color": YELLOW,
		"name": "wooden shield",
		"defense": 6,
		"weight": 6
	},
	
	"bronze": {
		"char": "(",
		"color": GREEN,
		"name": "bronze shield",
		"defense": 10,
		"weight": 10
	}
}

items = {
	"berries": {
		"char": "%",
		"color": BLUE,
		"name": "berries",
		"article": "none",
		"food": 4,
		"weight": 1
	},
	
	"mushroom": {
		"char": "%",
		"color": WHITE,
		"name": "mushroom",
		"food": 8,
		"weight": 1
	},
	
	"ration": {
		"char": "%",
		"color": YELLOW,
		"name": "iron ration",
		"food": 12,
		"weight": 1
	},
	
	"torch": {
		"char": "\\",
		"color": RED,
		"name": "torch",
		"light": 80,
		"weight": 1,
		"attack": 1,
		"defense": 1,
		"damage": 1,
		"special": ("fire", 6, 6)
	},
	
	"idol": {
		"char": "&",
		"color": YELLOW,
		"name": "golden snake idol",
		"weight": 3
	}
}

trophies = {
	"gem": {
		"char": "$",
		"color": GREEN,
		"name": "snake eye gem",
	},

	"bones": {
		"char": "$",
		"color": WHITE,
		"name": "bone necklace",
	},

	"gold": {
		"char": "$",
		"color": YELLOW,
		"name": "gold coin",
	},

	"key": {
		"char": "?",
		"color": BLUE,
		"name": "clock key",
	},

	"ember": {
		"char": "?",
		"color": RED,
		"name": "glowing ember",
	},

	"awakening": {
		"char": "?",
		"color": YELLOW,
		"name": "awakening",
	}
}

creatures = {
	"human": {
		"char": "@",
		"color": WHITE,
		"name": "human",
		"dice_size": 8,
		"muscle": 3,
		"stamina": 3,
		"agility": 3,
		"speed": 3,
		"senses": 3
	},

	"snake": {
		"char": "s",
		"color": GREEN,
		"name": "snake",
		"dice_size": 4,
		"muscle": 3,
		"stamina": 3,
		"agility": 5,
		"speed": 5,
		"can_swim": True,
		"drops": [trophies["gem"], None],
		"mind": "animal",
		"special": ("poison", 4, 4)
	},

	"rat": {
		"char": "r",
		"color": RED,
		"name": "rat",
		"dice_size": 4,
		"muscle": 3,
		"stamina": 3,
		"agility": 5,
		"speed": 5,
		"can_swim": True,
		"mind": "animal"
	},
	
	"bat": {
		"char": "b",
		"color": RED,
		"name": "vampire bat",
		"dice_size": 4,
		"muscle": 4,
		"stamina": 4,
		"agility": 5,
		"speed": 5,
		"can_fly": True,
		"mind": "animal"
	},
	
	"centipede": {
		"char": "c",
		"color": YELLOW,
		"name": "centipede",
		"dice_size": 4,
		"muscle": 2,
		"stamina": 2,
		"agility": 6,
		"speed": 6,
		"mind": "animal",
		"special": ("poison", 4, 4)
	},
	
	"worms": {
		"char": "w",
		"color": YELLOW,
		"name": "mass of worms",
		"dice_size": 6,
		"muscle": 5,
		"stamina": 6,
		"agility": 5,
		"speed": 5,
		"mind": "animal"
	},

	"zombie": {
		"char": "Z",
		"color": GREEN,
		"name": "zombie",
		"dice_size": 8,
		"muscle": 5,
		"stamina": 6,
		"agility": 4,
		"speed": 3,
		"mind": "undead"
	},

	"skeleton": {
		"char": "K",
		"color": WHITE,
		"name": "skeleton",
		"dice_size": 8,
		"muscle": 5,
		"stamina": 4,
		"agility": 5,
		"speed": 5,
		"drops": [trophies["bones"], None],
		"mind": "undead"
	},

	"mummy": {
		"char": "M",
		"color": YELLOW,
		"name": "mummy",
		"dice_size": 8,
		"muscle": 5,
		"stamina": 5,
		"agility": 5,
		"speed": 6,
		"drops": [trophies["gold"], None],
		"mind": "undead"
	},

	"ghoul": {
		"char": "G",
		"color": GREEN,
		"name": "ghoul",
		"dice_size": 10,
		"muscle": 5,
		"stamina": 5,
		"agility": 4,
		"speed": 4,
		"mind": "undead"
	},

	"clockwork": {
		"char": "A",
		"color": BLUE,
		"name": "animated armor",
		"dice_size": 10,
		"muscle": 5,
		"stamina": 5,
		"agility": 5,
		"speed": 5,
		"drops": [trophies["key"], None],
		"mind": "undead"
	},
	
	"hellhound": {
		"char": "h",
		"color": RED,
		"name": "hellhound",
		"dice_size": 6,
		"muscle": 6,
		"stamina": 6,
		"agility": 6,
		"speed": 6,
		"drops": [trophies["ember"], None],
		"mind": "undead",
		"special": ("fire", 6, 6)
	},

	"nightmare": {
		"char": "N",
		"color": RED,
		"name": "nightmare",
		"dice_size": 12,
		"muscle": 4,
		"stamina": 4,
		"agility": 4,
		"speed": 4,
		"drops": [trophies["awakening"], None],
		"mind": "undead"
	}
}

levels = [
	{
		"terrain": terrain["village"],
		"name": "Abandoned village",
		"items": [items["berries"], items["berries"], items["mushroom"],
			weapons["stone"], weapons["stone"],
			items["torch"], armor["cloth"]],
		"creatures": [creatures["snake"], creatures["rat"]]
	},

	{
		"terrain": terrain["cave"],
		"name": "Shallow cave",
		"items": [items["ration"], items["mushroom"],
			items["torch"], items["torch"],
			armor["cloth"], weapons["pickaxe"], weapons["pickaxe"],
			weapons["stone"], weapons["stone"]],
		"creatures": [creatures["bat"], creatures["rat"],
			creatures["centipede"]]
	},
	{
		"terrain": terrain["cave"],
		"name": "Cave",
		"items": [items["ration"], items["mushroom"],
			items["torch"], items["torch"],
			armor["cloth"], armor["leather"],
			weapons["pickaxe"], weapons["pickaxe"],
			weapons["stone"], weapons["stone"]],
		"creatures": [creatures["worms"], creatures["centipede"]]
	},
	{
		"terrain": terrain["cave"],
		"name": "Deep cave",
		"items": [items["ration"], items["mushroom"],
			items["torch"], items["torch"], armor["leather"],
			weapons["pickaxe"], weapons["pickaxe"],
			weapons["stone"], weapons["stone"]],
		"creatures": [creatures["worms"], creatures["zombie"],
			creatures["centipede"]]
	},

	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (7, 7),
		"items": [items["torch"], items["torch"],
			weapons["stone"], weapons["bullet"],
			weapons["pickaxe"], weapons["pickaxe"], weapons["mace"],
			armor["chainmail"], shields["leather"]],
		"creatures": [creatures["skeleton"], creatures["worms"],
			creatures["zombie"]]
	},
	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (7, 7),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["mace"], weapons["sword"],
			armor["chainmail"], shields["leather"]],
		"creatures": [creatures["skeleton"], creatures["mummy"],
			creatures["hellhound"]]
	},
	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (7, 7),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["mace"], weapons["sword"],
			armor["chainmail"], shields["leather"]],
		"creatures": [creatures["skeleton"], creatures["mummy"],
			creatures["ghoul"]]
	},
	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (5, 5),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["sword"], weapons["staff"],
			armor["scalemail"], shields["wood"]],
		"creatures": [creatures["ghoul"], creatures["mummy"],
			creatures["hellhound"]]
	},
	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (5, 5),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["spear"], weapons["staff"],
			armor["scalemail"], shields["wood"]],
		"creatures": [creatures["ghoul"], creatures["mummy"],
			creatures["clockwork"]]
	},
	{
		"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (5, 5),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["spear"], weapons["staff"],
			armor["scalemail"], shields["wood"]],
		"creatures": [creatures["hellhound"],
			creatures["ghoul"], creatures["clockwork"]]
	},
	{	"terrain": terrain["tomb"],
		"name": "Catacomb",
		"cellsize": (3, 3),
		"items": [items["torch"], items["torch"], weapons["pickaxe"],
			weapons["bullet"], weapons["spear"],
			armor["splintmail"], shields["bronze"]],
		"creatures": [creatures["hellhound"], creatures["nightmare"],
			creatures["ghoul"], creatures["clockwork"]]
	},
]

scrheight = -1
scrwidth = -1

world = None
worldrng = None
log = None

title_screen = None
character_screen = None
inventory_screen = None
game_screen = None
active_screen = None

finished = False

def curses_main(stdscr):
	global scrheight, scrwidth, world, worldrng, log, active_screen
	global title_screen, game_screen, character_screen, inventory_screen
	
	scrheight, scrwidth = stdscr.getmaxyx()

	if (scrheight < 24 or scrwidth < 80):
		raise RuntimeError("80x24 or larger terminal required.")

	if 'mousemask' in curses.__dict__:
		curses.mousemask(
			curses.BUTTON1_CLICKED | curses.BUTTON1_DOUBLE_CLICKED)
	stdscr.leaveok(0)
	if curses.has_colors():
		curses.init_pair(
			RED, curses.COLOR_RED, curses.COLOR_BLACK)
		curses.init_pair(
			GREEN, curses.COLOR_GREEN, curses.COLOR_BLACK)
		curses.init_pair(
			BLUE, curses.COLOR_CYAN, curses.COLOR_BLACK)
		curses.init_pair(
			YELLOW, curses.COLOR_YELLOW, curses.COLOR_BLACK)
	
	worldrng = random.Random()

	title_screen = TitleScreen(stdscr)
	character_screen = CharacterScreen(stdscr)
	inventory_screen = InventoryScreen(stdscr)
	game_screen = GameScreen(stdscr)
	active_screen = title_screen
	
	while not finished:
		if (scrheight < 24 or scrwidth < 80):
			stdscr.erase()
			stdscr.addstr(
				0, 0, "80x24 or larger terminal required.")
			stdscr.refresh()
		else:
			active_screen.render()
		key = stdscr.getch()
		if key == curses.KEY_RESIZE:
			scrheight, scrwidth = stdscr.getmaxyx()
		else:
			active_screen.handle_key(key)

def draw_dialog_bg(window, top, left, height, width):
	window.attron(curses.A_REVERSE)
	for y in range(height):
		window.addstr(top + y, left, " " * width)
	rectangle(window, top, left, top + height - 1, left + width - 1)
	window.attroff(curses.A_REVERSE)
	
def draw_help_dialog(window):
	draw_dialog_bg(window, 2, 15, 20, 52)
	window.addstr(3, 16, "How to play".center(50))
	for i in range(0, len(help_text)):
		window.addstr(5 + i, 16, help_text[i], curses.A_REVERSE)
	window.addstr(20, 16, "Press any key".center(50))

def draw_help_bar(window, message):
	window.addstr(scrheight - 1, 0,
		message.ljust(scrwidth - 1), curses.A_REVERSE)

def prompt(window, message):
	draw_dialog_bg(window, 10, 15, 4, 52)
	window.addstr(11, 16, message[0:50].center(50), curses.A_REVERSE)
	window.addstr(12, 16, " " * 50)
	curses.echo()
	answer = window.getstr(12, 16, 50)
	curses.noecho()
	return answer.decode()

def menu(window, message, message2, options):
	draw_dialog_bg(window, 2, 15, 20, 52)
	window.addstr(3, 16, message.center(50), curses.A_REVERSE)
	window.addstr(4, 16, message2.center(50), curses.A_REVERSE)
	draw_help_bar(window,
		"Arrows or jk to select, space or Enter to confirm, "
		"Esc or q to cancel.")
	
	selected = 0
	
	while True:
		for i, o in enumerate(options):
			if i == selected:
				window.addstr(i + 6, 16, o.center(50))
			else:
				window.addstr(i + 6, 16,
					o.center(50),
					curses.A_REVERSE)
		key = window.getch()
		if key == curses.KEY_DOWN or key == ord('j'):
			selected += 1
			if selected > len(options) - 1:
				selected = 0
		elif key == curses.KEY_UP or key == ord('k'):
			selected -= 1
			if selected < 0:
				selected = len(options) - 1
		elif key == 10 or key == ord(' ') or key == curses.KEY_ENTER:
			if len(options) > 0:
				return options[selected]
			else:
				return None
		elif key == 27 or key == ord('q'):
			return None
		else:
			curses.flash()

def next_coords(x, y, direction):
	if direction == 'n':
		return x, y - 1
	elif direction == 's':
		return x, y + 1
	elif direction == 'e':
		return x + 1, y
	elif direction ==  'w':
		return x - 1, y
	else:
		raise ValueError("Invalid compass direction " + str(direction))

def roll_dice(count, sides):
	roll = 0
	for i in range(count):
		roll += random.randint(1, sides)
	return roll

def count_items(items):
	group = {}
	for i in items:
		if i.name in group:
			group[i.name] += 1
		else:
			group[i.name] = 1
	return group

class TitleScreen:
	def __init__(self, stdscr):
		self.stdscr = stdscr
	
	def render(self):
		self.stdscr.clear()

		self.stdscr.addstr(0, 0,
			"Tomb of the Snake".ljust(scrwidth), curses.A_REVERSE)

		for i in range(4):
			self.stdscr.addstr(
				2 + i * 2, 0, blurb[i].center(scrwidth))
		
		if curses.has_colors():
			attr = curses.color_pair(RED) | curses.A_BOLD
		else:
			attr = 0
		for i in range(4):
			self.stdscr.addstr(10 + i, 0,
				title_banner[i].center(scrwidth), attr)
		self.stdscr.addstr(17, 0,
			"No Time To Play, 2021".center(scrwidth))
		if curses.has_colors():
			attr = curses.color_pair(BLUE)
		else:
			attr = 0
		self.stdscr.addstr(19, 0,
			"http://notimetoplay.org/".center(scrwidth), attr)

		draw_help_bar(self.stdscr,
			"[N]ew game [C]ontinue game [H]ow to play [Q]uit")
		
		self.stdscr.refresh()
			
	def handle_key(self, key):
		global finished, world, log, active_screen
		
		if key == ord('q') or key == 27:
			finished = True
		elif key == ord('n'):
			log = GameLog()
			world = GameWorld()
			world.player.log = log
			
			name = prompt(self.stdscr, "What's your name, hero?")
			world.player.name = name[0:50]

			active_screen = game_screen
		elif key == ord('c'):
			if world == None:
				world = GameWorld()
				log = GameLog()
				world.player.log = log
			
				name = prompt(
					self.stdscr, "What's your name, hero?")
				world.player.name = name[0:50]
			active_screen = game_screen
		elif key == ord('h'):
			draw_help_dialog(self.stdscr)
			self.stdscr.getch()
		else:
			curses.flash()

class CharacterScreen:
	def __init__(self, stdscr):
		self.stdscr = stdscr

	def render(self):
		self.stdscr.clear()

		self.stdscr.addstr(0, 0,
			"Character sheet".ljust(scrwidth), curses.A_REVERSE)

		p = world.player
		self.stdscr.addstr(2, 2, p.name)
		self.stdscr.hline(3, 2, curses.ACS_HLINE, len(p.name))
		self.stdscr.addstr(5, 2,  "Muscle:  %2d" % (p.muscle,))
		self.stdscr.addstr(7, 2,  "Stamina: %2d" % (p.stamina,))
		self.stdscr.addstr(9, 2,  "Agility: %2d" % (p.agility,))
		self.stdscr.addstr(11, 2, "Speed:   %2d" % (p.speed,))
		self.stdscr.addstr(13, 2, "Senses:  %2d" % (p.senses,))
		
		self.stdscr.vline(5, 39, curses.ACS_VLINE, 9)

		self.stdscr.addstr(5, 41,
			"Dice size: %d" % (p.dice_size,))
		self.stdscr.addstr(7, 41,
			"Life: %d of %d" % (p.life, p.stamina * p.dice_size))
		ap = p.speed * p.dice_size
		self.stdscr.addstr(9, 41,
			"Action points: %d of %d" % (p.action_points, ap))
		self.stdscr.addstr(11, 41,
			"Experience: %d" % (p.experience,))
		effects = " / ".join(
			[fxdescs[i] for i in p.effects.keys()])		
		self.stdscr.addstr(13, 41, effects)
			
		self.stdscr.hline(15, 2, curses.ACS_HLINE, 76)
		
		self.stdscr.addstr(17, 2, "Weapon: " + str(p.weapon))
		self.stdscr.addstr(19, 2, "Ammo: " + str(p.ammo))
		self.stdscr.addstr(17, 41, "Shield: " + str(p.shield))
		self.stdscr.addstr(19, 41, "Armor: " + str(p.armor))

		draw_help_bar(self.stdscr, "[I]nventory [B]ack to game")
		
		self.stdscr.refresh()
			
	def handle_key(self, key):
		global active_screen
		
		if key == ord('b') or key == 27:
			active_screen = game_screen
		if key == ord('i'):
			active_screen = inventory_screen
		else:
			curses.flash()

class InventoryScreen:
	def __init__(self, stdscr):
		self.stdscr = stdscr

	def render(self):
		self.stdscr.clear()

		self.stdscr.addstr(0, 0,
			"Inventory".ljust(scrwidth), curses.A_REVERSE)

		group = count_items(world.player.content)
		
		row = 2
		for i in group.keys():
			self.stdscr.addstr(
				row, 1, "%2dx %-40s" % (group[i], i))
			row += 1

		draw_help_bar(self.stdscr, "[C]haracter sheet [B]ack to game")
			
	def handle_key(self, key):
		global active_screen
		
		if key == ord('b') or key == 27:
			active_screen = game_screen
		if key == ord('c'):
			active_screen = character_screen
		else:
			curses.flash()

class GameScreen:
	def __init__(self, stdscr):
		self.stdscr = stdscr
		self.target_mode = False
		self.look_mode = False
		self.target_squares = []
		self.terrain = None
	
	def render(self):
		self.render_status(world.player)
		self.render_map(world.player.level)
		if world.player.is_dead:
			for i in range(5):
				self.stdscr.addstr(9 + i, 0,
					death_banner[i].center(80))
		self.render_log()
		if world.player.is_dead:
			draw_help_bar(self.stdscr, "? @ [I]nventory [M]enu")
		else:
			draw_help_bar(self.stdscr,
				"? @ [I]nventory [G]et [D]rop [E]at "
				"[W]ield [F]ire e[X]amine [T]unnel [M]enu")
		
		self.stdscr.refresh()
		
	def render_status(self, player):
		l = player.level
		status = "Level: %d Life:%3d AP:%3d" % (
			l.depth,
			player.life,
			player.action_points)
		status = status.rjust(scrwidth)
		self.stdscr.addstr(0, 0, status, curses.A_REVERSE)
		self.stdscr.addstr(0, 0, l.name, curses.A_REVERSE)
		
	def render_map(self, level):
		lmap = level.level_map
		self.terrain = world.player.level.terrain
		
		x1, y1, x2, y2 = self.update_seen()
		
		self.stdscr.attron(curses.A_DIM)
		for y in range(level.height):
			for x in range(level.width):
				if level.seen[y][x]:
					self.render_tile(x, y + 1, lmap[y][x])
				else:
					self.stdscr.addch(y + 1, x, " ")
		self.stdscr.attroff(curses.A_DIM)
		
		for y in range(y1, y2 + 1):
			for x in range(x1, x2 + 1):
				self.render_tile(x, y + 1, lmap[y][x])
		
		self.target_squares = []
		self.render_things(level, Item)
		self.render_things(level, Creature)
	
	def render_tile(self, x, y, char):
		if curses.has_colors():
			self.stdscr.attron(
				curses.color_pair(
					self.terrain[char]["color"]))
		self.stdscr.addch(y, x, char)
		if curses.has_colors():
			self.stdscr.attroff(
				curses.color_pair(
					self.terrain[char]["color"]))
	
	def render_things(self, level, thing_class):
		self.stdscr.attron(curses.A_BOLD)
		for i in level.content:
			if not isinstance(i, thing_class):
				continue
			elif not world.player.can_see(i.x, i.y):
				continue
			
			char = i.char
			if i != world.player:
				square = (i.x, i.y)
				if square not in self.target_squares:
					self.target_squares.append(square)
				target_num = self.target_squares.index(square)
				if self.target_mode or self.look_mode:
					if target_num < 10:
						char = str(target_num)
				
			if curses.has_colors():
				self.stdscr.attron(
					curses.color_pair(i.color))
			self.stdscr.addch(i.y + 1, i.x, char)
			if curses.has_colors():
				self.stdscr.attroff(
					curses.color_pair(i.color))
		self.stdscr.attroff(curses.A_BOLD)
	
	def update_seen(self):
		p = world.player
		radius = p.sight_radius
		x1 = max(0, p.x - radius)
		y1 = max(0, p.y - radius)
		x2 = min(p.level.width - 1, p.x + radius)
		y2 = min(p.level.height - 1, p.y + radius)
		
		for y in range(y1, y2 + 1):
			for x in range(x1, x2 + 1):
				p.level.seen[y][x] = True
		
		return x1, y1, x2, y2
			
	def render_log(self):
		num_msg = scrheight - 22
		tail = log.messages[-num_msg:]
		for i, t in enumerate(tail):
			self.stdscr.addstr(21 + i, 0, t.ljust(scrwidth))
		
	def play_ending(self):
		self.stdscr.timeout(2400)
		for i, e in enumerate(ending_text):
			self.stdscr.addstr(3 + i * 2, 4, e, curses.A_BOLD)
			self.stdscr.getch()
		draw_help_bar(self.stdscr, "Press any key")
		self.stdscr.timeout(-1)
		self.stdscr.getch()

	def handle_key(self, key):
		global active_screen
		
		if key == ord('m') or key == 27:
			self.look_mode = False
			self.target_mode = False
			active_screen = title_screen
		elif key == ord('@'):
			self.look_mode = False
			self.target_mode = False
			active_screen = character_screen
		elif key == ord('i'):
			self.look_mode = False
			self.target_mode = False
			active_screen = inventory_screen
		elif key == ord('?'):
			self.look_mode = False
			self.target_mode = False
			draw_help_dialog(self.stdscr)
			self.stdscr.getch()
		elif world.player.is_dead:
			curses.flash()
			return
		
		if world.player.action_points <= 0:
			world.update()
		
		if key in dirkeys:
			self.look_mode = False
			self.target_mode = False
			if world.player.walk(dirkeys[key]):
				world.update()
		elif key == ord('>'):
			self.look_mode = False
			self.target_mode = False
			p = world.player
			xp = p.experience
			p.descend()
			if p.experience > xp:
				# PC just leveled up.
				self.render()
				trait = menu(self.stdscr, "Ding! New level!",
					"Select trait to upgrade:",
					["muscle", "stamina", "agility",
						"speed", "senses"])
				if trait != None:
					p.__dict__[trait] += 1
					p.log("You feel %s." % (
						traitadjs[trait],))
				else:
					p.wounds = 0
					p.log("Your wounds heal.")
		elif key == ord('<'):
			self.look_mode = False
			self.target_mode = False
			p = world.player
			p.ascend()
			if p.level == world.level0:
				if world.mcguffin in p.content:
					self.render()
					self.play_ending()
					p.level.level_map[p.y][p.x] = ord('=')
		elif key == ord('g') or key == ord(','):
			self.look_mode = False
			self.target_mode = False
			world.player.get_item_here()
			world.update()
		elif key == ord('d'):
			self.look_mode = False
			self.target_mode = False
			itlist = count_items(world.player.content).keys()
			it = menu(self.stdscr, "Drop what?", "", itlist)
			if it != None:
				world.player.drop(it)
				world.update()
			else:
				world.player.log("Canceled.")
			world.update()
		elif key == ord('e'):
			self.look_mode = False
			self.target_mode = False
			foods = [i for i in world.player.content if i.food > 0]
			itlist = list(count_items(foods).keys())
			it = menu(self.stdscr, "Eat what?", "", itlist)
			if it != None:
				world.player.eat(it)
				world.update()
			else:
				world.player.log("Canceled.")
			world.update()
		elif key == ord('w'):
			self.look_mode = False
			self.target_mode = False
			if self.wield_or_wear():
				world.update()
		elif key == ord('f'):
			self.look_mode = False
			self.target_mode = not self.target_mode
		elif key == ord('x'):
			self.target_mode = False
			self.look_mode = not self.look_mode
		elif ord('0') <= key <= ord('9'):
			tnum = key - ord('0')
			if not self.target_mode and not self.look_mode:
				world.player.log("But you're not aiming!")
			elif tnum >= len(self.target_squares):
				world.player.log("Not enough targets.")
			elif self.target_mode:
				x, y = self.target_squares[tnum]
				if world.player.fire_at(x, y):
					world.update()
				self.target_mode = False
			elif self.look_mode:
				x, y = self.target_squares[tnum]
				world.player.look(x, y)
				self.look_mode = False
		elif key == ord('t'):
			self.look_mode = False
			self.target_mode = False
			draw_help_bar(self.stdscr, "Tunnel which way?")
			direction = self.stdscr.getch()
			if direction in dirkeys:
				world.player.tunnel(dirkeys[direction])
				world.update()
			else:
				world.player.log("Canceled.")
		elif key == ord('r'):
			self.look_mode = False
			self.target_mode = False
			draw_help_bar(self.stdscr, "Run which way?")
			direction = self.stdscr.getch()
			if direction in dirkeys:
				self.stdscr.timeout(500)
				d = dirkeys[direction]
				while world.player.is_way_clear(d):
					world.player.walk(d)
					world.update()
					self.render()
					if self.stdscr.getch() > -1:
						break
				self.stdscr.timeout(-1)
			else:
				world.player.log("Canceled.")
		elif key == ord('.'):
			self.look_mode = False
			self.target_mode = False
			world.player.log("You stand still.")
			world.update()
		elif key == curses.KEY_MOUSE:
			self.look_mode = False
			self.target_mode = False
			device, x, y, z, button = curses.getmouse()
			if button == curses.BUTTON1_CLICKED:
				world.player.look(x, y - 1)
			elif button == curses.BUTTON1_DOUBLE_CLICKED:
				if world.player.fire_at(x, y - 1):
					world.update()
		else:
			curses.flash()
	
	def wield_or_wear(self):
		slot = menu(self.stdscr,
			"Wield/wear equipment", "Choose slot:",
			["weapon", "ammo", "shield", "armor"])
		if slot == None:
			world.player.log("Canceled.")
			return False
		elif slot == "weapon":
			attr = "attack"
		elif slot == "ammo":
			attr = "ammo"
		elif slot == "shield":
			attr = "defense"
		elif slot == "armor":
			attr = "armor"
		
		equipment = self.select_equipment(slot, attr)
		if equipment == None:
			world.player.log("Canceled.")
			return False
		elif equipment == "none":
			equipment = None
			
		world.player.equip(equipment, slot)
		return True
	
	def select_equipment(self, slot, attr):
		shortlist = [i for i in world.player.content
			if i.__dict__[attr] > 0]
		shortlist = ["none"] + list(count_items(shortlist).keys())
		return menu(self.stdscr, "Choose " + slot, "", shortlist)

class GameLog:
	def __init__(self):
		self.messages = []
	
	def say(self, message):
		message = message[0].capitalize() + message[1:]
		self.messages.append(message)
		while len(self.messages) > 100:
			del self.messages[0]

	def call_attack(self, attacker, defender, damage):
		name1 = attacker.the_name()
		name2 = defender.the_name()
		if damage <= 0:
			self.say("%s pokes %s harmlessly." % (name1, name2))
		elif damage > defender.life:
			msg = "%s strikes %s for %d damage, killing %s!"
			msg  = msg % (name1, name2, damage, defender.pronoun)
			self.say(msg)
		else:
			msg = "%s strikes %s for %d damage."
			msg  = msg % (name1, name2, damage)
			self.say(msg)

	def __call__(self, message):
		self.say(message)

class GameWorld:
	def __init__(self):
		self.animal_ai = AnimalAI()
		self.undead_ai = UndeadAI()

		self.mcguffin = Item(items["idol"])
		self.player = Creature(creatures["human"])
		self.player.pronoun = "them"
		self.player.proper_name = True
		self.level0 = DungeonLevel(0, levels[0])
		self.level0.populate()
		
		self.player.move_to(0, 10, self.level0)
	
	def update(self):
		stuff = self.player.level.content
		for i in stuff.copy():
			if not isinstance(i, Creature):
				continue
			self.update_effects(i)
			if i.is_dead:
				drop = random.choice(i.drops)
				if drop != None:
					drop = Item(drop)
					drop.move_to(i.x, i.y, i.level)
				i.move_to(-1, -1, None)
			elif i.action_points <= 0:
				i.action_points += i.speed * i.dice_size
			elif i.mind == "animal":
				self.animal_ai.take_turn(i)
			elif i.mind == "undead":
				self.undead_ai.take_turn(i)
		self.update_torch(self.player, "weapon")
		self.update_torch(self.player, "shield")
	
	def update_torch(self, player, slot):
		item = player.__dict__[slot]
		if item != None and item.light > 0:
			item.light -= 1
			if item.light == 0:
				player.__dict__[slot] = None
				player.log("Your torch burns out.")
	
	def update_effects(self, creature):
		for i in creature.effects.copy().keys():
			effect = creature.effects[i]
			if effect["turns"] < 1:
				del creature.effects[i]
			else:
				effect["turns"] -= 1
				creature.wounds += roll_dice(
					1, effect["damage"])

class DungeonLevel:
	width = 80
	height = 20
	
	def __init__(self, depth, template):
		self.depth = depth

		self.name = template["name"]
		self.terrain = template["terrain"]
		self.items = template["items"]
		self.creatures = template["creatures"]
		#self.light_level = 1

		self.level_map = [bytearray(self.width)
			for i in range(self.height)]
		self.seen = [[False] * self.width for i in range(self.height)]
		self.prev_level = None
		self.next_level = None
		
		self.content = set()
	
	def is_on_grid(self, x, y):
		return x >= 0 and y >= 0 and x < self.width and y < self.height
	
	def terrain_at(self, x, y):
		tile = self.level_map[y][x]
		if tile in self.terrain.keys():
			return self.terrain[tile]
		else:
			return None
	
	def items_at(self, x, y):
		return (i for i in self.content
			if isinstance(i, Item) and i.x == x and i.y == y)
	
	def creature_at(self, x, y):
		for i in self.content:
			if isinstance(i, Creature) and i.x == x and i.y == y:
				return i
		return None
	
	def dig_next(self):
		if self.next_level != None:
			return
		
		self.next_level = DungeonLevel(
			self.depth + 1, levels[self.depth + 1])
		self.next_level.prev_level = self
		self.next_level.populate()
	
	def populate(self):
		if self.terrain == terrain["village"]:
			gen = VillageLevelGenerator(worldrng)
		elif self.terrain == terrain["cave"]:
			gen = CaveLevelGenerator(worldrng)
		elif self.terrain == terrain["tomb"]:
			gen = TombLevelGenerator(worldrng)
		else:
			raise RuntimeError("Unknown terrain type")
		
		gen.populate(self)

class Thing:
	def __init__(self):
		self.level = None
		self.x = -1
		self.y = -1
		
		self.name = "thing"
		self.pronoun = "it"
		self.proper_name = False
		self.special = None
	
	def move_to(self, x, y, parent = None):
		l = self.level
		
		if hasattr(l, "content") and self in l.content:
			l.content.remove(self)
		if hasattr(parent, "content"):
			parent.content.add(self)
			self.level = parent
		
		if not isinstance(parent, DungeonLevel): return

		if self.level.is_on_grid(x, y):
			self.x = x
			self.y = y
		else:
			raise ValueError("Can't move outside the level map.")

	def the_name(self):
		if self.proper_name:
			return self.name
		else:
			return "the " + self.name
	
	def a_name(self):
		if self.name[0] in ('a', 'e', 'i', 'o', 'u'):
			return "an " + self.name
		else:
			return "a " + self.name

	def __str__(self):
		return self.a_name()

class Item(Thing):
	def __init__(self, template):
		Thing.__init__(self)
		self.char = "?"
		
		self.weight = 0
		self.food = 0
		self.attack = 0
		self.defense = 0
		self.damage = 0
		self.ammo = 0
		self.armor = 0
		self.light = 0
		
		for attr in template.keys():
			self.__dict__[attr] = template[attr]

class Creature(Thing):
	def __init__(self, template):
		Thing.__init__(self)
		self.log = None

		self.char = "@"
		
		self.dice_size = 8
		self.muscle = 3
		self.agility = 3
		self.stamina = 3
		self.speed = 3
		self.senses = 3
		
		self.can_swim = False
		self.can_fly = False
		self.mind = None
		
		self.weapon = None
		self.shield = None
		self.armor = None
		self.ammo = None
		self.special = None
		
		self.drops = [None]
		
		for attr in template.keys():
			self.__dict__[attr] = template[attr]
		self.template = template
		
		self.wounds = 0
		self.experience = 0
		self.action_points = self.dice_size * self.speed
		
		self.content = set()
		self.effects = {}
		
		self.last_heading = None # For the AI system.

	def can_see(self, x, y):
		radius = self.sight_radius
		can_see_x = abs(self.x - x) <= radius
		can_see_y = abs(self.y - y) <= radius
		return can_see_x and can_see_y
		
	def can_enter(self, x, y):
		tile = chr(self.level.level_map[y][x])
		if tile in ('.', ',', '=', '>', '<'):
			return True
		elif tile == '~':
			return self.can_swim or self.can_fly
		else:
			return False

	def is_way_clear(self, direction):
		new_x, new_y = next_coords(self.x, self.y, direction)

		if not self.level.is_on_grid(new_x, new_y):
			return False
		elif self.level.creature_at(new_x, new_y) != None:
			return False
		elif not self.can_enter(new_x, new_y):
			return False
		else:
			return True

	def walk(self, direction):
		assert self.level != None, "Creature is out of play."
		new_x, new_y = next_coords(self.x, self.y, direction)

		if not self.level.is_on_grid(new_x, new_y):
			self.log("An invisible barrier stops you.")
			return False
		elif self.level.creature_at(new_x, new_y) != None:
			creature = self.level.creature_at(new_x, new_y)
			if self.is_enemy(creature):
				self.attack(creature)
			#else:
			#	self.say("You bump into " + creature.name)
			return True
		elif not self.can_enter(new_x, new_y):
			t = self.level.terrain_at(new_x, new_y)
			if t != None and t["name"] != None:
				self.log("The way is barred by "
					+ t["name"] + ".")
			else:
				self.log("The way is barred.")
			return False
		else:
			self.x = new_x
			self.y = new_y
			self.action_points -= 5
			return True

	def tunnel(self, direction):
		assert self.level != None, "Creature is out of play."
		new_x, new_y = next_coords(self.x, self.y, direction)
	
		if not self.level.is_on_grid(new_x, new_y):
			self.log("There's nothing in that direction.")
			return False
		elif self.level.level_map[new_y][new_x] != ord('#'):
			self.log("There's no wall in that direction.")
			return False
		
		for i in self.content.copy():
			if i.name == "pickaxe":
				self.level.level_map[new_y][new_x] = ord('.')
				self.content.remove(i)
				self.log("You break down the wall. "
					"The pickaxe falls apart.")
				self.action_points -= 20
				return True
		
		if self.weapon != None and self.weapon.name == "pickaxe":
				self.level.level_map[new_y][new_x] = ord('.')
				self.weapon = None
				self.log("You break down the wall. "
					"The pickaxe falls apart.")
				self.action_points -= 20
				return True

		self.log("You need a pickaxe for that.")
		return False
				
	def is_enemy(self, creature):
		return self.template != creature.template
	
	def attack(self, creature):
		if self.distance_to(creature) > 1:
			self.log(creature.the_name() +
				" is too far away for melee.")
			return False
		
		attack = roll_dice(self.agility, self.dice_size)
		if self.weapon != None:
			attack += self.weapon.attack
			if self.weapon.special != None:
				creature.apply_effect(self.weapon.special)
		elif self.special != None:
			creature.apply_effect(self.special)
		defense = roll_dice(creature.speed, creature.dice_size)
		if creature.shield != None:
			defense += creature.shield.defense
		
		if attack > defense:
			dmg_bonus = (self.muscle - 3) * self.dice_size // 2
			damage = (attack - defense) // 2 + dmg_bonus
			if self.weapon != None:
				damage += self.weapon.damage
			damage = creature.apply_armor(damage)
			if damage > 0:
				creature.wounds += damage
			self.log.call_attack(self, creature, damage)
		else:
			self.log("%s misses %s." %
				(self.the_name(), creature.the_name()))

		if self.weapon != None:
			self.action_points -= 4 + self.weapon.weight
		else:
			self.action_points -= 5
			
		return True
	
	def fire_at(self, x, y):
		if not self.can_see(x, y):
			self.log("You can't shoot what you can't see.")
			return False
		creature = self.level.creature_at(x, y)
		if creature == None:
			self.log("Nobody there for you to shoot.")
			return False
		distance = self.distance_to(creature)
		if distance < 2:
			self.log("You're too close to use your sling.")
			return False
		if self.ammo == None:
			self.log("You need ammo for your sling.")
			return False
		
		self.action_points -= 4 + self.ammo.weight
		
		attack = roll_dice(self.agility, self.dice_size)
		attack += self.ammo.ammo + self.senses - distance
		defense = roll_dice(creature.speed, creature.dice_size)
		if creature.shield != None:
			defense += creature.shield.defense

		if attack > defense:
			dmg_bonus = (self.muscle - 3) * self.dice_size // 2
			damage = (attack - defense) // 2 + dmg_bonus
			damage += self.ammo.damage
			damage = creature.apply_armor(damage)
			if damage > 0:
				creature.wounds += damage
				self.log("%s shoots %s for %d damage!" %
					(self.the_name(), creature.the_name(), damage))
			else:
				self.log("%s hits %s harmlessly." %
					(self.the_name(), creature.the_name()))
		else:
			self.ammo.move_to(x, y, self.level)
			self.log("%s's shot misses %s." %
				(self.the_name(), creature.the_name()))
		
		for item in self.content.copy():
			if item.name == self.ammo.name:
				self.ammo = item
				self.content.remove(item)
				return True
				
		self.ammo = None
		return True
	
	def distance_to(self, thing):
		if self.level != thing.level:
			return 2 * 1000 * 1000 * 1000
		else:
			return abs(self.x - thing.x) + abs(self.y - thing.y)
	
	def apply_armor(self, damage):
		if self.armor == None:
			return damage
		else:
			damage -= self.armor.armor
			if damage <= 0:
				return damage
			half = damage // 2
			self.armor.weight -= half
			if self.armor.weight < 0:
				self.log("Your armor falls apart.")
				self.armor = None
			return damage - half # In case damage was odd.
	
	def apply_effect(self, effect):
		kind, duration, damage = effect
		turns = roll_dice(1, duration)
		if kind in self.effects:
			self.effects[kind]["turns"] = max(
				turns, self.effects[kind]["turns"])
			self.effects[kind]["damage"] = max(
				damage, self.effects[kind]["damage"])
		else:
			self.effects[kind] = {"turns": turns, "damage": damage}
		self.log("%s is %s!" % (self.the_name(), fxdescs[kind]))
	
	@property
	def is_dead(self):
		return self.wounds >= self.dice_size * self.stamina
	
	@property
	def life(self):
		return int(self.dice_size * self.stamina - self.wounds)
	
	@property
	def sight_radius(self):
		w = self.weapon
		s = self.shield
		
		if self.level.depth == 0:
			modifier = 2
		else:
			modifier = 0
		if w != None and w.light > 0 and modifier < 2:
			modifier += 1
		if s != None and s.light > 0 and modifier < 2:
			modifier += 1
		radius = self.senses * modifier
		if radius < 1:
			radius = self.senses // 2
		return radius
	
	def descend(self):
		tile = chr(self.level.level_map[self.y][self.x])
		if tile == '>':
			if self.level.next_level == None:
				self.level.dig_next()
			self.move_to(self.x, self.y, self.level.next_level)
			if self.level.depth > self.experience:
				self.experience = self.level.depth
				if self.wounds > 0:
					self.wounds -= 1
			self.action_points -= 5
		else:
			self.log("There's no way down from here.")
		
	def ascend(self):
		tile = self.level.level_map[self.y][self.x]
		if tile == ord('<'):
			self.move_to(self.x, self.y, self.level.prev_level)
			self.action_points -= 5
		else:
			self.log("There's no way up from here.")
	
	def look(self, x, y):
		if not self.level.is_on_grid(x, y):
			return
		elif not self.can_see(x, y):
			self.log("You can't see that far right now.")
			return
		
		tile = self.level.level_map[y][x]
		msg = "You see: " + self.level.terrain[tile]["name"]
		for i in self.level.content:
			if i.x == x and i.y == y:
				if i == self:
					msg += ", yourself"
				else:
					msg += ", " + i.name
		self.log(msg)
	
	def get_item_here(self):
		item = next(self.level.items_at(self.x, self.y), None)
		if item == None:
			self.log("There's nothing portable here.")
			return
		
		item.move_to(-1, -1, self)
		self.log("You pick up the " + item.name)
		self.action_points -= 5
	
	def drop(self, name):
		for item in self.content.copy():
			if item.name == name:
				item.move_to(self.x, self.y, self.level)
				self.log("Dropped.")
				self.action_points -= 5
				return
		self.log("But you don't have a %s!" % (name,))
	
	def eat(self, name):
		for item in self.content.copy():
			if item.name == name:
				if item.food < 1:
					self.log("That's not edible.")
					return
				elif self.wounds < 1:
					self.log("But you're feeling full!")
					return
					
				self.content.remove(item)
				self.wounds -= item.food
				if self.wounds < 0:
					self.wounds = 0
				self.log("You eat the %s." % (item.name,))
				self.action_points -= 10
				return
		self.log("But you don't have a %s!" % (name,))
	
	def equip(self, name, slot):
		if name != None:
			for item in self.content.copy():
				if item.name == name:
					prev_gear = self.__dict__[slot]
					self.__dict__[slot] = item
					self.content.remove(item)
					if prev_gear != None:
						self.content.add(prev_gear)
					self.log(slot + " changed.")
					self.action_points -= 10
					return
			self.log("But you don't have a %s!" % (name,))
		else:
			if self.__dict__[slot] != None:
				self.content.add(self.__dict__[slot])
				self.__dict__[slot] = None
			self.action_points -= 5
	

class VillageLevelGenerator:
	def __init__(self, rng):
		self.rng = rng
		self.road_coords = []
		self.last_building = None
	
	def populate(self, level):
		for y in range(level.height):
			for x in range(level.width):
				level.level_map[y][x] = ord('.')
				
		self.make_trees(level)
		self.make_road(level)
		self.make_river(level)
		self.place_buildings(level)

		for y in range(level.height):
			for x in range(level.width):
				self.maybe_place_item(level, x, y)

		for y in range(level.height):
			for x in range(level.width):
				self.maybe_place_creature(level, x, y)
				
	def make_trees(self, level):
		size = level.width * level.height
		
		for i in range(size // 5):
			position = self.rng.randint(0, size - 1)
			x = position % level.width
			y = position // level.width
			if (position % 2 == 0):
				level.level_map[y][x] = ord('|')
			else:
				level.level_map[y][x] = ord('^')
				
	def make_road(self, level):
		y = level.height // 2 - 1
		for x in range(level.width):
			level.level_map[y][x] = ord(',')
			level.level_map[y + 1][x] = ord(',')
			self.road_coords.append(y)
			if (y <= 3):
				y += self.rng.choice([0, 0, 0, 0, 0, 0, 1])
			elif (y >= level.height - 3):
				y += self.rng.choice([-1, 0, 0, 0, 0, 0, 0])
			else:
				y += self.rng.choice([-1, 0, 0, 0, 0, 0, 1])
				
	def make_river(self, level):
		x = int(level.width * 0.4)
		for y in range(level.height):
			if level.level_map[y][x] == ord(','):
				level.level_map[y][x] = ord('=')
			else:
				level.level_map[y][x] = ord('~')
			delta_x = self.rng.randint(0, 2)
			if delta_x == 2: delta_x = 1
			x += delta_x
			
	def place_buildings(self, level):
		w = level.width
		# West of the river.
		self.place_building_cluster(level, int(w * 0.1), int(w * 0.4))
		# East of the river.
		self.place_building_cluster(level, int(w * 0.6), int(w * 0.9))
		
		x, y = self.last_building
		level.level_map[y][x] = ord('>')
		level.exit_down = self.last_building
		
	def place_building_cluster(self, level, from_x, to_x):
		h = level.height
		x = from_x
		while x < to_x:
			radius = self.rng.randint(1, 2)
			x += radius
			road_y = self.road_coords[x]
			if road_y < h // 2:
				y = road_y + int(h * 0.4)
			else:
				y = road_y - int(h * 0.4)
			self.make_building(level, x, y, radius)
			x += radius + 4
	
	def make_building(self, level, x, y, radius):
		for i in range(x - radius, x + radius + 1):
			for j in range(y - radius, y + radius + 1):
				level.level_map[j][i] = ord('#')
		# Make a door and clear the space in front of it.
		if y < level.height // 2:
			level.level_map[y + radius][x] = ord('.')
			level.level_map[y + radius + 1][x] = ord('.')
			level.level_map[y + radius + 1][x - 1] = ord('.')
			level.level_map[y + radius + 1][x + 1] = ord('.')
		else:
			level.level_map[y - radius][x] = ord('.')
			level.level_map[y - radius - 1][x] = ord('.')
			level.level_map[y - radius - 1][x - 1] = ord('.')
			level.level_map[y - radius - 1][x + 1] = ord('.')
		
		self.last_building = (x, y)
		
		radius -= 1;
		for i in range(x - radius, x + radius + 1):
			for j in range(y - radius, y + radius + 1):
				level.level_map[j][i] = ord('=')
	
	def maybe_place_item(self, level, x, y):
		tile = chr(level.level_map[y][x])
		if tile != '.' and tile != '=':
			return
		if self.rng.random() > 0.01:
			return
		
		item = Item(random.choice(level.items))
		item.move_to(x, y, level)
	
	def maybe_place_creature(self, level, x, y):
		tile = chr(level.level_map[y][x])
		if tile != '.' and tile != '=':
			return
		if self.rng.random() > 0.01:
			return
		
		creature = Creature(random.choice(level.creatures))
		creature.log = log
		creature.move_to(x, y, level)

class CaveLevelGenerator:
	def __init__(self, rng):
		self.rng = rng
		self.entrances = []
		self.far_cell = None
	
	def populate(self, level):
		for x in range (level.width):
			for y in range(level.height):
				level.level_map[y][x] = ord('#')

		assert level.prev_level != None

		x, y = level.prev_level.exit_down
		self.digRoomFrom(x, y, level)
		level.level_map[y][x] = ord('<')
		if level.depth % 2 == 1:
			while self.far_cell >= (20, 10):
				x, y = self.far_cell
				self.digRoomFrom(x, y, level)
		else:
			while self.far_cell < (70, 10):
				x, y = self.far_cell
				self.digRoomFrom(x, y, level)
		x, y = self.far_cell
		level.level_map[y][x] = ord('>')
		level.exit_down = self.far_cell

		self.makeWater(level)

		for y in range(level.height):
			for x in range(level.width):
				self.maybe_place_item(level, x, y)

		for y in range(level.height):
			for x in range(level.width):
				self.maybe_place_creature(level, x, y)
	
	def digRoomFrom(self, x, y, level):
		halfw = level.width // 2
		halfh = level.height // 2
		
		if level.depth % 2 == 1:
			directions = [(-1, 0), (-1, 0), (-1, 0),
				(0, -1), (0, -1), (1, 0), (0, 1)]
		else:
			directions = [(-1, 0), (0, -1),
				(1, 0), (1, 0), (1, 0), (0, 1), (0, 1)]
		self.far_cell = (x, y)
		for i in range(halfh):
			new_x = x
			new_y = y
			for j in range(halfw):
				direction = self.rng.choice(directions)
				dx, dy = direction
				new_x += dx
				new_y += dy
				if not self.advanceCorridor(
					new_x, new_y, level):
						break
	
	def advanceCorridor(self, x, y, level):
		if not(0 < x < level.width - 1 and 0 < y < level.height - 1):
			return False
		if level.level_map[y][x] == ord('#'):
			level.level_map[y][x] = ord('.')
		if level.depth % 2 == 1:
			if (x, y) < self.far_cell:
				self.far_cell = (x, y)
		else:
			if (x, y) > self.far_cell:
				self.far_cell = (x, y)
		return True
	
	def makeWater(self, level):
		min_x = 1
		min_y = 1
		max_x = level.width - 2
		max_y = level.height - 2
		
		water_tiles = []
		for y in range(min_y, max_y + 1):
			for x in range(min_x, max_x + 1):
				if level.level_map[y][x] != ord('#'):
					continue
				cnt = self.countNeighborTiles(
					x, y, level, ord('#'))
				if (cnt < 5):
					water_tiles.append((x, y))
		for i in water_tiles:
			x, y = i
			level.level_map[y][x] = ord('~')
	
	def countNeighborTiles(self, x, y, level, kind):
		count = 0
		for i in range(y - 1, y + 2):
			for j in range(x - 1, x + 2):
				if level.level_map[i][j] == kind:
					count += 1
		return count
	
	def maybe_place_item(self, level, x, y):
		tile = chr(level.level_map[y][x])
		if tile != '.':
			return
		if self.rng.random() > 0.02:
			return
		
		item = Item(random.choice(level.items))
		item.move_to(x, y, level)
	
	def maybe_place_creature(self, level, x, y):
		tile = chr(level.level_map[y][x])
		if tile != '.':
			return
		if self.rng.random() > 0.01:
			return
		
		creature = Creature(random.choice(level.creatures))
		creature.log = log
		creature.move_to(x, y, level)

class TombLevelGenerator:
	def __init__(self, rng):
		self.rng = rng
		self.cellw = -1
		self.cellh = -1
		self.level = None
	
	def populate(self, level):
		for x in range(level.width):
			for y in range(level.height):
				level.level_map[y][x] = ord('.')
		for x in range (level.width):
			level.level_map[0][x] = ord('#')
			level.level_map[level.height - 1][x] = ord('#')
		for y in range(level.height):
			level.level_map[y][0] = ord('#')
			level.level_map[y][level.width - 1] = ord('#')
		
		self.cellw, self.cellh = levels[level.depth]["cellsize"]
		self.level = level
		self.subdivideWide(1, 1, level.width - 2, level.height - 2)

		x, y = level.prev_level.exit_down
		level.level_map[y][x] = ord('<')
		if level.depth % 2 == 1:
			if level.depth < len(levels) - 1:
				level.level_map[1][1] = ord('>')
				level.exit_down = (1, 1)
			else:
				idol = Item(items["idol"])
				idol.move_to(1, 1, level)
		elif level.depth < len(levels) - 1:
			lh2 = level.height - 2
			lw2 = level.width - 2
			level.level_map[lh2][lw2] = ord('>')
			level.exit_down = (lw2, lh2)
		else:
			world.mcguffin.move_to(
				level.width - 2, level.height - 2, level)
			
	
	def subdivideWide(self, x1, y1, x2, y2):
		w = x2 - x1 + 1
		h = y2 - y1 + 1
		# You have to check both dimensions
		# or you'll get oddly skewed levels.
		if w < self.cellw or h < self.cellh or w == h:
			self.furnishRoom(x1, y1, x2, y2)
			return 0
		
		if w == 3:
			x = x1 + 1
		else:
			x = x1 + self.rng.randint(1, w - 2)
		for y in range(y1, y2 + 1):
			self.level.level_map[y][x] = ord('#')

		self.subdivideHigh(x1, y1, x - 1, y2)
		self.subdivideHigh(x + 1, y1, x2, y2)
		
		doory = y1 + self.rng.randint(0, h - 1)

		self.level.level_map[doory][x] = ord('.')
		# Account for walls placed deeper into recursion.
		self.level.level_map[doory][x - 1] = ord('.')
		self.level.level_map[doory][x + 1] = ord('.')
			
		return x

	def subdivideHigh(self, x1, y1, x2, y2):
		w = x2 - x1 + 1
		h = y2 - y1 + 1
		# You have to check both dimensions
		# or you'll get oddly skewed levels.
		if w < self.cellw or h < self.cellh or w == h:
			self.furnishRoom(x1, y1, x2, y2)
			return 0
		
		if h == 3:
			y = y1 + 1
		else:
			y = y1 + self.rng.randint(1, h - 2)
		for x in range(x1, x2 + 1):
			self.level.level_map[y][x] = ord('#')

		self.subdivideWide(x1, y1, x2, y - 1)
		self.subdivideWide(x1, y + 1, x2, y2)
		
		doorx = x1 + self.rng.randint(0, w - 1)

		self.level.level_map[y][doorx] = ord('.')
		# Account for walls placed deeper into recursion.
		self.level.level_map[y - 1][doorx] = ord('.')
		self.level.level_map[y + 1][doorx] = ord('.')
			
		return y

	def furnishRoom(self, x1, y1, x2, y2):
		w = x2 - x1 + 1
		h = y2 - y1 + 1
		lmap = self.level.level_map
		
		if w == 3 and h == 3:
			lmap[y1 + 1][x1 + 1] = ord('+')
		elif w == 3:
			if h % 2 == 1:
				for y in range(y1 + 1, y2, 2):
					lmap[y][x1 + 1] = ord('|')
			else:
				for y in range(y1 + 1, y2):
					if self.rng.randint(0, 1) == 0:
						lmap[y][x1 + 1] = ord('~')
		elif h == 3:
			if w % 2 == 1:
				for x in range(x1 + 1, x2, 2):
					lmap[y1 + 1][x] = ord('|')
			else:
				for x in range(x1 + 1, x2):
					if self.rng.randint(0, 1) == 0:
						lmap[y1 + 1][x] = ord('~')
		elif w == 5 and h == 5:
			lmap[y1 + 1][x1 + 1] = ord('+')
			lmap[y1 + 1][x1 + 3] = ord('+')
			lmap[y1 + 3][x1 + 1] = ord('+')
			lmap[y1 + 3][x1 + 3] = ord('+')
		elif w == 5:
			if h % 2 == 1:
				for y in range(y1 + 1, y2, 2):
					lmap[y][x1 + 1] = ord('|')
					lmap[y][x2 - 1] = ord('|')
			else:
				for y in range(y1 + 1, y2):
					if self.rng.randint(0, 1) == 0:
						lmap[y][x1 + 1] = ord('~')
					if self.rng.randint(0, 1) == 0:
						lmap[y][x2 - 1] = ord('~')
		elif h == 5:
			if w % 2 == 1:
				for x in range(x1 + 1, x2, 2):
					lmap[y1 + 1][x] = ord('|')
					lmap[y2 - 1][x] = ord('|')
			else:
				for x in range(x1 + 1, x2, 2):
					if self.rng.randint(0, 1) == 0:
						lmap[y1 + 1][x] = ord('~')
					if self.rng.randint(0, 1) == 0:
						lmap[y2 - 1][x] = ord('~')
		elif w == 7 and h == 7:
			for y in range(y1 + 1, y2, 2):
				for x in range(x1 + 1, x2, 2):
					lmap[y][x] = ord('|')
			lmap[y1 + 3][x1 + 3] = ord('+')
		elif w == 9 and h == 9:
			for y in range(y1 + 1, y2, 2):
				for x in range(x1 + 1, x2, 2):
					lmap[y][x] = ord('|')
			lmap[y1 + 3][x1 + 3] = ord('+')
			lmap[y1 + 3][x1 + 5] = ord('+')
			lmap[y1 + 5][x1 + 3] = ord('+')
			lmap[y1 + 5][x1 + 5] = ord('+')
		elif w > 5 and h > 5 and (w % 2 == 1 or h % 2 == 1):
			if h % 2 == 1:
				for y in range(y1 + 1, y2, 2):
					lmap[y][x1 + 1] = ord('#')
					lmap[y][x2 - 1] = ord('#')
			if w % 2 == 1:
				for x in range(x1 + 1, x2, 2):
					lmap[y1 + 1][x] = ord('#')
					lmap[y2 - 1][x] = ord('#')
			self.furnishRoom(x1 + 2, y1 + 2, x2 - 2, y2 - 2)
		else:
			for y in range(y1, y2 + 1):
				for x in range(x1, x2 + 1):
					if self.rng.random() > 0.01:
						continue
					item = Item(
						random.choice(
							self.level.items))
					item.move_to(x, y, self.level)

			for y in range(y1, y2 + 1):
				for x in range(x1, x2 + 1):
					if self.rng.random() >= 0.01:
						continue
					creature = Creature(
						random.choice(
							self.level.creatures))
					creature.log = log
					creature.move_to(x, y, self.level)


class AnimalAI:
	def take_turn(self, mob):
		p = world.player
		if mob.distance_to(p) == 1:
			mob.attack(p)
			return

		mob.last_heading = random.choice(['n', 's', 'e', 'w'])
		x, y = next_coords(mob.x, mob.y, mob.last_heading)
		if mob.level.is_on_grid(x, y) and mob.can_enter(x, y):
			mob.walk(mob.last_heading)

class UndeadAI:
	def take_turn(self, mob):
		p = world.player
		if mob.distance_to(p) == 1:
			mob.attack(p)
			mob.last_heading = None # To reset the chase mode.
			return
		elif mob.x == p.x:
			if mob.y < p.y:
				mob.last_heading = 's'
			elif mob.y > p.y:
				mob.last_heading = 'n'
		elif mob.y == p.y:
			if mob.x < p.x:
				mob.last_heading = 'e'
			elif mob.x > p.x:
				mob.last_heading = 'w'
		elif mob.last_heading == None:
			mob.last_heading = random.choice(
				['n', 's', 'e', 'w'])

		x, y = next_coords(mob.x, mob.y, mob.last_heading)
		if mob.level.is_on_grid(x, y) and mob.can_enter(x, y):
			mob.walk(mob.last_heading)
		else:
			mob.last_heading = None

if __name__ == "__main__":
	try:
		curses.wrapper(curses_main)
	except RuntimeError as e:
		print(e)