# adventureEngine.py
# Copyright 2005-2006, Paul McGuire
#
# Updated 2012 - latest pyparsing API
#

from pyparsing import *
import random
import string


def aOrAn(item):
    if item.desc[0] in "aeiou":
        return "an " + item.desc
    else:
        return "a " + item.desc


def enumerateItems(l):
    if len(l) == 0:
        return "nothing"
    out = []
    if len(l) > 1:
        out.append(", ".join(aOrAn(item) for item in l[:-1]))
        out.append("and")
    out.append(aOrAn(l[-1]))
    return " ".join(out)


def enumerateDoors(l):
    if len(l) == 0:
        return ""
    out = []
    if len(l) > 1:
        out.append(", ".join(l[:-1]))
        out.append("and")
    out.append(l[-1])
    return " ".join(out)


class Room:
    def __init__(self, desc):
        self.desc = desc
        self.inv = []
        self.gameOver = False
        self.doors = [None, None, None, None]

    def __getattr__(self, attr):
        return {
            "n": self.doors[0],
            "s": self.doors[1],
            "e": self.doors[2],
            "w": self.doors[3],
        }[attr]

    def enter(self, player):
        if self.gameOver:
            player.gameOver = True

    def addItem(self, it):
        self.inv.append(it)

    def removeItem(self, it):
        self.inv.remove(it)

    def describe(self):
        print(self.desc)
        visibleItems = [it for it in self.inv if it.isVisible]
        if random.random() > 0.5:
            if len(visibleItems) > 1:
                is_form = "are"
            else:
                is_form = "is"
            print("There {} {} here.".format(is_form, enumerateItems(visibleItems)))
        else:
            print("You see %s." % (enumerateItems(visibleItems)))


class Exit(Room):
    def __init__(self):
        super().__init__("")

    def enter(self, player):
        player.gameOver = True


class Item:
    items = {}

    def __init__(self, desc):
        self.desc = desc
        self.isDeadly = False
        self.isFragile = False
        self.isBroken = False
        self.isTakeable = True
        self.isVisible = True
        self.isOpenable = False
        self.useAction = None
        self.usableConditionTest = None
        self.cantTakeMessage = "You can't take that!"
        Item.items[desc] = self

    def __str__(self):
        return self.desc

    def breakItem(self):
        if not self.isBroken:
            print("<Crash!>")
            self.desc = "broken " + self.desc
            self.isBroken = True

    def isUsable(self, player, target):
        if self.usableConditionTest:
            return self.usableConditionTest(player, target)
        else:
            return False

    def useItem(self, player, target):
        if self.useAction:
            self.useAction(player, self, target)


class OpenableItem(Item):
    def __init__(self, desc, contents=None):
        super().__init__(desc)
        self.isOpenable = True
        self.isOpened = False
        if contents is not None:
            if isinstance(contents, Item):
                self.contents = [
                    contents,
                ]
            else:
                self.contents = contents
        else:
            self.contents = []

    def openItem(self, player):
        if not self.isOpened:
            self.isOpened = not self.isOpened
            if self.contents is not None:
                for item in self.contents:
                    player.room.addItem(item)
                self.contents = []
            self.desc = "open " + self.desc

    def closeItem(self, player):
        if self.isOpened:
            self.isOpened = not self.isOpened
            if self.desc.startswith("open "):
                self.desc = self.desc[5:]


class Command:
    "Base class for commands"

    def __init__(self, verb, verbProg):
        self.verb = verb
        self.verbProg = verbProg

    @staticmethod
    def helpDescription():
        return ""

    def _doCommand(self, player):
        pass

    def __call__(self, player):
        print(self.verbProg.capitalize() + "...")
        self._doCommand(player)


class MoveCommand(Command):
    def __init__(self, quals):
        super().__init__("MOVE", "moving")
        self.direction = quals.direction[0]

    @staticmethod
    def helpDescription():
        return """MOVE or GO - go NORTH, SOUTH, EAST, or WEST
          (can abbreviate as 'GO N' and 'GO W', or even just 'E' and 'S')"""

    def _doCommand(self, player):
        rm = player.room
        nextRoom = rm.doors[
            {
                "N": 0,
                "S": 1,
                "E": 2,
                "W": 3,
            }[self.direction]
        ]
        if nextRoom:
            player.moveTo(nextRoom)
        else:
            print("Can't go that way.")


