{ "info": { "author": "Sam Hopkins", "author_email": "sam@daredata.engineering", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3" ], "description": "# Novamud\n\nA very simple and flexible text-based online multiplayer framework.\nDungeons can be built with pure python in very simple classes. The required\npython knowledge to work with this is:\n\n- Native python data types (integers, strings, numbers)\n- Basic data structures such as lists, dictionaries\n- Defining and calling functions\n- Running a script\n- The basics of OOP\n - Creating classes\n - Overriding functions and how to call `super()`\n\n[Check out the API for a functionality reference.](https://gitlab.com/hershaw/novamud/blob/master/API.md)\n\n## Tutorials\n\n### Overview\n\n#### API overview\n\nThe novamud api provides a way to build `Room` based `Dungeon`s. It cannot be \nstressed enough that everything that goes on here is centered around Rooms.\n`Room`s decide everything, hold everything, where functionality is defined,\nand can be connected to other `Room`s.\n\nAlthough the `Room`s contain the heart of the functionality, the `Dungeon` is\nthe master coordinator of it all. The `Dungeon` is the where you initially\ncreate the rooms and connect them.\n\nYou can find each of the tutorial dungeons in the `tutorials/` section\nof the repo.\n\n#### Playing the game\n\nThe `Dungeon`s themselves just runs a websocket server. After you have written \nthe code for a new `Dungeon` and have it running on your local machine,\nyou can go to [the novamud client](http://novamud-client.herokuapp.com/index.html)\nand enter `ws://localhost:8080` and hit the connect button to start playing\nthe game. By default it listens on all IP addresses so anyone on your\nlocal network should be able to connect to it if they know your internal IP\naddress.\n\n#### Chat functionality\n\nThe novamud framework comes with builtin chat functionality. You can either\nchat to everyone that is in the same room that you are in OR you can chat\nwith everyone that is in the other rooms.\n\n- `say ...` is how you chat to everyone in the same room as you. Examples:\n - `say hello everyone!` And everyone will see `hello everyone!` show up\n on their screen next to your name.\n - `say hey guys, how are you today? for lunch I had a bitoque and it was magical`\n and everyone will see `hey guys, how are you today? for lunch I had a bitoque and it was magical`\n show up on their screen next to your name.\n- `say_dungeon ...` - works the same as `say` but everyone in the Dungeon will\n see your message rather than just everyone in your room. Be careful using\n this one because you don't want to be spamming the entire dungeon...\n\n### Baby's First Dungeon\n\n\n```py\n# in tutorials/babys_first_dungeon.py\nfrom novamud import Dungeon, Room\n\n\nclass BabyRoom(Room):\n pass\n\n\nclass BabyDungeon(Dungeon):\n def init_dungeon(self):\n hr = BabyRoom(self)\n self.start_room = hr\n\n\nif __name__ == '__main__':\n BabyDungeon().start_dungeon()\n\n```\n\nYou can run this dungeon by executing `python examples/babys_first_dungeon.py`.\n\nNow with this Dungeon you can get all of the default basic functionality of the \nnovamud framework up and running. So go ahead and run this file and you can get\nthe following experience:\n\n```\nPlease enter a name less than 8 chars long\n```\nAt this point I enter my name `sam` and hit the enter key\n\n```\nWelcome to Default Dungeon, sam\nDefault description for a default dungeon... did you forget to add a description?\n\nHope your are ready to start your adventure! Hit the enter key to enter the first room and then and then call \"describe_room\" and \"show_commands\" to get started!\n```\n\nNow that I have read about the two most important commands I can hit \nto go onto the very first room!\n\n```\nWelcome to the Default Room room\n```\n\nOkay cool, I know the name of the room that I am in but not much else, so I\nwill use the `describe_room` command\n```\nThis is a default room description. Be sure to come up with something better for this or your players will be bored AF.\nContains 1 players and 0 items\nThis room has no active doors to other rooms\n```\n\nOkay, looks like a coder forgot to do some work. Whatever though, let's see\nwhat I can do by using the `show_commands` command\n```\nRoom commands\nsay - Say something to everyone in the room you are currently in\nsay_dungeon - Say something to the entire Dungeon (all people in all rooms)\npick_up - Pick up a thing by its ID\ndrop - Drop the item you are currently carrying\ndescribe_room - Describe all objects, things\nlist_things - List all things that may be picked up in the room\ndescribe_thing - List all things that are in the room\ngo_to - Select a room to leave to\nshow_commands - Show all commands that are available to the room\n```\n\nAll together it looks like this:\n\n\n\n### ToddlerDungeon\n\nSo all we did with BabyDungeon was write the absolute minimum code\nneeded to make something run. We didn't really learn anything at all about\nwhat you can do with the framework so let's dip our toes into something\nthat has a few more interesting things in it with a ToddlerDungeon:\n\n```py\nfrom novamud import Dungeon, Room\n\n\nclass ToddlerRoom(Room):\n\n def register_commands(self):\n return ['hello_toddler']\n\n def hello_toddler(self, player):\n player.tell('toddler: hewo der, {}!'.format(player.name))\n\n def hello_toddler_describe(self):\n return 'Say hello to the toddler that is in the room.'\n\n\nclass ToddlerDungeon(Dungeon):\n def init_dungeon(self):\n tr = ToddlerRoom(self)\n self.start_room = tr\n\nif __name__ == '__main__':\n ToddlerDungeon().start_dungeon()\n \n```\n\n#### What's new here?\n\nSo the `ToddlerDungeon` looks pretty much the same as the `BabyDungeon`, no\nbig thing there. However, there's now some stuff going on inside the\n`ToddlerRoom` that we didn't see before. Let's take a closer look at that.\n\n#### ToddlerRoom\n\nThere are now 3 new functions defined on the `ToddlerRoom` that we haven't\nseen before. \n\n- `register_commands` - Remember when we said that everything is `Room` based\n including functionality? The `register_commands` function is how you tell\n the `ToddlerRoom` which of the commands should be available. The reason that\n we need to do this is that we may have other helper functions that we define\n on the `ToddlerRoom` that we don't want to give to the players as commands.\n- `hello_toddler` - Remember how we registered commands? Well here is where\n we define exactly what it does. All commands get one required argument which\n is the player that executed the command. On the player, there is a function\n called `tell` which takes a string as an argument and sends it to them. Notice\n that the player has an attribute called `name` on it which contains the\n name that the person gave us when they entered the dungeon.\n- `hello_toddler_describe` - Any time that we register a new command, we must\n write another function that has the same name with an `_describe` that\n returns a string. It is this function that is called with someone calls\n the `show_commands` command.\n\nSo if we run the `ToddlerDungeon` and use the basic functionality, we get\nthe following:\n\n\n\n\n### TeenageDungeon\n\nNow that we have learned how to add new commands and are starting to grow up,\nlet's take a look at a few more concepts, namely\n\n- Adding descriptions and names to everything\n- Adding a `Thing`\n\n```py\nfrom novamud import Dungeon, Room, Thing\n\n\nclass ColdplayPoster(Thing):\n name = 'ColdplayPoster'\n description = \"Much angst, such coldplay\"\n\n\nclass TeenagerRoom(Room):\n name = 'TeenagerRoom'\n description = (\"A room full of angst. You find a few coldplay posters on\"\n \"the wall and it smells slightly of disdain for authority.\")\n\n def init_room(self):\n cp1 = ColdplayPoster()\n cp2 = ColdplayPoster()\n self.add_thing(cp1)\n self.add_thing(cp2)\n\n def register_commands(self):\n return ['hello_teenager']\n\n def hello_teenager(self, player):\n player.tell(\n 'teenager: ugh. why are you bothering me, {}?'.format(player.name)\n )\n\n def hello_teenager_describe(self):\n return 'Say hello to the teenager that is in the room.'\n\n\nclass TeenagerDungeon(Dungeon):\n name = 'TeenagerDungeon'\n description = \"A dungeon that is as dark as the average teenage soul.\"\n\n def init_dungeon(self):\n tr = TeenagerRoom(self)\n self.start_room = tr\n\n\nif __name__ == '__main__':\n TeenagerDungeon().start_dungeon()\n\n```\n\n#### Descriptions and names\n\nFirst thing to notice is that the `TeenagerDungeon` and the `TeenagerRoom`\nhave a few new variables defined on them which are the `name` and the\n`description`. These are both used to help the user understand where they are\nas well as give you (the game designer) a bit of creative freedom in setting\nthe tone and feeling of your game.\n\nRun this dungeon and execute the following commands to see what the output\nlooks like:\n\n```\ndescribe_room\n```\n\nNotice the differences and where the descriptions show up.\n\n#### Things\n\nNotice the new class `ColdplayPoster` that inherits from `Thing`. It doesn't\nhave anything except for a name and description. True this is quite spartan\nbut this will actually get you quite far. \n\nInside of the newly introduced `init_room` function, we are creating two\ndifferent `ColdplayPoster`s. This is something that is quite different\nabout things as compared to `Dungeon`s and `Room`s. Each instiantated\n`Thing` will have its own `id` which is how you can keep track of which\none is which.\n\nWhen you call the `list_things` command, you will now notice that there\nare two coldplay posters, each with it's own `id`. This is how you can\ntell which one you want to pick up.\n\n#### Interacting with things\n\nYou can pick up and drop things! Try the following commands to get a sense\nfor how to interact with the new `ColdplayPoster` things that you have\ncreated:\n\n```\nlist_things\npick_up ColdplayPoster_1\nlist_things\npick_up ColdplayPoster_2\nlist_things\ndrop\nlist_things\n```\n\nAsk yourself: how many things can you carry at a time? when you pick one up\nis it still available in the room? After you drop it again, does it\nbecome available in the room?\n\n### YoungAdultDungeon\n\nNow that we have a sense for all of the important elements of the novadungeon\nsystem, let's make them interact with each other in a way that actually\nproduces some incentives and a very simple puzzle:\n\n```py\nfrom novamud import Dungeon, Room, Thing\n\n\nclass CarKeys(Thing):\n name = 'CarKeys'\n description = \"You need to have the car keys before you can leave\"\n\n\nclass ApartmentRoom(Room):\n name = 'ApartmentRoom'\n description = (\"Spartan but functional. Not particularly clean but that \"\n \"can be taken care of before your parents come by for \"\n \"your Sunday dinner that you normally hold at your house.\")\n\n def init_room(self):\n self.add_thing(CarKeys())\n\n def go_to(self, player, other_room_name):\n if not player.carrying or player.carrying.name != 'CarKeys':\n player.tell(\"You ain't going anywhere without those CarKeys!\")\n else:\n super().go_to(player, other_room_name)\n\n\nclass TascaRoom(Room):\n name = 'TascaRoom'\n description = (\"Your friendly local Tasca. The wine is cheap and passable \"\n \"but certainly nothing special. The bitoque is fantasic.\")\n\n def register_commands(self):\n return [\n 'eat_bitoque',\n 'drink_wine',\n ]\n\n def add_player(self, player):\n player.drink_level = 0\n super().add_player(player)\n\n def eat_bitoque(self, player):\n if player.drink_level > 0:\n player.drink_level -= 1\n player.tell(\n \"Much better! Your drink level is now down to {}\".format(\n player.drink_level)\n )\n else:\n player.tell(\"mmmmmmmm, bitoque!\")\n\n def eat_bitoque_describe(self):\n return \"Have a bitoque in case you are getting a bit too tipsy.\"\n\n def drink_wine(self, player):\n player.drink_level += 1\n if player.drink_level == 1:\n player.tell('The price makes it taste okay!')\n elif 1 <= player.drink_level < 2:\n player.tell(\n \"Your drink level is {}, you're still in the good zone\".format(\n player.drink_level\n )\n )\n elif player.drink_level >= 3:\n player.tell(\n \"It might be good to have some food in order to bring that\"\n \"drink level down a bit... you're currently at drink \"\n \"level {}\".format(player.drink_level)\n )\n\n def drink_wine_describe(self):\n return (\"Have some wine! If you have a bit too much, you can always \"\n \"eat some food to bring yourself down\")\n\n\nclass YoungAdultDungeon(Dungeon):\n\n name = 'YoungAdultDungeon'\n description = (\"You are now a young adult. You have a crappy apartment\"\n \"and a car with which you can go to meet up with your \"\n \"friends at a TascaRoom.\")\n\n def init_dungeon(self):\n ar = ApartmentRoom(self)\n tr = TascaRoom(self)\n ar.connect_room(tr, two_way=True)\n self.start_room = ar\n\n\nif __name__ == '__main__':\n YoungAdultDungeon().start_dungeon()\n```\n\nWoah, now there's a much bigger pile of code! What the hell is going on here!\nDon't worry your young adult head because although there's more code here\nthere isn't anything very complicated going on. There's just quite a few\nexamples of how to make the objects in the system interact with each other.\n\n#### Multiple rooms\n\nNotice we now have several `Room`s! This makes sense as `Dungeon`s with only\na single room can get very boring very quickly. Notice that in the \n`init_dungeon` function we instantiate both rooms and set one of them as\nthe `start_room`.\n\n#### connect_room\n\nThe single most important new concept in this `Dungeon` is the demonstration\nof how we can connect rooms to each other. After instiantiating two rooms,\nwe can connect them with the `Room.connect_room` function. Notice the second\nkeyword argument `two_way=True` which indicates that there should be a door\nfrom the `ApartmentRoom` into the `TascaRoom` AND a door from the `TascaRoom`\nto the `ApartmentRoom`. `two_way` defaults to `True` and although we haven't\ndemonstrated what it looks like to make a one-way door it is something\nthat you can use to create different and interesting puzzles later on.\n\n#### leaving the ApartmentRoom\n\nHere we have our first meaningful interaction with a `Thing` as well as an\naugmenting of `Room` functionality by overloading the `go_to` method. Let's\ntake a closer look at it:\n\n```py\ndef go_to(self, player, other_room_name):\n if not player.carrying or player.carrying.name != 'CarKeys':\n player.tell(\"You ain't going anywhere without those CarKeys!\")\n else:\n super().go_to(player, other_room_name)\n```\n\n`go_to` is a command just like the other commands that we've learned about\nearlier but since it's already defined on the base `Room` class, we need to\ntake the same two parameters that it takes. Remember that the `player` is\nalways the first argument passed to any command so we can take advantage of\nthis to see if they are carrying their `CarKeys` by checking the `carrying`\nattribute to make sure that they aren't leaving the apartment without their\n`CarKeys`. \n\nThis is a very important thing to know because any command that is available\non rooms (except for the `say` and `say_dungeon` commands) can be overridden\nin this same fashion to impose limits on them or augment their functionality.\n\nWhen playing the game, you'll notice that you can't leave the room unless\nyou have your keys. Pretty cool that we get this functionality with a\n4 line function, right?\n\n#### In the TascaRoom\n\nOnce you manage to get out of the `ApartmentRoom` and into the `TascaRoom`\nyou now have the chance to hang out for a while and enjoy yourself. Go ahead\nand drink some wine and eat some bitoque and see what happens when you do.\nSpoiler alert, if you drink you'll get a bit tipsy and if you eat some bitoque\nit will bring your buzz-level back down.\n\nThis is a significant development because we are now keeping custom state\non the Player itself and we are doing this by overloading some base `Room`\nfunctionality as well as with our own commands. This combination is very\npowerful so let's take a closer look:\n\nFirst let's look a the overriding of the `add_player` function:\n\n```py\ndef add_player(self, player):\n player.drink_level = 0\n super().add_player(player)\n```\n\nWe can see that we are defining a new attribute on the player which is called\n`drink_level`. We can now be confident that any player that enters the \n`TascaRoom` will have a `drink_level` of 0. Remember to make the call to\n`super()` or you'll break the whole framework!\n\nNow let's have a look a the `drink_wine` command:\n\n```py\ndef drink_wine(self, player):\n player.drink_level += 1\n if player.drink_level == 1:\n player.tell('The price makes it taste okay!')\n elif 1 <= player.drink_level < 2:\n player.tell(\n \"Your drink level is {}, you're still in the good zone\".format(\n player.drink_level\n )\n )\n elif player.drink_level >= 3:\n player.tell(\n \"It might be good to have some food in order to bring that\"\n \"drink level down a bit... you're currently at drink \"\n \"level {}\".format(player.drink_level)\n )\n```\n\nRemember how all players enter the `TascaRoom` with a `drink_level` of 0? Well\nnow we can bump that up! Plus we can add a bit of fun to the game by sending\nthe player different messages depending on how much wine they have had to\ndrink. Notice that a player can have an arbitrarily high `drink_level`... not\nthe mot realistic thing but we can easily fix that with a few lines of code\nif we wanted to.\n\nFinally, let's see how we can get that `drink_level` down a bit by eating some\nbitoque:\n\n```py\ndef eat_bitoque(self, player):\n if player.drink_level > 0:\n player.drink_level -= 1\n player.tell(\n \"Much better! Your drink level is now down to {}\".format(\n player.drink_level)\n )\n else:\n player.tell(\"mmmmmmmm, bitoque!\")\n```\n\nAs you can see we can easily implement a rule by which we check to see if\nthe `drink_level` is > 0 and then bring it down if so. If they don't have\nany drink in them at all then they will just enjoy their bitoque!\n\n\n### TavernDungeon\n\nSo far everything is just single-player which makes you wonder why the \nframework as 'mud' in the name! MUD stands for Multi User Dungeon after all.\n\nTo see a dungeon that actually requires at least 5 people to play, check\nout the `TavernDungeon` in `example_dungeon.py`. It is made using all of the\nconcepts that have already been introduced in the above tutorials and nothing\nradically different.", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://gitlab.com/hershaw/novamud", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "novamud", "package_url": "https://pypi.org/project/novamud/", "platform": "", "project_url": "https://pypi.org/project/novamud/", "project_urls": { "Homepage": "https://gitlab.com/hershaw/novamud" }, "release_url": "https://pypi.org/project/novamud/0.0.8/", "requires_dist": null, "requires_python": "", "summary": "Simple and flexible mud framework for people with only basic knowledge of python", "version": "0.0.8" }, "last_serial": 4418820, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "12ea8bffedbe304a6329ece03417dbc8", "sha256": "fba30697299f006e338a51d9d6fde2a36f15f2277516908d8e04e9ca053b6336" }, "downloads": -1, "filename": "novamud-0.0.1.tar.gz", "has_sig": false, "md5_digest": "12ea8bffedbe304a6329ece03417dbc8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4129, "upload_time": "2018-10-21T12:57:49", "url": "https://files.pythonhosted.org/packages/3e/0e/cf1a6394229aa38f3f689d84963c230104f4ee1367dba398882d2cd2b6bc/novamud-0.0.1.tar.gz" } ], "0.0.2": [ { "comment_text": "", "digests": { "md5": "553fd9e8ff83b00677652d53ee8f0812", "sha256": "550463cb7988b0a77801050cdec1b7806580bea18d2df9281beea22593700aa3" }, "downloads": -1, "filename": "novamud-0.0.2.tar.gz", "has_sig": false, "md5_digest": "553fd9e8ff83b00677652d53ee8f0812", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4127, "upload_time": "2018-10-21T13:00:18", "url": "https://files.pythonhosted.org/packages/e3/36/cd3fa5f4ccb1543176ee5d965b9a9822af5d11eeb463041776f1ba566c4e/novamud-0.0.2.tar.gz" } ], "0.0.3": [ { "comment_text": "", "digests": { "md5": "dd36b3357ef8149c820a1677f48d2ea6", "sha256": "d9f65e79cf7ab00173d7d32c6aa0c538b6ee304e7b2a3f14281bd3530b861889" }, "downloads": -1, "filename": "novamud-0.0.3.tar.gz", "has_sig": false, "md5_digest": "dd36b3357ef8149c820a1677f48d2ea6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4281, "upload_time": "2018-10-23T07:24:20", "url": "https://files.pythonhosted.org/packages/ed/d4/aa676c7ed1c5d88f2c80e3157b02f5e31534d528e63e54c92c04297c7229/novamud-0.0.3.tar.gz" } ], "0.0.4": [ { "comment_text": "", "digests": { "md5": "60f1c6b568fced0b630297f2842a82f1", "sha256": "19a42a9f0e797e24c5fc863be40753b8f8e3a7f23b41e815bfccf6835398a340" }, "downloads": -1, "filename": "novamud-0.0.4.tar.gz", "has_sig": false, "md5_digest": "60f1c6b568fced0b630297f2842a82f1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18197, "upload_time": "2018-10-23T17:02:06", "url": "https://files.pythonhosted.org/packages/54/b0/5282cd4493ebfbd6a0967c1581a587351f6702f4dd504c63c1dd31e51bc1/novamud-0.0.4.tar.gz" } ], "0.0.5": [ { "comment_text": "", "digests": { "md5": "b6ee18bbae4865074308d85dfdea1d59", "sha256": "91858ef2dfc3239fcfa40aad063727fb3d170f0cf7c80c075263524ede7fdbd2" }, "downloads": -1, "filename": "novamud-0.0.5.tar.gz", "has_sig": false, "md5_digest": "b6ee18bbae4865074308d85dfdea1d59", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18504, "upload_time": "2018-10-24T17:34:33", "url": "https://files.pythonhosted.org/packages/b7/78/e1b6c5ffa567023d3d599d42a6cb1b14e7313ef8cc25add55a0a8227ce18/novamud-0.0.5.tar.gz" } ], "0.0.6": [ { "comment_text": "", "digests": { "md5": "264253f07b7bbf3f7d8f68490985975d", "sha256": "91f1fa4823cc597330d01c7f02b4ce6960242cf968d7ebe3dc440eeb2cf357f8" }, "downloads": -1, "filename": "novamud-0.0.6.tar.gz", "has_sig": false, "md5_digest": "264253f07b7bbf3f7d8f68490985975d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18564, "upload_time": "2018-10-24T17:35:59", "url": "https://files.pythonhosted.org/packages/98/f4/d436a9b22aa3bc836de03a9c9e796df12c13155f347727da6027915a22c8/novamud-0.0.6.tar.gz" } ], "0.0.7": [ { "comment_text": "", "digests": { "md5": "827ea02954604eadd0210d9d2edb491a", "sha256": "7dae63dcdc3d9dc8c2466ad91cbcaad6c837633b06331a376a6893e9f8484fea" }, "downloads": -1, "filename": "novamud-0.0.7.tar.gz", "has_sig": false, "md5_digest": "827ea02954604eadd0210d9d2edb491a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18572, "upload_time": "2018-10-25T17:31:55", "url": "https://files.pythonhosted.org/packages/53/c0/ce1fb39b98b89fc9c89d9c5782e460e2a7d6c3e5970c3f46f1e5bca91f6d/novamud-0.0.7.tar.gz" } ], "0.0.8": [ { "comment_text": "", "digests": { "md5": "49d21c552becf47fca89adf806366e35", "sha256": "234555ab848dab4919433ec7d24d7b3259094c695cd9af5304a8bd8fba7fffac" }, "downloads": -1, "filename": "novamud-0.0.8.tar.gz", "has_sig": false, "md5_digest": "49d21c552becf47fca89adf806366e35", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18659, "upload_time": "2018-10-26T10:13:54", "url": "https://files.pythonhosted.org/packages/82/de/d9ac66b3e5ab3a891c2ac1f10f4babf959a536d783c5b495c432410fe1c7/novamud-0.0.8.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "49d21c552becf47fca89adf806366e35", "sha256": "234555ab848dab4919433ec7d24d7b3259094c695cd9af5304a8bd8fba7fffac" }, "downloads": -1, "filename": "novamud-0.0.8.tar.gz", "has_sig": false, "md5_digest": "49d21c552becf47fca89adf806366e35", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18659, "upload_time": "2018-10-26T10:13:54", "url": "https://files.pythonhosted.org/packages/82/de/d9ac66b3e5ab3a891c2ac1f10f4babf959a536d783c5b495c432410fe1c7/novamud-0.0.8.tar.gz" } ] }