import random
import sys
import termios
import tty

MAP = '''\
#######################
#@    #       #       #
####  # ##### # ### # #
#     #     # # #   #+#
# ######### # # # # ###
#           #+ +# #   #
# ####### # ##### #   #
#+#++     #       #  $#
#######################'''

PRETTIFY = {
    '#': '🧱',
    '+': '🧪',
    '@': '🧙',
    ' ': '　',
    'g': '🧌',
    's': '💀',
    'f': '👹',
    '$': '💰',
    'z': '🧟',
}

def prettify(string):
    prettified = ''
    for c in string:
        prettified = prettified + PRETTIFY.get(c, c)
    return prettified

def get_key_pressed(): # read input without having to hit enter first
    old = termios.tcgetattr(sys.stdin.fileno())
    tty.setraw(sys.stdin.fileno()) # raw terminal magic
    try:
        return sys.stdin.read(1)
    finally:
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)

def render_string(output):
    print('\033[H' + output.replace('\n', '\033[K\n') + '\033[J') # ansi magic

class Entity:
    def __init__(self, x, y, glyph):
        self.x = x
        self.y = y
        self.glyph = glyph

class Actor(Entity):
    def __init__(self, x, y, name, glyph, max_hp, power):
        super().__init__(x, y, glyph)
        self.name = name
        self.max_hp = max_hp
        self.hp = max_hp
        self.power = power
    def alive(self):
        return self.hp > 0
    def distance_to(self, other):
        return abs(self.x - other.x) + abs(self.y - other.y)
    def attack(self, other):
        dmg = random.randint(max(1, self.power - 1), self.power + 1)
        other.hp = max(0, other.hp - dmg)
        return f'{self.name} hits {other.name} for {dmg}.'

class Player(Actor):
    def __init__(self, x, y):
        super().__init__(x, y, 'An Adventurer', '@', 24, 5)
        self.potions = 2
    def use_potion(self, game): # return True if we should count as a turn
        if self.potions <= 0:
            game.log('No potions left.')
            return False # do not count as turn
        self.potions = self.potions - 1
        self.hp = max(self.max_hp, self.hp)
        game.log(f'You quaffed a potion and heal to {self.hp} HP.')
        return True # count as turn

class Monster(Actor):
    def __init__(self, x, y, name, glyph, max_hp, power, points):
        super().__init__(x, y, name, glyph, max_hp, power)
        self.points = points
    def act(self, game):
        if self.distance_to(game.player) == 1:
            game.log(self.attack(game.player))
    def drop_item(self, game):
        return

class Goblin(Monster):
    def __init__(self, x, y):
        super().__init__(x, y, 'Goblin', 'g', 8, 3, 100)

class Zombie(Monster):
    def __init__(self, x, y):
        super().__init__(x, y, 'Zombie', 'z', 20, 2, 200)
    def attack(self, other):
        dmg = random.randint(max(1, self.power - 1), self.power + 1)
        other.hp = max(0, other.hp - dmg)
        return f'{self.name} takes a bite out of {other.name} for {dmg}.'

class Skeleton(Monster):
    def __init__(self, x, y):
        super().__init__(x, y, 'Skeleton', 's', 10, 4, 200)
    def attack(self, other):
        dmg = random.randint(max(1, self.power - 1), self.power + 1)
        other.hp = max(0, other.hp - dmg)
        return f'{self.name} hurls a bone shard at {other.name} for {dmg}.'

class FireGiant(Monster):
    def __init__(self, x, y):
        super().__init__(x, y, 'Fire Giant', 'f', 14, 6, 500)
    def attack(self, other):
        if random.random() < 0.25:
            dmg = self.power + 2
            other.hp = max(0, other.hp - dmg)
            return f'{self.name} crushes {other.name} for {dmg}.'
        return super().attack(other)
    def drop_item(self, game):
        if random.random() < 0.5:
            game.entities.append(Potion(self.x, self.y))

class Item(Entity):
    def pick_up(self, game):
        game.entities.remove(self)

class Potion(Item):
    def __init__(self, x, y):
        super().__init__(x, y, '+')
    def pick_up(self, game):
        super().pick_up(game)
        game.player.potions = game.player.potions + 1
        game.log('You pick up a potion.')

class Treasure(Item):
    def __init__(self, x, y):
        super().__init__(x, y, '$')
    def pick_up(self, game):
        super().pick_up(game)
        game.log('The treasure is yours.  But all that is gold does not glitter!')
        game.over = True