class TakeCommand(Command):
    def __init__(self, quals):
        super().__init__("TAKE", "taking")
        self.subject = quals.item

    @staticmethod
    def helpDescription():
        return "TAKE or PICKUP or PICK UP - pick up an object (but some are deadly)"

    def _doCommand(self, player):
        rm = player.room
        subj = Item.items[self.subject]
        if subj in rm.inv and subj.isVisible:
            if subj.isTakeable:
                rm.removeItem(subj)
                player.take(subj)
            else:
                print(subj.cantTakeMessage)
        else:
            print("There is no %s here." % subj)


class DropCommand(Command):
    def __init__(self, quals):
        super().__init__("DROP", "dropping")
        self.subject = quals.item

    @staticmethod
    def helpDescription():
        return "DROP or LEAVE - drop an object (but fragile items may break)"

    def _doCommand(self, player):
        rm = player.room
        subj = Item.items[self.subject]
        if subj in player.inv:
            rm.addItem(subj)
            player.drop(subj)
        else:
            print("You don't have %s." % (aOrAn(subj)))


class InventoryCommand(Command):
    def __init__(self, quals):
        super().__init__("INV", "taking inventory")

    @staticmethod
    def helpDescription():
        return "INVENTORY or INV or I - lists what items you have"

    def _doCommand(self, player):
        print("You have %s." % enumerateItems(player.inv))


class LookCommand(Command):
    def __init__(self, quals):
        super().__init__("LOOK", "looking")

    @staticmethod
    def helpDescription():
        return "LOOK or L - describes the current room and any objects in it"

    def _doCommand(self, player):
        player.room.describe()


class DoorsCommand(Command):
    def __init__(self, quals):
        super().__init__("DOORS", "looking for doors")

    @staticmethod
    def helpDescription():
        return "DOORS - display what doors are visible from this room"

    def _doCommand(self, player):
        rm = player.room
        numDoors = sum([1 for r in rm.doors if r is not None])
        if numDoors == 0:
            reply = "There are no doors in any direction."
        else:
            if numDoors == 1:
                reply = "There is a door to the "
            else:
                reply = "There are doors to the "
            doorNames = [
                {0: "north", 1: "south", 2: "east", 3: "west"}[i]
                for i, d in enumerate(rm.doors)
                if d is not None
            ]
            # ~ print doorNames
            reply += enumerateDoors(doorNames)
            reply += "."
            print(reply)


class UseCommand(Command):
    def __init__(self, quals):
        super().__init__("USE", "using")
        self.subject = Item.items[quals.usedObj]
        if quals.targetObj:
            self.target = Item.items[quals.targetObj]
        else:
            self.target = None

    @staticmethod
    def helpDescription():
        return "USE or U - use an object, optionally IN or ON another object"

    def _doCommand(self, player):
        rm = player.room
        availItems = rm.inv + player.inv
        if self.subject in availItems:
            if self.subject.isUsable(player, self.target):
                self.subject.useItem(player, self.target)
            else:
                print("You can't use that here.")
        else:
            print("There is no %s here to use." % self.subject)


class OpenCommand(Command):
    def __init__(self, quals):
        super().__init__("OPEN", "opening")
        self.subject = Item.items[quals.item]

    @staticmethod
    def helpDescription():
        return "OPEN or O - open an object"

    def _doCommand(self, player):
        rm = player.room
        availItems = rm.inv + player.inv
        if self.subject in availItems:
            if self.subject.isOpenable:
                if not self.subject.isOpened:
                    self.subject.openItem(player)
                else:
                    print("It's already open.")
            else:
                print("You can't open that.")
        else:
            print("There is no %s here to open." % self.subject)


class CloseCommand(Command):
    def __init__(self, quals):
        super().__init__("CLOSE", "closing")
        self.subject = Item.items[quals.item]

    @staticmethod
    def helpDescription():
        return "CLOSE or CL - close an object"

    def _doCommand(self, player):
        rm = player.room
        availItems = rm.inv + player.inv
        if self.subject in availItems:
            if self.subject.isOpenable:
                if self.subject.isOpened:
                    self.subject.closeItem(player)
                else:
                    print("You can't close that, it's not open.")
            else:
                print("You can't close that.")
        else:
            print("There is no %s here to close." % self.subject)


class QuitCommand(Command):
    def __init__(self, quals):
        super().__init__("QUIT", "quitting")

    @staticmethod
    def helpDescription():
        return "QUIT or Q - ends the game"

    def _doCommand(self, player):
        print("Ok....")
        player.gameOver = True


