From dd2128beb11cd1e32f555b2923f6a7024879851d Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Thu, 18 Nov 2021 11:31:21 -0800 Subject: [PATCH] Initial commit with a mostly working engine. Basic commands are being parsed. I think the only weird part is the 'use' command because it needs to possibly target two things. A tiny test example is provided in __main__, this will probably be broken out later. Signed-off-by: Alek Ratzloff --- .gitignore | 190 ++++++++++++++++++++ .pylintrc | 3 + .vscode/settings.json | 7 + Pipfile | 18 ++ Pipfile.lock | 338 ++++++++++++++++++++++++++++++++++++ agame/__init__.py | 0 agame/__main__.py | 115 ++++++++++++ agame/action.py | 294 +++++++++++++++++++++++++++++++ agame/color.py | 39 +++++ agame/game.py | 113 ++++++++++++ agame/item.py | 131 ++++++++++++++ agame/room.py | 46 +++++ agame/trigger.py | 247 ++++++++++++++++++++++++++ agame/util.py | 11 ++ tests/test_trigger_regex.py | 98 +++++++++++ 15 files changed, 1650 insertions(+) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 .vscode/settings.json create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 agame/__init__.py create mode 100644 agame/__main__.py create mode 100644 agame/action.py create mode 100644 agame/color.py create mode 100644 agame/game.py create mode 100644 agame/item.py create mode 100644 agame/room.py create mode 100644 agame/trigger.py create mode 100644 agame/util.py create mode 100644 tests/test_trigger_regex.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..795d332 --- /dev/null +++ b/.gitignore @@ -0,0 +1,190 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/vim,python +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/vim,python + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +!.vscode/*.code-snippets + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..569c68e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,3 @@ +[MASTER] +disable=wildcard-import, + unused-wildcard-import \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..099d123 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.pythonPath": "/home/alek/.local/share/virtualenvs/adventuregame-Kcj1_Ep-/bin/python", + "python.languageServer": "Jedi", + "python.linting.mypyEnabled": true +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e49e517 --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "==21.9b0" +mypy = "*" +pylint = "*" +pytest = "*" + +[requires] +python_version = "3.9" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..71deef6 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,338 @@ +{ + "_meta": { + "hash": { + "sha256": "2782f015f061a958c6a0bbb02c739c5a6967777ccfa578d2a8de9b996dcd0ff4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:11f7356737b624c42e21e71fe85eea6875cb94c03c82ac76bd535a0ff10b0f25", + "sha256:abc423a1e85bc1553954a14f2053473d2b7f8baf32eae62a328be24f436b5107" + ], + "markers": "python_version ~= '3.6'", + "version": "==2.8.5" + }, + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115", + "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91" + ], + "index": "pypi", + "version": "==21.9b0" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "isort": { + "hashes": [ + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "version": "==5.10.1" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", + "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", + "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", + "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", + "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", + "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", + "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", + "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", + "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", + "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", + "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", + "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", + "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", + "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", + "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", + "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", + "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", + "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", + "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", + "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", + "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", + "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.6.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mypy": { + "hashes": [ + "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", + "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", + "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", + "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", + "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", + "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", + "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", + "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", + "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", + "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", + "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", + "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", + "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", + "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", + "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", + "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", + "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", + "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", + "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", + "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", + "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", + "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", + "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" + ], + "index": "pypi", + "version": "==0.910" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "packaging": { + "hashes": [ + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" + ], + "markers": "python_version >= '3.6'", + "version": "==21.2" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "platformdirs": { + "hashes": [ + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pylint": { + "hashes": [ + "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126", + "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436" + ], + "index": "pypi", + "version": "==2.11.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "regex": { + "hashes": [ + "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", + "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", + "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", + "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", + "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", + "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", + "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", + "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", + "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", + "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", + "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", + "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", + "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", + "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", + "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", + "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", + "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", + "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", + "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", + "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", + "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", + "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", + "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", + "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", + "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", + "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", + "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", + "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", + "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", + "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", + "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", + "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", + "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", + "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", + "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", + "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", + "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", + "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", + "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", + "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", + "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", + "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", + "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", + "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", + "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", + "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", + "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", + "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", + "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" + ], + "version": "==2021.11.10" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" + ], + "markers": "python_version < '3.10'", + "version": "==3.10.0.2" + }, + "wrapt": { + "hashes": [ + "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179", + "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096", + "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374", + "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df", + "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185", + "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785", + "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7", + "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909", + "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918", + "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33", + "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068", + "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829", + "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af", + "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79", + "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce", + "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc", + "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36", + "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade", + "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca", + "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32", + "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125", + "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e", + "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709", + "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f", + "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b", + "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb", + "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb", + "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489", + "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640", + "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb", + "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851", + "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d", + "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44", + "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13", + "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2", + "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb", + "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b", + "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9", + "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755", + "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c", + "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a", + "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf", + "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3", + "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229", + "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e", + "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de", + "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554", + "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10", + "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80", + "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", + "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.13.3" + } + } +} diff --git a/agame/__init__.py b/agame/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agame/__main__.py b/agame/__main__.py new file mode 100644 index 0000000..d428468 --- /dev/null +++ b/agame/__main__.py @@ -0,0 +1,115 @@ +from agame.action import * +from agame.game import * +from agame.item import * +from agame.room import * +from agame.trigger import * + + +# TODO - take a custom module name that has: +# .database +# .game +# so that you can just RUN it +# Something similar to wsgi using "app" in the passed module + + +# This is the *game* here +database = Database() +database.add_items( + Item( + id="glowing_rock", + name="glowing rock", + desc="This rock is glowing.", + synonyms=("rock",), + room_desc="You see a ((glowing rock)). You have **got** to have it.", + triggers={ + GET: [ + PrintAction( + "You try to pick up the rock, but it slips out of your greasy hands.", + "Maybe you should wash your hands, you disgusting little man.", + ) + ], + LOOK: [PrintAction("Man, that rock looks awesome.")], + }, + ), + Item( + id="cell_door", + name="door", + room_desc="A ((door)) sits on the far wall.", + triggers={ + GET: [PrintAction("The door is pretty attached to its wall.")], + OPEN: [ + CheckVarAction( + "cell_door_open", + Compare.EQUALS, + True, + yes=[ + PrintAction( + "It's already open. You push on the door even //more//, just in case." + ), + SleepAction(1.0), + PrintAction("..."), + SleepAction(1.0), + PrintAction("Yup, still open."), + ], + no=[ + SetVarAction("cell_door_open", True), + PrintAction("The door swings open, thanks to you."), + ], + ) + ], + CLOSE: [ + CheckVarAction( + "cell_door_open", + Compare.EQUALS, + True, + yes=[ + PrintAction("You close that door. Nice job."), + SetVarAction("cell_door_open", False), + ], + no=[PrintAction("The door is already closed.")], + ) + ], + LOOK: [ + CheckVarAction( + "cell_door_open", + Compare.EQUALS, + True, + yes=[PrintAction("It's a door, wide open, because you opened it.")], + no=[PrintAction("A closed door. You can change this.")], + ) + ], + }, + ), +) +database.add_rooms( + Room( + id="start", + name="Test room", + desc="You're in ((Todd's Test Cell)).", + items=[ + database.items["glowing_rock"].create_inst(), + database.items["cell_door"].create_inst(), + ], + ), +) + +# Build the game state +game = Game( + database=database, + room=database.rooms["start"], + vars={ + "cell_door_open": False, + }, +) + +game.print_room() +while True: + try: + game.say() + line = input("> ") + game.say() + game.run_command(line) + except (KeyboardInterrupt, EOFError): + game.say() + game.say("Bye.") + break diff --git a/agame/action.py b/agame/action.py new file mode 100644 index 0000000..89fd7da --- /dev/null +++ b/agame/action.py @@ -0,0 +1,294 @@ +import dataclasses +import time +from typing import Any, Optional, Sequence, Union, TYPE_CHECKING +import enum + +if TYPE_CHECKING: + from agame.game import Game + + +__all__ = ( + "Action", + "SleepAction", + "PrintAction", + "TeleportAction", + "GetAction", + "CheckInvItemsAction", + "CheckRoomItemsAction", + "Compare", + "Var", + "CheckVarAction", + "SetVarAction", +) + + +class Action: + """ + An action that the game engine can take. + + This is the base class. It can be instantiated as a "dummy" action if + needed. + """ + + def act(self, game: "Game"): + """ + Complete this action. + """ + + +@dataclasses.dataclass +class SleepAction(Action): + """ + An action that delays the amount of time, in seconds. Decimal values are + allowed. + """ + + secs: float + + def act(self, game: "Game"): + time.sleep(self.secs) + + +@dataclasses.dataclass +class PrintAction(Action): + """ + Prints a message to the screen. + """ + + lines: Sequence[str] + + def __init__(self, *lines: str): + self.lines = lines + + def act(self, game: "Game"): + if not self.lines: + return + line = self.lines[0] + game.say(line) + for line in self.lines[1:]: + game.say() + game.say(line) + + +@dataclasses.dataclass +class TeleportAction(Action): + """ + Moves the player to another room. + """ + + room_id: str + + def act(self, game: "Game"): + # TODO + raise NotImplementedError("TODO - implement teleport action") + + +@dataclasses.dataclass +class GetAction(Action): + """ + Removes an item from the current room and puts it in the player's inventory. + """ + + item_id: str + pickup_text: Optional[str] = None + + # def __init__(self, item_id: str, pickup_text: ) + + def act(self, game: "Game"): + # Find the first instance of the item in the room and remove it + item = game.room.remove(self.item_id) + assert ( + item is not None + ), f"attempted to remove an item (id: {self.item_id}) that does not exist in the current room; this is a game logic error/bug" + + if item.id in game.inventory: + # Item in inventory already? Update count + game.inventory[item.id].count += item.count + else: + # Otherwise just add it + game.inventory[item.id] = item + + # Print text + if self.pickup_text is None: + game.say(f"You pick up (({item.name})).") + else: + game.say(self.pickup_text) + + +@dataclasses.dataclass +class CheckInvItemsAction(Action): + """ + Checks if all supplied items are present in the inventory, and executes the + appropriate action sequence. + """ + + item_ids: Union[str, Sequence[str]] + yes: Sequence[Action] + no: Sequence[Action] + + def act(self, game: "Game"): + # If the item_ids are just a string, use that as a list. + items = [self.item_ids] if isinstance(self.item_ids, str) else self.item_ids + for item_id in items: + if item_id not in game.inventory: + game.do_actions(self.no) + return + game.do_actions(self.yes) + + +@dataclasses.dataclass +class CheckRoomItemsAction(Action): + """ + Checks if all supplied items are present in the current room, and executes the + appropriate action sequence. + """ + + item_ids: str + yes: Sequence[Action] + no: Sequence[Action] + + def act(self, game: "Game"): + items = [self.item_ids] if isinstance(self.item_ids, str) else self.item_ids + for item_id in items: + if item_id not in game.room.items: + game.do_actions(self.no) + return + game.do_actions(self.yes) + + +class Compare(enum.Enum): + """ + A comparison for a value. + """ + + # Does what it says on the tin. + # + # This is type-sensitive and literally just does `==` in Python. Seriously. + EQUALS = enum.auto() + + # Also does what it says on the tin. + # + # This is type-sensitive and literally just does `!=` in Python. Seriously. + NOT_EQUALS = enum.auto() + + # Checks if a value is less than to another value. + # + # This uses the `cmp` to check the values. + LESS_THAN = enum.auto() + + # Checks if a value is less than or equal to another value. + # + # This uses the `cmp` to check the values. + LESS_THAN_EQUALS = enum.auto() + + # Checks if a value is greater than to another value. + # + # This uses the `cmp` to check the values. + GREATER_THAN = enum.auto() + + # Checks if a value is greater than or equal to another value. + # + # This uses the `cmp` to check the values. + GREATER_THAN_EQUALS = enum.auto() + + # Checks if the string-ified version of the value matches the other value, + # as a regex. + MATCHES = enum.auto() + + +@dataclasses.dataclass +class Var: + id: str + + +@dataclasses.dataclass +class CheckVarAction(Action): + """ + Check a variable's value using some kind of comparison. + + If you want to check one variable against another, use the `action.Var` + type. + """ + + # The variable to check the value of. + var_id: str + + # The comparison operation to use. See `Compare` for comparisons. + compare: Compare + + # The constant value to check against. + against: Any + + # The action to execute when this is true. + yes: Sequence[Action] + + # The action to execute when this is false. + no: Sequence[Action] + + def act(self, game: "Game"): + val = game.vars.get(self.var_id, None) + compare_val = None + if isinstance(self.against, Var): + compare_val = game.vars.get(self.against.id, None) + else: + compare_val = self.against + + result = False + if self.compare == Compare.EQUALS: + result = val == compare_val + elif self.compare == Compare.NOT_EQUALS: + result = val != compare_val + elif self.compare == Compare.LESS_THAN: + if isinstance(val, str) or isinstance(compare_val, str): + result = str(val) < str(compare_val) + elif (isinstance(val, int) or isinstance(val, float)) and ( + isinstance(compare_val, int) or isinstance(compare_val, float) + ): + result = val < compare_val + elif self.compare == Compare.LESS_THAN_EQUALS: + if isinstance(val, str) or isinstance(compare_val, str): + result = str(val) <= str(compare_val) + elif (isinstance(val, int) or isinstance(val, float)) and ( + isinstance(compare_val, int) or isinstance(compare_val, float) + ): + result = val <= compare_val + elif self.compare == Compare.GREATER_THAN: + if isinstance(val, str) or isinstance(compare_val, str): + result = str(val) > str(compare_val) + elif (isinstance(val, int) or isinstance(val, float)) and ( + isinstance(compare_val, int) or isinstance(compare_val, float) + ): + result = val > compare_val + elif self.compare == Compare.GREATER_THAN_EQUALS: + if isinstance(val, str) or isinstance(compare_val, str): + result = str(val) >= str(compare_val) + elif (isinstance(val, int) or isinstance(val, float)) and ( + isinstance(compare_val, int) or isinstance(compare_val, float) + ): + result = val >= compare_val + elif self.compare == Compare.MATCHES: + pass + else: + assert False, f"{self.compare} isn't a Compare value, ya doink" + + # Check result, do action + if result: + game.do_actions(self.yes) + else: + game.do_actions(self.no) + + +@dataclasses.dataclass +class SetVarAction(Action): + """ + Set a variable to a specific value. + """ + + var_id: str + value: Any + + def act(self, game: "Game"): + value = ( + game.vars.get(self.value.id) if isinstance(self.value, Var) else self.value + ) + game.vars[self.var_id] = value diff --git a/agame/color.py b/agame/color.py new file mode 100644 index 0000000..2d99843 --- /dev/null +++ b/agame/color.py @@ -0,0 +1,39 @@ +import re + + +__all__ = ("colorize",) + +BOLD_PAT = re.compile(r"\*\*(.+?)\*\*", re.MULTILINE) +ITALIC_PAT = re.compile(r"//(.+?)//", re.MULTILINE) +INTEREST_PAT = re.compile(r"\(\((.+?)\)\)", re.MULTILINE) +SHADOW_PAT = re.compile(r"\{\{(.+?)\}\}", re.MULTILINE) + +BOLD_COL = "\u001b[1m" +ITALIC_COL = "\u001b[3m" +INTEREST_COL = "\u001b[34;1m" +SHADOW_COL = "\u001b[30;1m" +RESET_COL = "\u001b[0m" + + +def colorize(text: str) -> str: + """ + Colorizes text for output on an ANSI terminal. + + This will use escape codes to replace things. + + Style guide: + ((This)) is "interest" styling. This will make the text blue. + {{This}} is "shadow" styling. This will make the text a dark grey (or at + least, more subtle.) + """ + replacements = [ + (INTEREST_PAT, INTEREST_COL), + (SHADOW_PAT, SHADOW_COL), + (BOLD_PAT, BOLD_COL), + (ITALIC_PAT, ITALIC_COL), + ] + + for (pat, col) in replacements: + text = pat.sub(col + r"\1" + RESET_COL, text) + + return text diff --git a/agame/game.py b/agame/game.py new file mode 100644 index 0000000..7416706 --- /dev/null +++ b/agame/game.py @@ -0,0 +1,113 @@ +import dataclasses +import textwrap +from typing import Any, MutableMapping, Match, Optional, Sequence +from agame.action import Action +from agame.color import colorize +from agame.item import Item, ItemInst +from agame.room import Room +from agame.trigger import * + + +__all__ = ( + "Database", + "Game", +) + + +@dataclasses.dataclass +class Database: + # All items available to the game. + items: MutableMapping[str, Item] = dataclasses.field(default_factory=dict) + # All rooms available to the game. + rooms: MutableMapping[str, Room] = dataclasses.field(default_factory=dict) + + def add_item(self, item: Item): + self.items[item.id] = item + + def add_items(self, *items: Item): + for item in items: + self.add_item(item) + + def add_room(self, room: Room): + self.rooms[room.id] = room + + def add_rooms(self, *rooms: Room): + for room in rooms: + self.add_room(room) + + +@dataclasses.dataclass +class Game: + # Game room/items database + database: Database + # Current room. + room: Room + # Player inventory. + inventory: MutableMapping[str, ItemInst] = dataclasses.field(default_factory=dict) + # Variables. + vars: MutableMapping[str, Any] = dataclasses.field(default_factory=dict) + + def run_command(self, line: str): + line = line.strip() + if not line: + return + + triggers: Sequence[Trigger] = [ + GetTrigger(), + UseTrigger(), + PutTrigger(), + LookTrigger(), + OpenTrigger(), + CloseTrigger(), + GoTrigger(), + ] + + trigger = None + match: Optional[Match] = None + for t in triggers: + match = t.pattern().fullmatch(line) + if match: + trigger = t + break + if not trigger: + self.say("I'm not sure what you mean.") + return + + assert match, "why were no patterns matched?" + trigger.trigger(self, match) + + def do_actions(self, actions: Sequence[Action]): + "Executes the supplied actions." + for action in actions: + action.act(self) + + def print_room(self): + "Prints this room's description." + self.say(self.room.name) + self.say() + self.say(self.room.desc) + # Look at revealed text + for item in self.room.items.values(): + if not item.revealed or item.room_desc is None: + continue + if item.room_desc == "": + # TODO - pluralization, 'a' vs 'an' + self.say(f"You see a (({item.name})).") + else: + self.say(item.room_desc) + + def say(self, message: Optional[str] = None): + "Format, colorize, wrap, and print the message." + message = message or "" + message = textwrap.fill(message) + print(colorize(message)) + + @property + def rooms(self): + "Shortcut property for `game.database.rooms`." + return self.database.rooms + + @property + def items(self): + "Shortcut property for `game.database.items`." + return self.database.items diff --git a/agame/item.py b/agame/item.py new file mode 100644 index 0000000..8bf002d --- /dev/null +++ b/agame/item.py @@ -0,0 +1,131 @@ +import dataclasses +from typing import Mapping, Optional, Sequence +from agame.action import Action + + +__all__ = ( + "ItemInst", + "Item", +) + + +@dataclasses.dataclass +class ItemInst: + """ + An instance of an item in the game. + """ + + # Reference to the global item that this is an instance of. + item: "Item" + + # Gets whether this item can be taken. + fixed: bool = False + + # Gets how many of this item instance are present in this stack. + count: int = 1 + + # Gets whether this item is revealed or not. + revealed: bool = True + + @property + def id(self) -> str: + return self.item.id + + @property + def name(self) -> str: + return self.item.name + + @property + def desc(self) -> Optional[str]: + return self.item.desc + + @property + def synonyms(self) -> Sequence[str]: + return self.item.synonyms + + @property + def room_desc(self) -> Optional[str]: + return self.item.room_desc + + @property + def triggers(self) -> Mapping[str, Sequence[Action]]: + return self.item.triggers + + @property + def use_actions(self) -> Mapping[str, Sequence[Action]]: + return self.item.use_actions + + +@dataclasses.dataclass +class Item: + """ + A game item. + """ + + # The ID of this item. This is how items are looked up in the game. + id: str + + # The printable name of this item. + name: str + + # A long description for this item. + desc: Optional[str] = None + + # A list of all synonyms for this item. + synonyms: Sequence[str] = dataclasses.field(default_factory=list) + + # The description that is used in the context of a room's `look` command. + # + # When someone wants to look at the entire room, all items that have been + # revealed will also be displayed. + # + # If you want to disable this behavior entirely, `room_desc` should be set + # to `None`. + # + # If you want to use the default text, "You see a (({item.name}))", + # `room_desc` should be set to the blank string, `""`. + # + # Otherwise, the `room_desc` string will be colorized and printed as + # written. + room_desc: Optional[str] = None + + # A list of triggers that a game may use. Since this is just a mapping of + # strings to action sequences, only one set of actions is allowed per + # trigger. + # + # Valid triggers include: + # * get + # * use + # * put + # * look + # * open + # * close + # ...more to come + triggers: Mapping[str, Sequence[Action]] = dataclasses.field(default_factory=dict) + + # A mapping of other items that this item may be used with, specifically on + # the USE command. + # + # USE is a strange beast, because instead of just one implicit target (which + # is verified to exist by the trigger, of all things) we have *two* targets: + # the subject and the direct object. And not always! + # + # For example, we have an item, paintbrush. These are some options: + # use paintbrush # <- on what? + # use paintbrush on canvas # <- you paint a beautiful masterpiece. + # use paintbrush on car # <- that's not allowed (custom text) + # use paintbrush on fake item # <- that item doesn't exist + # + # We can't really represent this with the current str -> sequence[action] + # stuff we have in place right now, so a special field will be good enough + # until a more insane/robust solution is implemented. + use_actions: Mapping[str, Sequence[Action]] = dataclasses.field( + default_factory=dict + ) + + def create_inst(self, *args, **kwargs): + """ + Creates a new item instance, passing the supplied args and kwargs to the + ItemInst constructor. + """ + return ItemInst(item=self, *args, **kwargs) diff --git a/agame/room.py b/agame/room.py new file mode 100644 index 0000000..bd146f1 --- /dev/null +++ b/agame/room.py @@ -0,0 +1,46 @@ +import dataclasses +from typing import MutableMapping, Optional, Sequence, Union, TYPE_CHECKING +from agame.item import ItemInst +from agame.util import search_item_name + + +__all__ = ("Room",) + + +@dataclasses.dataclass +class Room: + id: str + name: str + desc: str + items: MutableMapping[str, ItemInst] + + def __init__( + self, + id: str, + name: str, + desc: str, + items: Union[Sequence[ItemInst], MutableMapping[str, ItemInst]], + ): + self.id = id + self.name = name + self.desc = desc + if isinstance(items, MutableMapping): + self.items = items + else: + self.items = {item.id: item for item in items} + + def search_item_name(self, item_name: str) -> Optional[ItemInst]: + """ + Searches all item instances in the room for the given item name, also + checking synonyms. Returns the first item instance found, or none if no + synonyms or names were found to match. + """ + return search_item_name(self.items.values(), item_name) + + def remove(self, item_id: str) -> Optional[ItemInst]: + """ + Removes an item with the given ID from the room, returning it. + + If it's not present in the room, `None` is returned. + """ + return self.items.pop(item_id, None) diff --git a/agame/trigger.py b/agame/trigger.py new file mode 100644 index 0000000..233dfb9 --- /dev/null +++ b/agame/trigger.py @@ -0,0 +1,247 @@ +import abc +import re +from typing import Match, Pattern, TYPE_CHECKING +from agame.util import search_item_name + +if TYPE_CHECKING: + from agame.game import Game + + +# Triggers: +# get/take/grab/pick up [a[n]/the] x +# use [a[n]/the] x [with/on [a[n]/the] y] +# put [a[n]/the] x on/in [a[n]/the] y +# look [[at] [a[n]/the] x] +# open x +# close x +# go/(go to)/leave/exit x +# give [a[n]/the] x to [a[n]/the] y +# push [a[n]/the] x +# pull [a[n]/the] x +# (put down)/drop [a[n]/the] x +__all__ = ( + "Trigger", + "GetTrigger", + "UseTrigger", + "PutTrigger", + "LookTrigger", + "OpenTrigger", + "CloseTrigger", + "GoTrigger", + "GET", + "USE", + "PUT", + "LOOK", + "OPEN", + "CLOSE", + "GO", +) +GET = "get" +USE = "use" +PUT = "put" +LOOK = "look" +OPEN = "open" +CLOSE = "close" +GO = "go" + + +class Trigger(metaclass=abc.ABCMeta): + @staticmethod + @abc.abstractmethod + def pattern() -> Pattern: + pass + + @abc.abstractmethod + def trigger(self, game: "Game", match: Match): + pass + + +class GetTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Pget|take|grab|pick[ ]*up) + (([ ]+(an?|the))?[ ]+(?P.+))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + otrigger = match["trigger"].lower().capitalize() + game.say(f"{otrigger} what?") + return + item = game.room.search_item_name(item_name.lower()) + if item and GET in item.triggers: + actions = item.triggers[GET] + # if there are any actions, do them. else do the default action + game.do_actions(actions) + else: + game.say("Can't get that.") + + +class UseTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + # TODO(low) - wouldn't it be cool to specify "use" actions? + # e.g. you have a gun item and you want to be allowed to use "shoot" in order to + # use the gun. + return re.compile( + r""" + (?Puse) + (([ ]+(an?|the))?[ ]+(?P.+?) + ([ ]+(with|on)([ ]+(an?|the))?[ ]+(?P.+))?)? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + game.say("Use what?") + return + target_name = match["target"] + + # Get the item from inventory or room + item = game.room.search_item_name(item_name) or search_item_name( + game.inventory.values(), item_name.lower() + ) + if not item: + game.say(f"I'm not sure what you mean by {item_name}.") + return + + target = None + if target_name: + # Get the target from inventory or room + target = game.room.search_item_name(target_name) or search_item_name( + game.inventory.values(), target_name.lower() + ) + if not target: + game.say(f"I'm not sure what you mean by {target_name}.") + # Check if the target can be used on something + elif target.id in item.use_actions: + game.do_actions(item.use_actions[target.id]) + else: + game.say("I'm not sure how to do that.") + elif USE in item.triggers: + # Check if the item can be used by itself + game.do_actions(item.triggers[USE]) + elif item.use_actions: + # This item can be used with *something*, but we don't know what. + game.say(f"Use (({item_name})) with what?") + else: + # This can't be used. + game.say("I can't really use that.") + + +class PutTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Pput) + ([ ]+((an?|the)[ ]+)?(?P.+?) + ((on|in)[ ]+)?((an?|the)[ ]+)?[ ]+(?P.+))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + + +class LookTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Plook) + ([ ]+(at[ ]+)?((an?|the)[ ]+)?(?P.+?))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + game.print_room() + return + item = game.room.search_item_name(item_name.lower()) + if item and LOOK in item.triggers: + actions = item.triggers[LOOK] + game.do_actions(actions) + else: + game.say("Can't see that.") + + +class OpenTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Popen) + (((an?|the)[ ]+)?[ ]+(?P.+?))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + game.say("Open what?") + return + item = game.room.search_item_name(item_name.lower()) + if item and OPEN in item.triggers: + actions = item.triggers[OPEN] + game.do_actions(actions) + else: + game.say("Can't open that.") + + +class CloseTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Pclose) + (((an?|the)[ ]+)?[ ]+(?P.+?))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + game.say("Close what?") + return + item = game.room.search_item_name(item_name.lower()) + if item and CLOSE in item.triggers: + actions = item.triggers[CLOSE] + game.do_actions(actions) + else: + game.say("Can't close that.") + + +class GoTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Pgo|go[ ]*to|leave|exit) + (((an?|the)[ ]+)?[ ]+(?P.+?))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + otrigger = match["trigger"].lower().capitalize() + game.say(f"{otrigger} where?") + return + item = game.room.search_item_name(item_name.lower()) + if item and GO in item.triggers: + actions = item.triggers[GO] + game.do_actions(actions) diff --git a/agame/util.py b/agame/util.py new file mode 100644 index 0000000..02c031d --- /dev/null +++ b/agame/util.py @@ -0,0 +1,11 @@ +from typing import Iterable, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from agame.item import ItemInst + + +def search_item_name(seq: Iterable["ItemInst"], item_name: str) -> Optional["ItemInst"]: + for item in seq: + if item.name == item_name or item_name in item.synonyms: + return item + return None diff --git a/tests/test_trigger_regex.py b/tests/test_trigger_regex.py new file mode 100644 index 0000000..3e8a135 --- /dev/null +++ b/tests/test_trigger_regex.py @@ -0,0 +1,98 @@ +from agame.trigger import * + + +def test_get_item(): + cases = [ + ("get item", "item"), + ("take item", "item"), + ("grab item", "item"), + ("pick up item", "item"), + ("pickup item", "item"), + ("get the item", "item"), + ("take the item", "item"), + ("grab the item", "item"), + ("pick up the item", "item"), + ("pickup the item", "item"), + ("get a item", "item"), + ("take a item", "item"), + ("grab a item", "item"), + ("pick up a item", "item"), + ("pickup a item", "item"), + ("get an item", "item"), + ("take an item", "item"), + ("grab an item", "item"), + ("pick up an item", "item"), + ("pickup an item", "item"), + ("get item", "item"), + ("take item", "item"), + ("grab item", "item"), + ("pick up item", "item"), + ("pickup item", "item"), + ("get the item", "item"), + ("take the item", "item"), + ("grab the item", "item"), + ("pick up the item", "item"), + ("pickup the item", "item"), + ("get a item", "item"), + ("take a item", "item"), + ("grab a item", "item"), + ("pick up a item", "item"), + ("pickup a item", "item"), + ("get an item", "item"), + ("take an item", "item"), + ("grab an item", "item"), + ("pick up an item", "item"), + ("pickup an item", "item"), + ] + + pat = GetTrigger.pattern() + for (line, expected) in cases: + line = line.strip() + match = pat.fullmatch(line) + assert match is not None + assert match["item"] == expected + + +def test_use_item(): + cases = [ + ("use item", "item"), + ("use the item", "item"), + ("use a item", "item"), + ("use an item", "item"), + ("use item", "item"), + ("use the item", "item"), + ("use a item", "item"), + ("use an item", "item"), + ("use an item", "item"), + ] + + pat = UseTrigger.pattern() + for (line, expected) in cases: + line = line.strip() + match = pat.fullmatch(line) + assert match is not None + assert match["item"] == expected + + +def test_use_item_with(): + cases = [ + ("use item with other item", "item", "other item"), + ("use the item with an other item", "item", "other item"), + ("use a item with a other item", "item", "other item"), + ("use an item with the other item", "item", "other item"), + ("use item with other item", "item", "other item"), + ("use the item with an other item", "item", "other item"), + ("use a item with a other item", "item", "other item"), + ("use an item with the other item", "item", "other item"), + ] + + pat = UseTrigger.pattern() + for (line, item, other_item) in cases: + match = pat.fullmatch(line) + assert match is not None + assert match["item"] == item + assert match["target"] == other_item + + +# TODO : put +# TODO : look