class Game:
    def __init__(self):
        self.map = []
        for row in MAP.splitlines():
            self.map.append(list(row))
        self.h = len(self.map)
        self.w = len(self.map[0])
        self.entities = []
        self.messages = [''] * 5 + ['You descend into the Bowdoin Dungeon.']
        for y, row in enumerate(self.map):
            for x, cell in enumerate(row):
                if cell == '@':
                    self.player = Player(x, y)
                    self.map[y][x] = ' '
                elif cell == '+':
                    self.entities.append(Potion(x, y))
                    self.map[y][x] = ' '
                elif cell == '$':
                    self.entities.append(Treasure(x, y))
                    self.map[y][x] = ' '
        self.spawn_monsters()
        self.time = 0
        self.score = 0
        self.over = False
    def log(self, msg):
        self.messages = self.messages[-5:] + [msg]
    def walkable(self, x, y):
        return self.map[y][x] != '#'
    def entity_at(self, x, y, kind):
        for e in self.entities:
            if e.x == x and e.y == y and isinstance(e, kind):
                return e
    def spawn_monsters(self):
        player = (self.player.x, self.player.y)
        for y, row in enumerate(self.map):
            for x, cell in enumerate(row):
                if cell != '#' and (x, y) != player and not self.entity_at(x, y, Entity):
                    roll = random.random()
                    if roll < 0.02:
                        self.entities.append(FireGiant(x, y))
                    elif roll < 0.06:
                        self.entities.append(Skeleton(x, y))
                    elif roll < 0.16:
                        self.entities.append(Goblin(x, y))
                    elif roll < 0.20:
                        self.entities.append(Zombie(x, y))
    def pick_up(self):
        item = self.entity_at(self.player.x, self.player.y, Item)
        if item:
            item.pick_up(self)
    def tick(self):
        self.time = self.time + 1
        for e in self.entities:
            if self.over or not self.player.alive():
                return
            if isinstance(e, Monster):
                e.act(self)
    def try_move(self, dx, dy): # return True if we should count as a turn
        nx = self.player.x + dx
        ny = self.player.y + dy
        if not self.walkable(nx, ny):
            self.log('A wall blocks your path.')
            return False # do not count as a turn
        foe = self.entity_at(nx, ny, Monster)
        if foe:
            self.log(self.player.attack(foe))
            if not foe.alive():
                self.log(f'The {foe.name} dies.')
                self.entities.remove(foe)
                self.score = self.score + foe.points
                foe.drop_item(self)
        else:
            self.player.x = nx
            self.player.y = ny
            self.pick_up()
        return True # count as a turn
    def render(self):
        grid = []
        for row in self.map:
            grid.append(list(row))
        for e in self.entities:
            grid[e.y][e.x] = e.glyph
        grid[self.player.y][self.player.x] = self.player.glyph
        output = 'Bowdoin Dungeon Crawler: h/j/k/l move, . wait, p potion, q quit.\n\n'
        for row in grid:
            output = output + prettify(''.join(row)) + '\n'
        output = output + f'\n🩸: {self.player.hp:2d}/{self.player.max_hp}   🧪: {self.player.potions}   🕛: {self.time}   🏆: {self.score}\n'
        for msg in self.messages:
            output = output + f'- {msg}\n'
        render_string(output)
    def play(self):
        moves = {'h': (-1, 0), 'j': (0, 1), 'k': (0, -1), 'l': (1, 0), '.': (0, 0)}
        cheat_code = 'gopolarbears'
        key_history = ''
        while not self.over:
            self.render()
            key = get_key_pressed()
            key_history = key_history + key
            key_history = key_history[-len(cheat_code):]
            if key_history == cheat_code:
                self.log('Go polar bears!')
                self.player.hp = self.player.hp + 100
                self.score = self.score - 1000000
                key_history = ''
            if key in ('q', '\x03', '\x04', ''): # q, CTRL+C, CTRL+D, EOF
                self.log('You abandon your quest.')
                self.over = True
            else:
                if key in moves:
                    if self.try_move(moves[key][0], moves[key][1]):
                        self.tick()
                elif key == 'p':
                    if self.player.use_potion(self):
                        self.tick()
                if not self.player.alive():
                    self.log('A hero has fallen.')
                    self.over = True
        self.render()

Game().play()