class HelpCommand(Command):
    def __init__(self, quals):
        super().__init__("HELP", "helping")

    @staticmethod
    def helpDescription():
        return "HELP or H or ? - displays this help message"

    def _doCommand(self, player):
        print("Enter any of the following commands (not case sensitive):")
        for cmd in [
            InventoryCommand,
            DropCommand,
            TakeCommand,
            UseCommand,
            OpenCommand,
            CloseCommand,
            MoveCommand,
            LookCommand,
            DoorsCommand,
            QuitCommand,
            HelpCommand,
        ]:
            print("  - %s" % cmd.helpDescription())
        print()


class AppParseException(ParseException):
    pass


class Parser:
    def __init__(self):
        self.bnf = self.makeBNF()

    def makeBNF(self):
        invVerb = oneOf("INV INVENTORY I", caseless=True)
        dropVerb = oneOf("DROP LEAVE", caseless=True)
        takeVerb = oneOf("TAKE PICKUP", caseless=True) | (
            CaselessLiteral("PICK") + CaselessLiteral("UP")
        )
        moveVerb = oneOf("MOVE GO", caseless=True) | empty
        useVerb = oneOf("USE U", caseless=True)
        openVerb = oneOf("OPEN O", caseless=True)
        closeVerb = oneOf("CLOSE CL", caseless=True)
        quitVerb = oneOf("QUIT Q", caseless=True)
        lookVerb = oneOf("LOOK L", caseless=True)
        doorsVerb = CaselessLiteral("DOORS")
        helpVerb = oneOf("H HELP ?", caseless=True)

        itemRef = OneOrMore(Word(alphas)).setParseAction(self.validateItemName)
        nDir = oneOf("N NORTH", caseless=True).setParseAction(replaceWith("N"))
        sDir = oneOf("S SOUTH", caseless=True).setParseAction(replaceWith("S"))
        eDir = oneOf("E EAST", caseless=True).setParseAction(replaceWith("E"))
        wDir = oneOf("W WEST", caseless=True).setParseAction(replaceWith("W"))
        moveDirection = nDir | sDir | eDir | wDir

        invCommand = invVerb
        dropCommand = dropVerb + itemRef("item")
        takeCommand = takeVerb + itemRef("item")
        useCommand = (
            useVerb
            + itemRef("usedObj")
            + Optional(oneOf("IN ON", caseless=True))
            + Optional(itemRef, default=None)("targetObj")
        )
        openCommand = openVerb + itemRef("item")
        closeCommand = closeVerb + itemRef("item")
        moveCommand = moveVerb + moveDirection("direction")
        quitCommand = quitVerb
        lookCommand = lookVerb
        doorsCommand = doorsVerb
        helpCommand = helpVerb

        # attach command classes to expressions
        invCommand.setParseAction(InventoryCommand)
        dropCommand.setParseAction(DropCommand)
        takeCommand.setParseAction(TakeCommand)
        useCommand.setParseAction(UseCommand)
        openCommand.setParseAction(OpenCommand)
        closeCommand.setParseAction(CloseCommand)
        moveCommand.setParseAction(MoveCommand)
        quitCommand.setParseAction(QuitCommand)
        lookCommand.setParseAction(LookCommand)
        doorsCommand.setParseAction(DoorsCommand)
        helpCommand.setParseAction(HelpCommand)

        # define parser using all command expressions
        return (
            invCommand
            | useCommand
            | openCommand
            | closeCommand
            | dropCommand
            | takeCommand
            | moveCommand
            | lookCommand
            | doorsCommand
            | helpCommand
            | quitCommand
        )("command") + LineEnd()

    def validateItemName(self, s, l, t):
        iname = " ".join(t)
        if iname not in Item.items:
            raise AppParseException(s, l, "No such item '%s'." % iname)
        return iname

    def parseCmd(self, cmdstr):
        try:
            ret = self.bnf.parseString(cmdstr)
            return ret
        except AppParseException as pe:
            print(pe.msg)
        except ParseException as pe:
            print(
                random.choice(
                    [
                        "Sorry, I don't understand that.",
                        "Huh?",
                        "Excuse me?",
                        "???",
                        "What?",
                    ]
                )
            )


class Player:
    def __init__(self, name):
        self.name = name
        self.gameOver = False
        self.inv = []

    def moveTo(self, rm):
        self.room = rm
        rm.enter(self)
        if self.gameOver:
            if rm.desc:
                rm.describe()
            print("Game over!")
        else:
            rm.describe()

    def take(self, it):
        if it.isDeadly:
            print("Aaaagh!...., the %s killed me!" % it)
            self.gameOver = True
        else:
            self.inv.append(it)

    def drop(self, it):
        self.inv.remove(it)
        if it.isFragile:
            it.breakItem()


def createRooms(rm):
    """
    create rooms, using multiline string showing map layout
    string contains symbols for the following:
     A-Z, a-z indicate rooms, and rooms will be stored in a dictionary by
               reference letter
     -, | symbols indicate connection between rooms
     <, >, ^, . symbols indicate one-way connection between rooms
    """
    # start with empty dictionary of rooms
    ret = {}

    # look for room symbols, and initialize dictionary
    # - exit room is always marked 'Z'
    for c in rm:
        if c in string.ascii_letters:
            if c != "Z":
                ret[c] = Room(c)
            else:
                ret[c] = Exit()

    # scan through input string looking for connections between rooms
    rows = rm.split("\n")
    for row, line in enumerate(rows):
        for col, c in enumerate(line):
            if c in string.ascii_letters:
                room = ret[c]
                n = None
                s = None
                e = None
                w = None

                # look in neighboring cells for connection symbols (must take
                # care to guard that neighboring cells exist before testing
                # contents)
                if col > 0 and line[col - 1] in "<-":
                    other = line[col - 2]
                    w = ret[other]
                if col < len(line) - 1 and line[col + 1] in "->":
                    other = line[col + 2]
                    e = ret[other]
                if row > 1 and col < len(rows[row - 1]) and rows[row - 1][col] in "|^":
                    other = rows[row - 2][col]
                    n = ret[other]
                if (
                    row < len(rows) - 1
                    and col < len(rows[row + 1])
                    and rows[row + 1][col] in "|."
                ):
                    other = rows[row + 2][col]
                    s = ret[other]

                # set connections to neighboring rooms
                room.doors = [n, s, e, w]

    return ret


# put items in rooms
def putItemInRoom(i, r):
    if isinstance(r, str):
        r = rooms[r]
    r.addItem(Item.items[i])


def playGame(p, startRoom):
    # create parser
    parser = Parser()
    p.moveTo(startRoom)
    while not p.gameOver:
        cmdstr = input(">> ")
        cmd = parser.parseCmd(cmdstr)
        if cmd is not None:
            cmd.command(p)
    print()
    print("You ended the game with:")
    for i in p.inv:
        print(" -", aOrAn(i))


# ====================
# start game definition
roomMap = """
     d-Z
     |
   f-c-e
   . |
   q<b
     |
     A
"""
rooms = createRooms(roomMap)
rooms["A"].desc = "You are standing on the front porch of a wooden shack."
rooms["b"].desc = "You are in a garden."
rooms["c"].desc = "You are in a kitchen."
rooms["d"].desc = "You are on the back porch."
rooms["e"].desc = "You are in a library."
rooms["f"].desc = "You are on the patio."
rooms["q"].desc = "You are sinking in quicksand.  You're dead..."
rooms["q"].gameOver = True

# define global variables for referencing rooms
frontPorch = rooms["A"]
garden = rooms["b"]
kitchen = rooms["c"]
backPorch = rooms["d"]
library = rooms["e"]
patio = rooms["f"]

# create items
itemNames = (
    """sword.diamond.apple.flower.coin.shovel.book.mirror.telescope.gold bar""".split(
        "."
    )
)
for itemName in itemNames:
    Item(itemName)
Item.items["apple"].isDeadly = True
Item.items["mirror"].isFragile = True
Item.items["coin"].isVisible = False
Item.items["shovel"].usableConditionTest = lambda p, t: p.room is garden


def useShovel(p, subj, target):
    coin = Item.items["coin"]
    if not coin.isVisible and coin in p.room.inv:
        coin.isVisible = True


Item.items["shovel"].useAction = useShovel

Item.items["telescope"].isTakeable = False


def useTelescope(p, subj, target):
    print("You don't see anything.")


Item.items["telescope"].useAction = useTelescope

OpenableItem("treasure chest", Item.items["gold bar"])
Item.items["chest"] = Item.items["treasure chest"]
Item.items["chest"].isTakeable = False
Item.items["chest"].cantTakeMessage = "It's too heavy!"

OpenableItem("mailbox")
Item.items["mailbox"].isTakeable = False
Item.items["mailbox"].cantTakeMessage = "It's nailed to the wall!"

putItemInRoom("mailbox", frontPorch)
putItemInRoom("shovel", frontPorch)
putItemInRoom("coin", garden)
putItemInRoom("flower", garden)
putItemInRoom("apple", library)
putItemInRoom("mirror", library)
putItemInRoom("telescope", library)
putItemInRoom("book", kitchen)
putItemInRoom("diamond", backPorch)
putItemInRoom("treasure chest", patio)

# create player
plyr = Player("Bob")
plyr.take(Item.items["sword"])

# start game
playGame(plyr, frontPorch)
