├── README ├── elide ├── LICENSE ├── elide │ ├── stepper.kv │ ├── grid │ │ ├── __init__.py │ │ └── board.kv │ ├── examples │ │ ├── __init__.py │ │ └── sugarscape_immediate_growback.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_resources.py │ │ ├── test_sprite_builder.py │ │ ├── test_strings_editor.py │ │ ├── test_python_editor.py │ │ ├── util.py │ │ ├── test_character_switcher.py │ │ └── test_gridboard.py │ ├── kivygarden │ │ ├── __init__.py │ │ └── texturestack │ │ │ ├── README.md │ │ │ └── LICENSE │ ├── assets │ │ ├── next.png │ │ ├── right.png │ │ ├── Symbola.ttf │ │ ├── polygons-0.png │ │ ├── parchmentBasic.png │ │ ├── rltiles │ │ │ ├── arm-0.png │ │ │ ├── armor-0.png │ │ │ ├── base-0.png │ │ │ ├── beard-0.png │ │ │ ├── body-0.png │ │ │ ├── book-0.png │ │ │ ├── boot-0.png │ │ │ ├── cloak-0.png │ │ │ ├── floor-0.png │ │ │ ├── food-0.png │ │ │ ├── gem-0.png │ │ │ ├── hair-0.png │ │ │ ├── hand1-0.png │ │ │ ├── hand2-0.png │ │ │ ├── head-0.png │ │ │ ├── leg-0.png │ │ │ ├── misc-0.png │ │ │ ├── ring-0.png │ │ │ ├── amulet-0.png │ │ │ ├── dc-mon-0.png │ │ │ ├── dungeon-0.png │ │ │ ├── potion-0.png │ │ │ ├── scroll-0.png │ │ │ ├── weapon-0.png │ │ │ ├── floor.atlas │ │ │ ├── cloak.atlas │ │ │ ├── beard.atlas │ │ │ ├── gem.atlas │ │ │ ├── boot.atlas │ │ │ ├── arm.atlas │ │ │ ├── hair.atlas │ │ │ ├── potion.atlas │ │ │ ├── leg.atlas │ │ │ ├── scroll.atlas │ │ │ ├── amulet.atlas │ │ │ ├── ring.atlas │ │ │ ├── book.atlas │ │ │ ├── food.atlas │ │ │ ├── misc.atlas │ │ │ ├── hand2.atlas │ │ │ ├── base.atlas │ │ │ ├── head.atlas │ │ │ ├── dungeon.atlas │ │ │ ├── body.atlas │ │ │ └── weapon.atlas │ │ ├── kenney1bit │ │ │ └── colored-transparent_packed.png │ │ └── polygons.atlas │ ├── dummy.kv │ ├── menu.kv │ ├── rulebooks.kv │ ├── pallet.kv │ ├── graph │ │ ├── board.kv │ │ ├── __init__.py │ │ └── pawn.py │ ├── rulebooks.py │ ├── logview.kv │ ├── gen.kv │ ├── timestream.kv │ ├── calendar.kv │ ├── charsview.kv │ ├── __main__.py │ ├── __init__.py │ ├── spritebuilder.kv │ ├── logview.py │ ├── boardview.py │ ├── gen.py │ ├── statcfg.kv │ ├── rulesview.kv │ ├── charmenu.kv │ ├── boardscatter.py │ ├── util.py │ ├── dummy.py │ ├── elide.kv │ ├── card.kv │ └── stepper.py ├── README.md ├── MANIFEST.in ├── tox.ini ├── TESTS └── pyproject.toml ├── lisien ├── lisien │ ├── py.typed │ ├── tests │ │ ├── data │ │ │ ├── sickle.lisien │ │ │ ├── big_grid.lisien │ │ │ ├── pathfind.lisien │ │ │ ├── college10.lisien │ │ │ ├── college24.lisien │ │ │ └── wolfsheep.lisien │ │ ├── remake_big_grid.py │ │ ├── __init__.py │ │ ├── remake_sickle.py │ │ ├── remake_pathfind.py │ │ ├── remake_wolfsheep.py │ │ ├── test_resume.py │ │ ├── test_performance.py │ │ ├── remake_college.py │ │ ├── regen_test_xml.py │ │ ├── test_msgpack.py │ │ ├── test_neighborhood.py │ │ ├── test_time_travel.py │ │ ├── test_thing.py │ │ ├── test_place.py │ │ ├── test_contents.py │ │ └── test_rule_poller.py │ ├── examples │ │ ├── export_examples.py │ │ ├── __init__.py │ │ ├── pathfind.py │ │ └── polygons.py │ ├── __init__.py │ ├── proxy │ │ ├── __main__.py │ │ ├── worker_subinterpreter.py │ │ └── __init__.py │ └── server │ │ ├── __main__.py │ │ └── __init__.py ├── LICENSE ├── README.md ├── MANIFEST.in ├── test_requirements.txt ├── tox_terp.ini ├── tox_slow.ini ├── tox.ini └── pyproject.toml ├── recipes ├── __init__.py ├── elide │ └── __init__.py ├── lisien │ └── __init__.py ├── androidstorage4kivy │ └── __init__.py ├── sqlalchemy │ ├── __init__.py │ ├── __init__.py.orig │ └── __init__.py.rej ├── msgpack │ └── __init__.py └── networkx │ ├── __init__.py │ └── patches │ ├── no-compress.patch │ └── no-tests.patch ├── _static └── custom.css ├── .coveragerc ├── ruff.toml ├── elide_icon ├── OS X │ └── ELiDE.icns ├── Windows │ └── ELiDE.ico ├── pngs │ ├── icon_144.png │ ├── icon_1024.png │ ├── icon_128px.png │ ├── icon_16px.png │ ├── icon_18px.png │ ├── icon_192px.png │ ├── icon_19px.png │ ├── icon_24px.png │ ├── icon_256px.png │ ├── icon_29px.png │ ├── icon_32px.png │ ├── icon_36px.png │ ├── icon_40px.png │ ├── icon_48px.png │ ├── icon_512px.png │ ├── icon_60px.png │ ├── icon_64px.png │ ├── icon_72px.png │ ├── icon_76px.png │ └── icon_96px.png ├── Linux │ ├── icon_16px.png │ ├── icon_24px.png │ ├── icon_48px.png │ └── icon_96px.png ├── favicon │ └── favicon.ico ├── iOS │ ├── icon_29x29.png │ ├── icon_40x40.png │ ├── icon_60x60.png │ ├── icon_76x76.png │ ├── icon_29x29@2x.png │ ├── icon_29x29@3x.png │ ├── icon_40x40@2x.png │ ├── icon_40x40@3x.png │ ├── icon_60x60@2x.png │ ├── icon_60x60@3x.png │ ├── icon_76x76@2x.png │ ├── icon_76x76@3x.png │ ├── icon_83.5x83.6@2x.png │ ├── itunes_icon_512x512.png │ ├── itunes_icon_1024x1024.png │ └── itunes_icon_2048x2048.png └── Android │ ├── icon_144.png │ ├── icon_16px.png │ ├── icon_192px.png │ ├── icon_24px.png │ ├── icon_32px.png │ ├── icon_36px.png │ ├── icon_48px.png │ ├── icon_512px.png │ ├── icon_64px.png │ ├── icon_72px.png │ └── icon_96px.png ├── .gitmodules ├── .editorconfig ├── requirements.txt ├── pytest.ini ├── pycharm_tox.ini ├── .gitignore ├── .woodpecker ├── subinterpreter-headless-tests.yaml ├── headless-tests.yaml └── slow-headless-tests.yaml ├── index.rst ├── find_ramdisk.py ├── pull_kivy_logs.py ├── butler.py ├── CONTRIBUTING.md ├── release.sh ├── main.py ├── check_version.py ├── setup.py └── updbuildozer.py /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /elide/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /elide/elide/stepper.kv: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lisien/lisien/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /recipes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elide/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /elide/elide/grid/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lisien/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /lisien/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /elide/elide/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elide/elide/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /elide/elide/kivygarden/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_static/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | tab-size: 4; 3 | } 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | /tmp/* 4 | 5 | [report] 6 | omit = 7 | /tmp/* 8 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 79 2 | target-version = "py310" 3 | 4 | [format] 5 | indent-style = "tab" 6 | -------------------------------------------------------------------------------- /lisien/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include lisien/py.typed 2 | include lisien/world.rng 3 | include lisien/tests/data/* 4 | -------------------------------------------------------------------------------- /elide/elide/assets/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/next.png -------------------------------------------------------------------------------- /elide/elide/assets/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/right.png -------------------------------------------------------------------------------- /elide_icon/OS X/ELiDE.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/OS X/ELiDE.icns -------------------------------------------------------------------------------- /elide_icon/Windows/ELiDE.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Windows/ELiDE.ico -------------------------------------------------------------------------------- /elide_icon/pngs/icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_144.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs"] 2 | path = docs 3 | url = https://codeberg.org/clayote/Lisien.git 4 | branch = pages 5 | -------------------------------------------------------------------------------- /elide/elide/assets/Symbola.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/Symbola.ttf -------------------------------------------------------------------------------- /elide_icon/Linux/icon_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Linux/icon_16px.png -------------------------------------------------------------------------------- /elide_icon/Linux/icon_24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Linux/icon_24px.png -------------------------------------------------------------------------------- /elide_icon/Linux/icon_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Linux/icon_48px.png -------------------------------------------------------------------------------- /elide_icon/Linux/icon_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Linux/icon_96px.png -------------------------------------------------------------------------------- /elide_icon/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/favicon/favicon.ico -------------------------------------------------------------------------------- /elide_icon/iOS/icon_29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_29x29.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_40x40.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_60x60.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_76x76.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_1024.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_128px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_16px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_18px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_18px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_192px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_192px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_19px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_19px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_24px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_256px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_29px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_29px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_32px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_36px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_36px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_40px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_48px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_512px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_60px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_60px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_64px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_72px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_72px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_76px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_76px.png -------------------------------------------------------------------------------- /elide_icon/pngs/icon_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/pngs/icon_96px.png -------------------------------------------------------------------------------- /elide/elide/assets/polygons-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/polygons-0.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_144.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_16px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_192px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_192px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_24px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_32px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_36px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_36px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_48px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_512px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_64px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_72px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_72px.png -------------------------------------------------------------------------------- /elide_icon/Android/icon_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/Android/icon_96px.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_29x29@2x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_29x29@3x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_40x40@2x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_40x40@3x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_60x60@2x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_60x60@3x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_76x76@2x.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_76x76@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_76x76@3x.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /elide/elide/assets/parchmentBasic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/parchmentBasic.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/arm-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/arm-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/armor-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/armor-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/base-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/base-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/beard-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/beard-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/body-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/body-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/book-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/book-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/boot-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/boot-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/cloak-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/cloak-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/floor-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/floor-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/food-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/food-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/gem-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/gem-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/hair-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/hair-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/hand1-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/hand1-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/hand2-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/hand2-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/head-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/head-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/leg-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/leg-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/misc-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/misc-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/ring-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/ring-0.png -------------------------------------------------------------------------------- /elide_icon/iOS/icon_83.5x83.6@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/icon_83.5x83.6@2x.png -------------------------------------------------------------------------------- /elide_icon/iOS/itunes_icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/itunes_icon_512x512.png -------------------------------------------------------------------------------- /lisien/lisien/tests/data/sickle.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/sickle.lisien -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/amulet-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/amulet-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/dc-mon-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/dc-mon-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/dungeon-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/dungeon-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/potion-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/potion-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/scroll-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/scroll-0.png -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/weapon-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/rltiles/weapon-0.png -------------------------------------------------------------------------------- /elide_icon/iOS/itunes_icon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/itunes_icon_1024x1024.png -------------------------------------------------------------------------------- /elide_icon/iOS/itunes_icon_2048x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide_icon/iOS/itunes_icon_2048x2048.png -------------------------------------------------------------------------------- /lisien/lisien/tests/data/big_grid.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/big_grid.lisien -------------------------------------------------------------------------------- /lisien/lisien/tests/data/pathfind.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/pathfind.lisien -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker>=1.4,<2 2 | msgpack>=1.0.0 3 | networkx>=2.4,<4 4 | sqlalchemy>=1.4,<3 5 | numpy>=1.19.5,<3 6 | tblib>=1.7.0,<2 7 | -------------------------------------------------------------------------------- /lisien/lisien/tests/data/college10.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/college10.lisien -------------------------------------------------------------------------------- /lisien/lisien/tests/data/college24.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/college24.lisien -------------------------------------------------------------------------------- /lisien/lisien/tests/data/wolfsheep.lisien: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/lisien/lisien/tests/data/wolfsheep.lisien -------------------------------------------------------------------------------- /elide/elide/assets/kenney1bit/colored-transparent_packed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TacticalMetaphysics/Lisien/HEAD/elide/elide/assets/kenney1bit/colored-transparent_packed.png -------------------------------------------------------------------------------- /recipes/elide/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import PyProjectRecipe 2 | 3 | 4 | class ElideRecipe(PyProjectRecipe): 5 | version = "0.20.0" 6 | 7 | 8 | recipe = ElideRecipe() 9 | -------------------------------------------------------------------------------- /recipes/lisien/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import PyProjectRecipe 2 | 3 | 4 | class LisienRecipe(PyProjectRecipe): 5 | version = "0.20.0" 6 | 7 | 8 | recipe = LisienRecipe() 9 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | slow: marks tests as slow (deselect with '-m "not slow"') 4 | big: marks tests as too big to diagnose specific bugs witt 5 | tmp_path_retention_policy = failed 6 | -------------------------------------------------------------------------------- /elide/elide/tests/test_resources.py: -------------------------------------------------------------------------------- 1 | from kivy.resources import resource_find 2 | 3 | import elide # may add resource paths at import time 4 | 5 | 6 | def test_elide_dot_kv(): 7 | assert resource_find("elide.kv") 8 | -------------------------------------------------------------------------------- /elide/elide/dummy.kv: -------------------------------------------------------------------------------- 1 | : 2 | name: "".join((self.prefix, str(self.num))) 3 | x_center_up: self.x_up + self.width / 2 4 | y_center_up: self.y_up + self.height / 2 5 | right_up: self.x_up + self.width 6 | top_up: self.y_up + self.height -------------------------------------------------------------------------------- /elide/elide/menu.kv: -------------------------------------------------------------------------------- 1 | : 2 | viewclass: 'Button' 3 | RecycleBoxLayout: 4 | default_size: None, dp(56) 5 | default_size_hint: 1, None 6 | height: self.minimum_height 7 | size_hint_y: None 8 | orientation: 'vertical' -------------------------------------------------------------------------------- /elide/elide/rulebooks.kv: -------------------------------------------------------------------------------- 1 | : 2 | BoxLayout: 3 | orientation: 'vertical' 4 | BoxLayout: 5 | orientation: 'horizontal' 6 | RecycleView: 7 | 8 | RecycleView: 9 | Button: 10 | text: 'Close' 11 | on_release: root.toggle() -------------------------------------------------------------------------------- /pycharm_tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310,py311,py312 3 | skipsdist = true 4 | 5 | [testenv] 6 | passenv = 7 | DISPLAY 8 | PYTHONPATH 9 | deps = 10 | pytest 11 | -r LiSE/test_requirements.txt 12 | kivy 13 | pygments 14 | commands = 15 | python -m pytest 16 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/floor.atlas: -------------------------------------------------------------------------------- 1 | {"floor-0.png": {"floor-indigo": [2, 94, 32, 32], "floor-lapis": [36, 94, 32, 32], "floor-moss": [70, 94, 32, 32], "floor-mucus": [104, 94, 32, 32], "floor-normal": [138, 94, 32, 32], "floor-stone": [172, 94, 32, 32], "floor-tunnel": [206, 94, 32, 32]}} -------------------------------------------------------------------------------- /elide/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include elide/assets/*.png 2 | include elide/assets/*.jpg 3 | include elide/assets/*.ttf 4 | include elide/assets/*.atlas 5 | include elide/assets/rltiles/* 6 | include elide/assets/kenney1bit/* 7 | include elide/*.kv 8 | include elide/graph/*.kv 9 | include elide/grid/*.kv 10 | -------------------------------------------------------------------------------- /elide/elide/grid/board.kv: -------------------------------------------------------------------------------- 1 | : 2 | app: app 3 | size_hint: None, None 4 | : 5 | plane: boardplane 6 | GridBoardScatterPlane: 7 | id: boardplane 8 | board: root.board 9 | scale_min: root.scale_min 10 | scale_max: root.scale_max 11 | pos: root.pos 12 | size: root.size -------------------------------------------------------------------------------- /recipes/androidstorage4kivy/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import PyProjectRecipe 2 | 3 | 4 | class AndroidStorage4KivyRecipe(PyProjectRecipe): 5 | version = "0.1.2a" 6 | url = "https://github.com/clayote/androidstorage4kivy/archive/refs/heads/main.zip" 7 | 8 | 9 | recipe = AndroidStorage4KivyRecipe() 10 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/cloak.atlas: -------------------------------------------------------------------------------- 1 | {"cloak-0.png": {"blue": [36, 478, 32, 32], "brown": [70, 478, 32, 32], "gray": [138, 478, 32, 32], "yellow": [308, 478, 32, 32], "black": [2, 478, 32, 32], "cyan": [104, 478, 32, 32], "green": [172, 478, 32, 32], "magenta": [206, 478, 32, 32], "white": [274, 478, 32, 32], "red": [240, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/polygons.atlas: -------------------------------------------------------------------------------- 1 | {"polygons-0.png": {"yay_square_blink": [104, 146, 100, 100], "yay_triangle_blink": [2, 44, 100, 100], "sad_square": [206, 248, 100, 100], "meh_square": [2, 248, 100, 100], "sad_triangle": [2, 146, 100, 100], "yay_triangle": [104, 44, 100, 100], "meh_triangle": [104, 248, 100, 100], "yay_square": [206, 146, 100, 100]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/beard.atlas: -------------------------------------------------------------------------------- 1 | {"beard-0.png": {"short_red": [206, 478, 32, 32], "short_white": [240, 478, 32, 32], "short_yellow": [274, 478, 32, 32], "long_white": [70, 478, 32, 32], "short_black": [172, 478, 32, 32], "long_black": [2, 478, 32, 32], "long_red": [36, 478, 32, 32], "long_yellow": [104, 478, 32, 32], "pj": [138, 478, 32, 32]}} -------------------------------------------------------------------------------- /lisien/test_requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types>=0.7.0 2 | astor~=0.8.1 3 | blinker>=1.4,<2 4 | msgpack>=1.0.0 5 | networkx>=2.4,<4 6 | sqlalchemy>=1.4,<3 7 | numpy>=1.19.5,<3 8 | tblib>=1.7.0,<2 9 | lxml~=6.0.1 10 | build 11 | twine 12 | pytest 13 | tox 14 | kivy 15 | kivy_garden.collider @ git+https://github.com/kivy-garden/collider.git@804cf93 16 | pygments 17 | -------------------------------------------------------------------------------- /lisien/tox_terp.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py314 4 | 5 | [testenv] 6 | recreate = true 7 | change_dir = {env_tmp_dir} 8 | deps = 9 | pytest 10 | parquetdb @ git+https://github.com/lllangWV/ParquetDB.git@bcc6613d2d552ece646c4628a514b43c381d2bc6 11 | commands = 12 | pytest -vvx -k subinterpreter --pyargs lisien.tests -------------------------------------------------------------------------------- /lisien/tox_slow.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py312,py313,py314 4 | 5 | [testenv] 6 | recreate = true 7 | change_dir = {env_tmp_dir} 8 | deps = 9 | pytest 10 | parquetdb @ git+https://github.com/lllangWV/ParquetDB.git@bcc6613d2d552ece646c4628a514b43c381d2bc6 11 | commands = 12 | pytest -vvx -k '(slow or big) and not subinterpreter' --pyargs lisien.tests -------------------------------------------------------------------------------- /elide/elide/pallet.kv: -------------------------------------------------------------------------------- 1 | : 2 | canvas: 3 | Rectangle: 4 | pos: 5 | ( 6 | root.x + (root.width / 2 - root.tex.size[0] / 2) if root.tex else 0, 7 | root.y + root.height - root.tex.size[1] if root.tex else 0 8 | ) 9 | size: root.tex.size 10 | texture: root.tex 11 | : 12 | orientation: 'lr-tb' 13 | padding_y: 100 14 | size_hint: (None, None) 15 | height: self.minimum_height -------------------------------------------------------------------------------- /elide/elide/graph/board.kv: -------------------------------------------------------------------------------- 1 | : 2 | app: app 3 | size_hint: None, None 4 | : 5 | plane: boardplane 6 | GraphBoardScatterPlane: 7 | id: boardplane 8 | board: root.board 9 | adding_portal: root.adding_portal 10 | reciprocal_portal: root.reciprocal_portal 11 | scale_min: root.scale_min 12 | scale_max: root.scale_max 13 | pos: root.pos 14 | size: root.size 15 | size_hint: None, None -------------------------------------------------------------------------------- /lisien/tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py312,py313,py314 4 | 5 | [testenv] 6 | passenv = TMPDIR 7 | recreate = true 8 | change_dir = {env_tmp_dir} 9 | deps = 10 | pytest 11 | parquetdb @ git+https://github.com/lllangWV/ParquetDB.git@bcc6613d2d552ece646c4628a514b43c381d2bc6 12 | commands = 13 | pytest -vvx -k 'not slow and not big and not subinterpreter' --pyargs lisien.tests 14 | -------------------------------------------------------------------------------- /elide/elide/rulebooks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from kivy.properties import ObjectProperty 4 | from kivy.uix.boxlayout import BoxLayout 5 | from kivy.uix.recycleview import RecycleView 6 | from kivy.uix.screenmanager import Screen 7 | 8 | 9 | class RulebookList(RecycleView): 10 | pass 11 | 12 | 13 | class RulebookItem(BoxLayout): 14 | pass 15 | 16 | 17 | class RulebooksScreen(Screen): 18 | toggle = ObjectProperty() 19 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/gem.atlas: -------------------------------------------------------------------------------- 1 | {"gem-0.png": {"blue": [36, 222, 32, 32], "violet": [104, 188, 32, 32], "stone": [70, 188, 32, 32], "yellowish_brown": [206, 188, 32, 32], "grey_stone": [172, 222, 32, 32], "gold_piece": [104, 222, 32, 32], "boulder": [70, 222, 32, 32], "green": [138, 222, 32, 32], "orange": [206, 222, 32, 32], "white": [138, 188, 32, 32], "yellow": [172, 188, 32, 32], "black": [2, 222, 32, 32], "red": [2, 188, 32, 32], "rock": [36, 188, 32, 32]}} -------------------------------------------------------------------------------- /recipes/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import PyProjectRecipe 2 | 3 | 4 | class SQLAlchemyRecipe(PyProjectRecipe): 5 | name = "sqlalchemy" 6 | version = "2.0.44" 7 | url = "https://github.com/sqlalchemy/sqlalchemy/archive/refs/tags/rel_{}.tar.gz" 8 | depends = ["setuptools"] 9 | 10 | @property 11 | def versioned_url(self): 12 | return self.url.format(self.version.replace(".", "_")) 13 | 14 | 15 | recipe = SQLAlchemyRecipe() 16 | -------------------------------------------------------------------------------- /recipes/sqlalchemy/__init__.py.orig: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import PyProjectRecipe 2 | 3 | 4 | class SQLAlchemyRecipe(PyProjectRecipe): 5 | name = 'sqlalchemy' 6 | version = '2.0.30' 7 | url = 'https://github.com/sqlalchemy/sqlalchemy/archive/refs/tags/rel_{}.tar.gz' 8 | depends = ['setuptools'] 9 | 10 | @property 11 | def versioned_url(self): 12 | return self.url.format(self.version.replace(".", "_")) 13 | 14 | 15 | recipe = SQLAlchemyRecipe() 16 | -------------------------------------------------------------------------------- /elide/elide/logview.kv: -------------------------------------------------------------------------------- 1 | : 2 | halign: 'left' 3 | text_size: self.width, dp(56) 4 | : 5 | viewclass: 'LogLabel' 6 | do_scroll_x: False 7 | RecycleBoxLayout: 8 | orientation: 'vertical' 9 | height: self.minimum_height 10 | size_hint_y: None 11 | default_size_hint: 1, None 12 | default_size: None, dp(56) 13 | : 14 | name: 'log' 15 | BoxLayout: 16 | orientation: 'vertical' 17 | LogView: 18 | level: 10 19 | size_hint_y: 0.9 20 | Button: 21 | text: 'Close' 22 | on_release: root.toggle() 23 | size_hint_y: 0.1 -------------------------------------------------------------------------------- /elide/elide/examples/sugarscape_immediate_growback.py: -------------------------------------------------------------------------------- 1 | from inspect import getsource 2 | from multiprocessing import freeze_support 3 | from tempfile import mkdtemp 4 | 5 | import networkx as nx 6 | from kivy.clock import Clock 7 | from kivy.lang.builder import Builder 8 | from kivy.properties import BooleanProperty, NumericProperty 9 | from networkx import grid_2d_graph 10 | 11 | from elide.game import GameApp, GameScreen, GridBoard 12 | 13 | 14 | def make_grid() -> nx.Graph: 15 | pass 16 | 17 | 18 | def game_start(engine: "lisien.Engine") -> None: 19 | pass 20 | -------------------------------------------------------------------------------- /lisien/lisien/tests/remake_big_grid.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import networkx as nx 4 | 5 | from lisien.engine import Engine 6 | from lisien.tests.data import DATA_DIR 7 | 8 | 9 | def main(): 10 | big_grid = nx.grid_2d_graph(100, 100) 11 | big_grid.add_node("them", location=(0, 0)) 12 | big_grid.graph["straightly"] = nx.shortest_path(big_grid, (0, 0), (99, 99)) 13 | with Engine(None, workers=0) as eng: 14 | eng.add_character("grid", big_grid) 15 | eng.export("grid", os.path.join(DATA_DIR, "big_grid.lisien")) 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /elide/tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py312,py313,py314 4 | 5 | [testenv] 6 | # install pytest in the virtualenv where commands will be executed 7 | passenv = DISPLAY 8 | recreate = true 9 | change_dir = {env_tmp_dir} 10 | deps = 11 | pytest 12 | ../lisien/ 13 | commands = 14 | python -m pytest -vvx --pyargs elide.tests 15 | 16 | [testenv:py314] 17 | deps = 18 | pytest 19 | ../lisien/ 20 | ../../kivy/dist/kivy-3.0.0.dev0-cp314-cp314-linux_x86_64.whl 21 | 22 | [pytest] 23 | tmp_path_retention_policy = failed 24 | -------------------------------------------------------------------------------- /recipes/msgpack/__init__.py: -------------------------------------------------------------------------------- 1 | from pythonforandroid.recipe import CythonRecipe 2 | 3 | 4 | class MsgPackRecipe(CythonRecipe): 5 | version = "1.1.2" 6 | url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz" 7 | depends = ["setuptools"] 8 | call_hostpython_via_targetpython = False 9 | 10 | def apply_patches(self, arch, build_dir=None): 11 | build_dir = build_dir if build_dir else self.get_build_dir(arch.arch) 12 | super().apply_patches(arch, build_dir) 13 | 14 | 15 | recipe = MsgPackRecipe() 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | build/ 3 | dist/ 4 | wheelhouse/ 5 | *.whl 6 | .idea/* 7 | *.db 8 | *.db-journal 9 | *.egg-info 10 | *.log 11 | .eric6project 12 | .ropeproject 13 | *.e4p 14 | .cache 15 | buildozer/elide.ini 16 | *.prof 17 | *.so 18 | *.csv 19 | lisien/allegedb 20 | *.pyc 21 | *~ 22 | trigger.py 23 | prereq.py 24 | action.py 25 | function.py 26 | method.py 27 | strings.json 28 | .coverage 29 | .pytest_cache 30 | breakage.diff 31 | .venv 32 | venv 33 | flame.svg 34 | test.py 35 | _build 36 | .vscode/launch.json 37 | .vscode/settings.json 38 | */.idea 39 | */.tox 40 | .tox 41 | /.idea/ 42 | .buildozer 43 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/boot.atlas: -------------------------------------------------------------------------------- 1 | {"boot-0.png": {"short_purple": [2, 444, 32, 32], "short_red": [36, 444, 32, 32], "middle_brown2": [172, 478, 32, 32], "short_brown2": [444, 478, 32, 32], "middle_ybrown": [376, 478, 32, 32], "middle_purple": [342, 478, 32, 32], "mesh_blue": [70, 478, 32, 32], "long_red": [2, 478, 32, 32], "short_brown": [478, 478, 32, 32], "middle_brown": [206, 478, 32, 32], "mesh_white": [138, 478, 32, 32], "pj": [410, 478, 32, 32], "mesh_black": [36, 478, 32, 32], "middle_gold": [240, 478, 32, 32], "middle_gray": [274, 478, 32, 32], "middle_green": [308, 478, 32, 32], "mesh_red": [104, 478, 32, 32]}} -------------------------------------------------------------------------------- /.woodpecker/subinterpreter-headless-tests.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: push 3 | branch: main 4 | - event: pull_request 5 | repo: clayote/Lisien 6 | 7 | depends_on: 8 | - headless-tests 9 | 10 | steps: 11 | - name: headless_subinterpreter_3.14 12 | image: python:3.14 13 | commands: 14 | - TMPDIR=$(python find_ramdisk.py) 15 | - echo "TMPDIR=$TMPDIR" 16 | - export TMPDIR 17 | - python -m venv .venv${PYTHON_VERSION} 18 | - . .venv${PYTHON_VERSION}/bin/activate 19 | - python -m pip install --upgrade pip 20 | - python -m pip install tox 21 | - tox run -c lisien/tox_terp.ini -------------------------------------------------------------------------------- /elide/elide/gen.kv: -------------------------------------------------------------------------------- 1 | : 2 | directions: 4 if but4.state == 'down' else 8 3 | orientation: 'vertical' 4 | BoxLayout: 5 | orientation: 'horizontal' 6 | MenuIntInput: 7 | id: input_x 8 | hint_text: str(root.xval) if root.xval else 'x' 9 | set_value: root.setter('xval') 10 | Label: 11 | text: 'x' 12 | size_hint_x: 0.1 13 | MenuIntInput: 14 | id: input_y 15 | hint_text: str(root.yval) if root.yval else 'y' 16 | set_value: root.setter('yval') 17 | BoxLayout: 18 | ToggleButton: 19 | id: but4 20 | group: 'dir' 21 | text: '4-way' 22 | state: 'down' 23 | ToggleButton: 24 | id: but8 25 | group: 'dir' 26 | text: '8-way' -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | Lisien documentation master file, created by 3 | sphinx-quickstart on Mon Feb 19 13:37:28 2018. 4 | You can adapt this file completely to your liking, but it should at least 5 | contain the root `toctree` directive. 6 | 7 | #################################### 8 | Welcome to Lisien's documentation! 9 | #################################### 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | :caption: Contents: 14 | 15 | manual 16 | lisien/design 17 | lisien/index 18 | elide/index 19 | 20 | #################### 21 | Indices and tables 22 | #################### 23 | 24 | - :ref:`genindex` 25 | - :ref:`modindex` 26 | - :ref:`search` 27 | -------------------------------------------------------------------------------- /elide/elide/timestream.kv: -------------------------------------------------------------------------------- 1 | : 2 | text: f"{self.branch}\n{int(self.turn)}" 3 | : 4 | key_viewclass: 'widget' 5 | effect_cls: 'ScrollEffect' 6 | RecycleGridLayout: 7 | cols: root.cols 8 | default_width: 100 9 | default_height: 100 10 | default_size_hint: None, None 11 | height: self.minimum_height 12 | width: self.minimum_width 13 | size_hint: None, None 14 | : 15 | name: 'timestream' 16 | timestream: timestream 17 | BoxLayout: 18 | orientation: 'vertical' 19 | Timestream: 20 | id: timestream 21 | size_hint_y: 0.95 22 | BoxLayout: 23 | size_hint_y: 0.05 24 | Button: 25 | text: 'Cancel' 26 | on_press: root.toggle() -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/arm.atlas: -------------------------------------------------------------------------------- 1 | {"arm-0.png": {"glove_gold": [172, 478, 32, 32], "glove_chunli": [138, 478, 32, 32], "glove_blue": [70, 478, 32, 32], "glove_short_yellow": [36, 444, 32, 32], "glove_white": [70, 444, 32, 32], "glove_short_red": [478, 478, 32, 32], "glove_wrist_purple": [104, 444, 32, 32], "glove_grayfist": [240, 478, 32, 32], "glove_black2": [2, 478, 32, 32], "glove_black": [36, 478, 32, 32], "glove_short_gray": [410, 478, 32, 32], "glove_orange": [274, 478, 32, 32], "glove_short_blue": [376, 478, 32, 32], "glove_gray": [206, 478, 32, 32], "glove_purple": [308, 478, 32, 32], "glove_brown": [104, 478, 32, 32], "glove_red": [342, 478, 32, 32], "glove_short_green": [444, 478, 32, 32], "glove_short_white": [2, 444, 32, 32]}} -------------------------------------------------------------------------------- /lisien/lisien/examples/export_examples.py: -------------------------------------------------------------------------------- 1 | from tempfile import TemporaryDirectory 2 | 3 | import kobold 4 | import polygons 5 | import wolfsheep 6 | 7 | from lisien import Engine 8 | 9 | RANDOM_SEED = 69105 10 | 11 | with ( 12 | TemporaryDirectory() as td, 13 | Engine(td, workers=0, random_seed=RANDOM_SEED) as eng, 14 | ): 15 | kobold.inittest(eng) 16 | eng.export("kobold") 17 | with ( 18 | TemporaryDirectory() as td, 19 | Engine(td, workers=0, random_seed=RANDOM_SEED) as eng, 20 | ): 21 | polygons.install(eng) 22 | eng.export("polygons") 23 | with ( 24 | TemporaryDirectory() as td, 25 | Engine(td, workers=0, random_seed=RANDOM_SEED) as eng, 26 | ): 27 | wolfsheep.install(eng, seed=RANDOM_SEED) 28 | eng.export("wolfsheep") 29 | -------------------------------------------------------------------------------- /.woodpecker/headless-tests.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: push 3 | branch: main 4 | - event: pull_request 5 | repo: clayote/Lisien 6 | 7 | matrix: 8 | PYTHON_VERSION: 9 | - 3.12 10 | - 3.13 11 | - 3.14 12 | 13 | steps: 14 | - name: headless_${PYTHON_VERSION} 15 | image: python:${PYTHON_VERSION} 16 | commands: 17 | - TMPDIR=$(python find_ramdisk.py) 18 | - echo "TMPDIR=$TMPDIR" 19 | - export TMPDIR 20 | - python -m venv .venv 21 | - . .venv/bin/activate 22 | - python -m pip install --upgrade pip 23 | - python -m pip install tox 24 | - TOX_ENV_LIST=$(echo "${PYTHON_VERSION}" | sed -Ee 's/([0-9])\.([0-9]{2})/py\1\2/') 25 | - echo "TOX_ENV_LIST=$TOX_ENV_LIST" 26 | - tox run -c lisien/tox.ini -e $TOX_ENV_LIST 27 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/hair.atlas: -------------------------------------------------------------------------------- 1 | {"hair-0.png": {"elf_yellow": [206, 478, 32, 32], "short_red": [206, 444, 32, 32], "sam": [138, 444, 32, 32], "elf_white": [172, 478, 32, 32], "short_yellow": [274, 444, 32, 32], "arwen": [36, 478, 32, 32], "elf_black": [104, 478, 32, 32], "fem_yellow": [342, 478, 32, 32], "fem_red": [274, 478, 32, 32], "long_yellow": [36, 444, 32, 32], "pj": [104, 444, 32, 32], "legolas": [410, 478, 32, 32], "short_white": [240, 444, 32, 32], "long_white": [2, 444, 32, 32], "frodo": [376, 478, 32, 32], "long_black": [444, 478, 32, 32], "merry": [70, 444, 32, 32], "elf_red": [138, 478, 32, 32], "aragorn": [2, 478, 32, 32], "fem_white": [308, 478, 32, 32], "fem_black": [240, 478, 32, 32], "long_red": [478, 478, 32, 32], "short_black": [172, 444, 32, 32], "boromir": [70, 478, 32, 32]}} -------------------------------------------------------------------------------- /lisien/lisien/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /lisien/lisien/examples/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /recipes/sqlalchemy/__init__.py.rej: -------------------------------------------------------------------------------- 1 | --- pythonforandroid/recipes/sqlalchemy/__init__.py 2 | +++ pythonforandroid/recipes/sqlalchemy/__init__.py 3 | @@ -3,11 +3,11 @@ from pythonforandroid.recipe import CompiledComponentsPythonRecipe 4 | 5 | class SQLAlchemyRecipe(CompiledComponentsPythonRecipe): 6 | name = 'sqlalchemy' 7 | - version = '1.3.3' 8 | - url = 'https://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-{version}.tar.gz' 9 | + version = '2.0.40' 10 | + url = 'https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz' 11 | call_hostpython_via_targetpython = False 12 | 13 | - depends = ['setuptools'] 14 | + depends = ['setuptools', 'typing_extensions'] 15 | 16 | patches = ['zipsafe.patch'] 17 | 18 | -------------------------------------------------------------------------------- /lisien/lisien/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | from .engine import Engine as Engine 17 | -------------------------------------------------------------------------------- /.woodpecker/slow-headless-tests.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: push 3 | branch: main 4 | - event: pull_request 5 | repo: clayote/Lisien 6 | 7 | depends_on: 8 | - headless-tests 9 | 10 | matrix: 11 | PYTHON_VERSION: 12 | - 3.12 13 | - 3.13 14 | - 3.14 15 | 16 | steps: 17 | - name: headless_slow_${PYTHON_VERSION} 18 | image: python:${PYTHON_VERSION} 19 | commands: 20 | - TMPDIR=$(python find_ramdisk.py) 21 | - echo "TMPDIR=$TMPDIR" 22 | - export TMPDIR 23 | - python -m venv .venv${PYTHON_VERSION} 24 | - . .venv${PYTHON_VERSION}/bin/activate 25 | - python -m pip install --upgrade pip 26 | - python -m pip install tox 27 | - TOX_ENV_LIST=$(echo "${PYTHON_VERSION}" | sed -Ee 's/([0-9])\.([0-9]{2})/py\1\2/') 28 | - echo "TOX_ENV_LIST=$TOX_ENV_LIST" 29 | - tox run -c lisien/tox_slow.ini -e $TOX_ENV_LIST -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/potion.atlas: -------------------------------------------------------------------------------- 1 | {"potion-0.png": {"white": [206, 120, 32, 32], "yellow": [2, 86, 32, 32], "cloudy": [172, 222, 32, 32], "effervescent": [70, 188, 32, 32], "golden": [172, 188, 32, 32], "sky_blue": [104, 120, 32, 32], "poison": [138, 154, 32, 32], "brown": [70, 222, 32, 32], "pink": [104, 154, 32, 32], "clear": [138, 222, 32, 32], "dark_green": [36, 188, 32, 32], "emerald": [104, 188, 32, 32], "milky": [2, 154, 32, 32], "cyan": [206, 222, 32, 32], "orange": [70, 154, 32, 32], "murky": [36, 154, 32, 32], "silver": [70, 120, 32, 32], "purple_red": [2, 120, 32, 32], "smoky": [138, 120, 32, 32], "swirly": [172, 120, 32, 32], "puce": [206, 154, 32, 32], "ruby": [36, 120, 32, 32], "dark": [2, 188, 32, 32], "brilliant_blue": [36, 222, 32, 32], "black": [2, 222, 32, 32], "magenta": [206, 188, 32, 32], "potion1": [172, 154, 32, 32], "bubbly": [104, 222, 32, 32], "fizzy": [138, 188, 32, 32]}} -------------------------------------------------------------------------------- /find_ramdisk.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tempfile 4 | 5 | 6 | diskfree = ( 7 | subprocess.Popen( 8 | ["df", "-t", "tmpfs", "--output=avail,target"], stdout=subprocess.PIPE 9 | ) 10 | .communicate()[0] 11 | .decode() 12 | ) 13 | tmpdirs = [] 14 | for line in diskfree.split("\n"): 15 | try: 16 | avail, target = line.split() 17 | except ValueError: 18 | continue 19 | tmpdirs.append((int(avail), str(target))) 20 | tmpdirs.sort(reverse=True) 21 | for _, target in tmpdirs: 22 | if not os.path.exists(target): 23 | continue 24 | try: 25 | fn = os.path.join(target, "test.txt") 26 | with open(fn, "wt") as testfile: 27 | testfile.write("aoeu") 28 | with open(fn, "rt") as testfile: 29 | red = testfile.read() 30 | if red == "aoeu": 31 | print(target) 32 | exit() 33 | except (OSError, PermissionError): 34 | continue 35 | print(tempfile.gettempdir()) 36 | -------------------------------------------------------------------------------- /elide/elide/calendar.kv: -------------------------------------------------------------------------------- 1 | : 2 | key_viewclass: 'widget' 3 | RecycleGridLayout: 4 | cols: root.cols 5 | size_hint: None, None 6 | default_size: dp(84), dp(36) 7 | default_size_hint: None, None 8 | size_hint: None, None 9 | size: self.minimum_size 10 | orientation: 'tb-lr' 11 | : 12 | turn_labels: False 13 | key_viewclass: 'widget' 14 | RecycleGridLayout: 15 | cols: 1 16 | orientation: 'lr-tb' 17 | default_size_hint: 1, None 18 | default_size: dp(84), dp(36) 19 | size: self.minimum_size 20 | : 21 | text: str(self.val) if self.val is not None else '' 22 | : 23 | padding: 5 24 | Label: 25 | x: root.center_x - (self.width / 2) 26 | y: root.center_y 27 | text: str(root.value) 28 | size: self.texture_size 29 | : 30 | text: str(self.val) 31 | : 32 | multiline: False 33 | on_text_validate: self._trigger_parse_text() -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/leg.atlas: -------------------------------------------------------------------------------- 1 | {"leg-0.png": {"pants_short_brown3": [104, 444, 32, 32], "belt_gray": [2, 478, 32, 32], "pants16": [410, 478, 32, 32], "pants_orange": [70, 444, 32, 32], "metal_green": [376, 478, 32, 32], "pants_blue": [478, 478, 32, 32], "pj": [240, 444, 32, 32], "pants_darkgreen": [36, 444, 32, 32], "metal_gray": [342, 478, 32, 32], "pants_black": [444, 478, 32, 32], "skirt_white": [342, 444, 32, 32], "bikini_red": [70, 478, 32, 32], "pants_short_gray": [206, 444, 32, 32], "pants_brown": [2, 444, 32, 32], "skirt_green": [308, 444, 32, 32], "leg_armor03": [206, 478, 32, 32], "leg_armor02": [172, 478, 32, 32], "leg_armor01": [138, 478, 32, 32], "leg_armor00": [104, 478, 32, 32], "leg_armor05": [274, 478, 32, 32], "leg_armor04": [240, 478, 32, 32], "pants_short_brown": [138, 444, 32, 32], "pants_short_darkbrown": [172, 444, 32, 32], "belt_redbrown": [36, 478, 32, 32], "skirt_blue": [274, 444, 32, 32], "loincloth_red": [308, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/scroll.atlas: -------------------------------------------------------------------------------- 1 | {"scroll-0.png": {"venzar_borgavve": [36, 120, 32, 32], "read_me": [104, 154, 32, 32], "andova_begarin": [2, 222, 32, 32], "prirutsenie": [70, 154, 32, 32], "pratyavayah": [36, 154, 32, 32], "velox_neb": [2, 120, 32, 32], "verr_yed_horre": [70, 120, 32, 32], "kernod_wel": [104, 188, 32, 32], "tharr": [172, 154, 32, 32], "temov": [138, 154, 32, 32], "foobie_bletch": [206, 222, 32, 32], "ve_forbryderne": [206, 154, 32, 32], "mail": [206, 188, 32, 32], "kirje": [138, 188, 32, 32], "elbib_yloh": [172, 222, 32, 32], "juyed_awk_yacc": [70, 188, 32, 32], "garven_deh": [2, 188, 32, 32], "nr_9": [2, 154, 32, 32], "blank_paper": [36, 222, 32, 32], "duam_xnaht": [104, 222, 32, 32], "elam_ebow": [138, 222, 32, 32], "xixaxa_xoxaxa_xuxaxa": [104, 120, 32, 32], "yum_yum": [138, 120, 32, 32], "hackem_muche": [36, 188, 32, 32], "zelgo_mer": [172, 120, 32, 32], "lep_gex_ven_zea": [172, 188, 32, 32], "daiyen_fooels": [70, 222, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/graph/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from .board import GraphBoard 16 | from .pawn import Pawn 17 | from .spot import GraphSpot 18 | 19 | __all__ = ["GraphBoard", "Pawn", "GraphSpot"] 20 | -------------------------------------------------------------------------------- /recipes/networkx/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | 4 | from pythonforandroid.recipe import PyProjectRecipe 5 | 6 | 7 | class NetworkXRecipe(PyProjectRecipe): 8 | version = "3.5" 9 | url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz" 10 | patches = ["patches/no-compress.patch", "patches/no-tests.patch"] 11 | 12 | def apply_patches(self, arch, build_dir=None): 13 | build_dir = Path( 14 | build_dir if build_dir is not None else self.get_build_dir(arch) 15 | ) 16 | for a_path, dir_names, file_names in build_dir.walk(): 17 | if not a_path.exists(): 18 | continue 19 | for del_dir in ["examples", "tests"]: 20 | if del_dir in dir_names: 21 | shutil.rmtree(a_path.joinpath(del_dir)) 22 | super().apply_patches(arch, str(build_dir)) 23 | 24 | install_python_package = PyProjectRecipe.install_wheel 25 | 26 | 27 | recipe = NetworkXRecipe() 28 | -------------------------------------------------------------------------------- /pull_kivy_logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | filenames = list( 6 | filter( 7 | None, 8 | subprocess.run( 9 | [ 10 | "adb", 11 | "shell", 12 | "run-as org.tacmeta.elide ls files/app/.kivy/logs", 13 | ], 14 | stdout=subprocess.PIPE, 15 | ) 16 | .stdout.decode() 17 | .split("\n"), 18 | ) 19 | ) 20 | os.makedirs("kivylogs", exist_ok=True) 21 | for fn in filenames: 22 | with open(os.path.join("kivylogs", fn[:-3] + "log"), "wb") as outf: 23 | outf.write( 24 | subprocess.run( 25 | [ 26 | "adb", 27 | "shell", 28 | "run-as org.tacmeta.elide cat files/app/.kivy/logs/" + fn, 29 | ], 30 | stdout=subprocess.PIPE, 31 | ).stdout 32 | ) 33 | 34 | lisien_log = subprocess.run( 35 | ["adb", "shell", "run-as org.tacmeta.elide cat files/app/lisien.log"], 36 | stdout=subprocess.PIPE, 37 | ).stdout 38 | if lisien_log: 39 | with open(os.path.join("kivylogs", "lisien.log"), "wb") as outf: 40 | outf.write(lisien_log) 41 | -------------------------------------------------------------------------------- /elide/elide/charsview.kv: -------------------------------------------------------------------------------- 1 | #: import resource_find kivy.resources.resource_find 2 | : 3 | viewclass: 'RecycleToggleButton' 4 | character_name: boxl.character_name 5 | CharactersRecycleBoxLayout: 6 | id: boxl 7 | disabled: app.manager.current != 'chars' 8 | multiselect: False 9 | default_size: None, dp(56) 10 | default_size_hint: 1, None 11 | size_hint_y: None 12 | height: self.minimum_height 13 | orientation: 'vertical' 14 | : 15 | name: 'chars' 16 | charsview: charsview 17 | BoxLayout: 18 | id: chars 19 | orientation: 'vertical' 20 | CharactersView: 21 | id: charsview 22 | size_hint_y: 0.8 23 | character_name: root.character_name 24 | TextInput: 25 | id: newname 26 | size_hint_y: 0.1 27 | hint_text: 'New character name' 28 | write_tab: False 29 | multiline: False 30 | Button: 31 | text: '+' 32 | on_release: root._trigger_new_character(newname.text) 33 | size_hint_y: 0.05 34 | Button: 35 | text: 'Close' 36 | on_release: root.toggle() 37 | size_hint_y: 0.05 -------------------------------------------------------------------------------- /elide/elide/kivygarden/texturestack/README.md: -------------------------------------------------------------------------------- 1 | texturestack 2 | ============ 3 | Widget for compositing textures, like virtual paper dolls. 4 | 5 | "Virtual paper doll" is a technique for building sprite graphics used in, eg., the player avatar in Dungeon Crawl Stone Soup. Basically, you just put the sprites one on top of the other, and then move them around like they're one sprite. You might do this with plain Image widgets, but I found it awkward to manage the exact order of the various subwidgets, so I made this widget to handle them for me. 6 | 7 | Includes two classes: TextureStack proper requires a list of kivy Texture objects; ImageStack is happy with a list of paths to loadable images. For demonstration purposes I have included [the ProcJam 2015 art pack](http://www.procjam.com/2015/09/01/procjam-art-pack-now-available/) by Marsh Davies, available under [Creative Commons By-NC](https://creativecommons.org/licenses/by-nc/4.0/) 8 | 9 | In case of my death, I, Zachary Spector, wish for this code to be relicensed under [CC0](https://creativecommons.org/choose/zero/). 10 | -------------------------------------------------------------------------------- /elide/TESTS: -------------------------------------------------------------------------------- 1 | Manual tests to perform on ELiDE before making a release. 2 | 3 | * Open and close the app with all the example scenarios, make sure they look right. 4 | * Create Places with various graphics. 5 | * Create Things with various graphics. 6 | * Place Things on Places of various graphics (big ones, little ones) 7 | * Move Things between Places. 8 | * Move Places about (with and without Things in them) 9 | * Create Portals, one-way and two way 10 | * Move Places about when there are Portals between them 11 | * Delete Places with and without Things in, with and without Portals going to/from them 12 | * Delete Things 13 | * On all the entity types, create, alter, and delete stats of all types 14 | * Travel through time and make sure it's as you remember, after a variety of changes, forwards, backwards, and between branches 15 | * Make new branches by creating or deleting stuff in the past 16 | * Create and delete Characters 17 | * Switch between Characters 18 | * Edit some strings 19 | * Edit some code 20 | * Actually run a simulation with the edited code, before and after closing and reopening the app -------------------------------------------------------------------------------- /lisien/lisien/proxy/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | import sys 17 | 18 | from lisien.proxy.manager import EngineProxyManager 19 | 20 | if __name__ == "__main__": 21 | if os.path.exists(sys.argv[-1]) and os.path.isfile(sys.argv[-1]): 22 | mgr = EngineProxyManager() 23 | eng = mgr.start(replay_file=sys.argv[-1], loglevel="debug") 24 | mgr.shutdown() 25 | -------------------------------------------------------------------------------- /elide/elide/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | import sys 17 | 18 | from elide.app import ElideApp 19 | 20 | 21 | def elide(): 22 | kwargs = {} 23 | if os.path.isdir(sys.argv[-1]): 24 | kwargs["prefix"] = sys.argv[-1] 25 | kwargs["immediate_start"] = True 26 | app = ElideApp(**kwargs) 27 | app.run() 28 | 29 | 30 | if __name__ == "__main__": 31 | elide() 32 | -------------------------------------------------------------------------------- /elide/elide/kivygarden/texturestack/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Zachary Spector 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /recipes/networkx/patches/no-compress.patch: -------------------------------------------------------------------------------- 1 | diff -ur networkx-3.5/networkx/utils/decorators.py networkx-3.5-b/networkx/utils/decorators.py 2 | --- networkx-3.5/networkx/utils/decorators.py 2025-05-29 23:34:32.000000000 +1200 3 | +++ networkx-3.5-b/networkx/utils/decorators.py 2025-10-17 17:04:32.328617154 +1300 4 | @@ -1,6 +1,4 @@ 5 | -import bz2 6 | import collections 7 | -import gzip 8 | import inspect 9 | import itertools 10 | import re 11 | @@ -91,14 +89,6 @@ 12 | return argmap(_not_implemented_for, 0) 13 | 14 | 15 | -# To handle new extensions, define a function accepting a `path` and `mode`. 16 | -# Then add the extension to _dispatch_dict. 17 | -fopeners = { 18 | - ".gz": gzip.open, 19 | - ".gzip": gzip.open, 20 | - ".bz2": bz2.BZ2File, 21 | -} 22 | -_dispatch_dict = defaultdict(lambda: open, **fopeners) 23 | 24 | 25 | def open_file(path_arg, mode="r"): 26 | @@ -191,7 +181,7 @@ 27 | # could be None, or a file handle, in which case the algorithm will deal with it 28 | return path, lambda: None 29 | 30 | - fobj = _dispatch_dict[ext](path, mode=mode) 31 | + fobj = open(path, mode=mode) 32 | return fobj, lambda: fobj.close() 33 | 34 | return argmap(_open_file, path_arg, try_finally=True) 35 | -------------------------------------------------------------------------------- /butler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tomllib 4 | 5 | with open("lisien/pyproject.toml", "rb") as inf: 6 | cfg = tomllib.load(inf) 7 | 8 | version = cfg["project"]["version"] 9 | 10 | for lisien_wheel in os.listdir("lisien/dist"): 11 | if lisien_wheel.endswith(".whl"): 12 | break 13 | else: 14 | sys.exit("Couldn't find the lisien wheel") 15 | for elide_wheel in os.listdir("elide/dist"): 16 | if elide_wheel.endswith(".whl"): 17 | break 18 | else: 19 | sys.exit("Couldn't find the elide wheel") 20 | for elide_apk in os.listdir("bin/"): 21 | if elide_apk.startswith("Elide") and elide_apk.endswith( 22 | "-arm64-v8a_armeabi-v7a-debug.apk" 23 | ): 24 | break 25 | else: 26 | sys.exit("Couldn't find the Elide APK") 27 | 28 | windist = os.path.expanduser("~/lisien_windows") 29 | if not os.path.exists(windist): 30 | sys.exit("Couldn't find the Elide Windows distribution") 31 | os.system( 32 | f"butler push lisien/dist/{lisien_wheel} clayote/lisien:lisien-whl --userversion {version}" 33 | ) 34 | os.system( 35 | f"butler push elide/dist/{elide_wheel} clayote/lisien:elide-whl --userversion {version}" 36 | ) 37 | os.system( 38 | f"butler push {windist} clayote/lisien:windows --userversion {version}" 39 | ) 40 | os.system( 41 | f"butler push bin/{elide_apk} clayote/lisien:android --userversion {version}" 42 | ) 43 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/amulet.atlas: -------------------------------------------------------------------------------- 1 | {"amulet-0.png": {"cameo_orange": [104, 222, 32, 32], "stone3_green": [70, 52, 32, 32], "stone1_cyan": [70, 86, 32, 32], "stone1_green": [104, 86, 32, 32], "stone1_pink": [138, 86, 32, 32], "octagonal": [206, 154, 32, 32], "hexagonal": [172, 154, 32, 32], "circular": [2, 188, 32, 32], "bone_gray": [36, 222, 32, 32], "square": [36, 86, 32, 32], "crystal_red": [104, 188, 32, 32], "celtic_yellow": [206, 222, 32, 32], "golden": [138, 154, 32, 32], "eye_green": [2, 154, 32, 32], "eye_cyan": [206, 188, 32, 32], "penta_orange": [70, 120, 32, 32], "stone3_magenta": [104, 52, 32, 32], "celtic_red": [172, 222, 32, 32], "celtic_blue": [138, 222, 32, 32], "face2": [104, 154, 32, 32], "face1_gold": [70, 154, 32, 32], "eye_magenta": [36, 154, 32, 32], "stone2_green": [206, 86, 32, 32], "stone2_red": [2, 52, 32, 32], "oval": [2, 120, 32, 32], "pyramidal": [104, 120, 32, 32], "concave": [36, 188, 32, 32], "cameo_blue": [70, 222, 32, 32], "stone3_blue": [36, 52, 32, 32], "spherical": [2, 86, 32, 32], "stone2_blue": [172, 86, 32, 32], "crystal_green": [70, 188, 32, 32], "cylinder_gray": [172, 188, 32, 32], "ring_cyan": [138, 120, 32, 32], "ring_red": [206, 120, 32, 32], "triangular": [138, 52, 32, 32], "ring_green": [172, 120, 32, 32], "amulet_of_yendor": [2, 222, 32, 32], "crystal_white": [138, 188, 32, 32], "penta_green": [36, 120, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/ring.atlas: -------------------------------------------------------------------------------- 1 | {"ring-0.png": {"gold_white": [104, 154, 32, 32], "ruby": [206, 86, 32, 32], "opal": [104, 120, 32, 32], "jade": [36, 120, 32, 32], "plain_dark": [36, 86, 32, 32], "emerald": [36, 188, 32, 32], "granite": [172, 154, 32, 32], "gold_blue": [138, 188, 32, 32], "pearl": [138, 120, 32, 32], "gold_green": [2, 154, 32, 32], "plain_black": [172, 120, 32, 32], "agate": [2, 222, 32, 32], "coral": [206, 222, 32, 32], "wooden": [36, 18, 32, 32], "plain_blue": [206, 120, 32, 32], "brass": [70, 222, 32, 32], "sapphire": [2, 52, 32, 32], "shiny": [36, 52, 32, 32], "topaz": [172, 52, 32, 32], "plain_yellow": [172, 86, 32, 32], "wire": [2, 18, 32, 32], "plain_cyan": [2, 86, 32, 32], "engagement": [70, 188, 32, 32], "plain_green": [70, 86, 32, 32], "clay": [138, 222, 32, 32], "glass": [104, 188, 32, 32], "bronze": [104, 222, 32, 32], "gold_cyan": [206, 188, 32, 32], "twisted": [206, 52, 32, 32], "copper": [172, 222, 32, 32], "gold_yellow": [138, 154, 32, 32], "iron": [206, 154, 32, 32], "black_onyx": [36, 222, 32, 32], "plain_red": [138, 86, 32, 32], "diamond": [2, 188, 32, 32], "gold": [172, 188, 32, 32], "gold_red": [70, 154, 32, 32], "gold_magenta": [36, 154, 32, 32], "ivory": [2, 120, 32, 32], "silver": [70, 52, 32, 32], "steel": [104, 52, 32, 32], "plain_magenta": [104, 86, 32, 32], "moonstone": [70, 120, 32, 32], "tiger_eye": [138, 52, 32, 32]}} -------------------------------------------------------------------------------- /elide/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | include-package-data=true 7 | packages = ["elide", "elide.graph", "elide.grid", "elide.kivygarden.texturestack", "elide.tests"] 8 | 9 | [project] 10 | name = "elide" 11 | version = "0.23.1" 12 | authors = [ 13 | { name="Zachary Spector", email="public@zacharyspector.com" }, 14 | ] 15 | description = "Extensible Life Simulator Engine Development Environment" 16 | readme = "README.md" 17 | license = "AGPL-3.0-only" 18 | license-files = ["LICENSE"] 19 | requires-python = "~=3.12" 20 | classifiers = [ 21 | "Programming Language :: Python :: 3", 22 | "Operating System :: OS Independent", 23 | "Development Status :: 3 - Alpha", 24 | "Intended Audience :: Developers" 25 | ] 26 | dependencies = [ 27 | "lisien==0.23.1", 28 | "kivy>=2.0.0", 29 | "kivy-deps.glew ; sys_platform == 'win32'", 30 | "kivy-deps.sdl2 ; sys_platform == 'win32'", 31 | "pygments>=2.7.4" 32 | ] 33 | 34 | [project.urls] 35 | "Homepage" = "https://codeberg.org/clayote/Lisien" 36 | "Bug Tracker" = "https://codeberg.org/clayote/Lisien/issues" 37 | 38 | [tool.pytest.ini_options] 39 | markers = [ 40 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 41 | "big: marks tests as too big to diagnose specific bugs with" 42 | ] 43 | asyncio_default_fixture_loop_scope = "function" 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Report any problems or feature requests for Lisien on 2 | [Codeberg](https://codeberg.org/clayote/Lisien/issues). 3 | 4 | The best way for new users to help with Lisien is to try to make a game with it. Try 5 | modifying [the Awareness sim](https://github.com/TacticalMetaphysics/LiSE/blob/main/ELiDE/ELiDE/examples/awareness.py). 6 | This template shows you how to use 7 | Elide's widgets as the frontend to your game, which will likely be the easiest route in the long run, but if you have 8 | trouble with it, or you need 3D, other Python game engines are available; consider [Pyglet](http://pyglet.org/) 9 | or [Ursina](https://www.ursinaengine.org/). If you're using such an engine, you don't need Elide, though it might be helpful as a 10 | way to browse the world state if it's not readily apparent from looking at your game. 11 | 12 | In the likely case that something about the engine doesn't behave how you expect, or you need a feature, file a ticket 13 | against the Lisien repository. Please link me to your source code or at least paste a runnable snippet in the ticket if at 14 | all possible. 15 | 16 | Lisien does not follow the formatting conventions of Python's standard `black` 17 | autoformatter, due to ideological differences. Instead, use [ruff](https://github.com/astral-sh/ruff) with the included 18 | `ruff.toml`. 19 | 20 | If you need more help, send email to public@zacharyspector.com 21 | -------------------------------------------------------------------------------- /lisien/lisien/tests/remake_sickle.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | 17 | from lisien.engine import Engine 18 | from lisien.examples.sickle import install 19 | 20 | 21 | def main(): 22 | outpath = os.path.join( 23 | os.path.abspath(os.path.dirname(__file__)), "data", "sickle.lisien" 24 | ) 25 | if os.path.exists(outpath): 26 | os.remove(outpath) 27 | with Engine( 28 | None, 29 | workers=0, 30 | keyframe_interval=None, 31 | keep_rules_journal=False, 32 | random_seed=69105, 33 | ) as eng: 34 | install(eng) 35 | print("Installed. Exporting to", outpath) 36 | eng.export(path=outpath) 37 | print("All done") 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /lisien/lisien/proxy/worker_subinterpreter.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | def worker_subinterpreter( 16 | i: int, 17 | prefix: str, 18 | branches: dict, 19 | eternal: dict, 20 | in_queue, 21 | out_queue, 22 | logq, 23 | *, 24 | function: dict | None, 25 | method: dict | None, 26 | trigger: dict | None, 27 | prereq: dict | None, 28 | action: dict | None, 29 | ): 30 | from lisien.proxy.routine import worker_subroutine 31 | 32 | return worker_subroutine( 33 | i, 34 | prefix, 35 | branches, 36 | eternal, 37 | in_queue.get, 38 | out_queue.put, 39 | logq, 40 | function=function, 41 | method=method, 42 | trigger=trigger, 43 | prereq=prereq, 44 | action=action, 45 | ) 46 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/book.atlas: -------------------------------------------------------------------------------- 1 | {"book-0.png": {"cyan": [206, 222, 32, 32], "silver": [36, 52, 32, 32], "white": [104, 18, 32, 32], "metal_green": [172, 120, 32, 32], "dark_gray": [70, 188, 32, 32], "tan": [104, 52, 32, 32], "cloth": [138, 222, 32, 32], "glittering": [2, 154, 32, 32], "light_green": [36, 120, 32, 32], "light_brown": [206, 154, 32, 32], "dark_blue": [2, 188, 32, 32], "plaid": [104, 86, 32, 32], "violet": [70, 18, 32, 32], "parchment": [36, 86, 32, 32], "stained": [70, 52, 32, 32], "dull": [172, 188, 32, 32], "indigo": [104, 154, 32, 32], "blank_paper": [2, 222, 32, 32], "metal_blue": [104, 120, 32, 32], "box2": [70, 222, 32, 32], "wrinkled": [138, 18, 32, 32], "dog_eared": [138, 188, 32, 32], "metal_cyan": [138, 120, 32, 32], "gray": [70, 154, 32, 32], "pink": [70, 86, 32, 32], "magenta": [70, 120, 32, 32], "gold": [36, 154, 32, 32], "book_of_the_dead": [36, 222, 32, 32], "bronze": [104, 222, 32, 32], "light_gray": [2, 120, 32, 32], "ragged": [172, 86, 32, 32], "leather": [138, 154, 32, 32], "turquoise": [206, 52, 32, 32], "dark_green": [104, 188, 32, 32], "red": [206, 86, 32, 32], "copper": [172, 222, 32, 32], "light_blue": [172, 154, 32, 32], "mottled": [206, 120, 32, 32], "orange": [2, 86, 32, 32], "dark_brown": [36, 188, 32, 32], "purple": [138, 86, 32, 32], "dusty": [206, 188, 32, 32], "thick": [138, 52, 32, 32], "yellow": [172, 18, 32, 32], "shining": [2, 52, 32, 32], "vellum": [2, 18, 32, 32], "velvet": [36, 18, 32, 32], "thin": [172, 52, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from kivy.logger import ConsoleHandler, KivyFormatter, Logger 16 | from kivy.resources import resource_add_path 17 | 18 | formatter = KivyFormatter("%(asctime)s [%(levelname)-7s] %(message)s") 19 | for handler in Logger.handlers: 20 | if not isinstance(handler, ConsoleHandler): 21 | handler.setFormatter(formatter) 22 | 23 | resource_add_path(__path__[0]) 24 | for submodule in [ 25 | "/assets", 26 | "/assets/rltiles", 27 | "/assets/kenney1bit", 28 | ]: 29 | resource_add_path(__path__[0] + submodule) 30 | 31 | __all__ = [ 32 | "graph", 33 | "grid", 34 | "app", 35 | "card", 36 | "game", 37 | "menu", 38 | "spritebuilder", 39 | "calendar", 40 | ] 41 | -------------------------------------------------------------------------------- /lisien/lisien/tests/remake_pathfind.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | 17 | 18 | from lisien.engine import Engine 19 | from lisien.examples.pathfind import install 20 | from lisien.tests.data import DATA_DIR 21 | 22 | 23 | def main(random_seed=69105): 24 | outpath = os.path.join(DATA_DIR, "pathfind.lisien") 25 | if os.path.exists(outpath): 26 | os.remove(outpath) 27 | with Engine( 28 | None, 29 | workers=0, 30 | keyframe_interval=None, 31 | keep_rules_journal=False, 32 | random_seed=random_seed, 33 | ) as eng: 34 | install(eng, random_seed) 35 | print("Installed. Exporting to", outpath) 36 | eng.export(path=outpath) 37 | print("All done") 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /lisien/lisien/tests/remake_wolfsheep.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | 17 | from lisien.engine import Engine 18 | from lisien.examples.wolfsheep import install 19 | 20 | 21 | def main(random_seed=69105): 22 | outpath = os.path.join( 23 | os.path.abspath(os.path.dirname(__file__)), "data", "wolfsheep.lisien" 24 | ) 25 | if os.path.exists(outpath): 26 | os.remove(outpath) 27 | with Engine( 28 | None, 29 | workers=0, 30 | keyframe_interval=None, 31 | keep_rules_journal=False, 32 | random_seed=random_seed, 33 | ) as eng: 34 | install(eng) 35 | print("Installed. Exporting to", outpath) 36 | eng.export(path=outpath) 37 | print("All done") 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /elide/elide/spritebuilder.kv: -------------------------------------------------------------------------------- 1 | : 2 | orientation: 'vertical' 3 | SpriteBuilder: 4 | id: builder 5 | name: root.name 6 | prefix: root.prefix 7 | default_imgpaths: root.default_imgpaths 8 | imgpaths: root.imgpaths 9 | data: root.data 10 | SpriteSelector: 11 | id: selector 12 | textbox: textbox 13 | size_hint_y: 0.1 14 | prefix: root.prefix 15 | default_imgpaths: root.default_imgpaths 16 | imgpaths: root.imgpaths 17 | pallets: builder.pallets 18 | preview: preview 19 | TextInput: 20 | id: textbox 21 | multiline: False 22 | write_tab: False 23 | hint_text: 'Enter name prefix' 24 | Widget: 25 | id: preview 26 | canvas: 27 | Color: 28 | rgba: 1, 1, 1, 1 29 | Button: 30 | text: 'Import' 31 | on_release: root._choose_graphic_to_import() 32 | Button: 33 | text: 'OK' 34 | on_release: root.pressed() 35 | : 36 | name: 'pawncfg' 37 | imgpaths: dialog.imgpaths 38 | PawnConfigDialog: 39 | id: dialog 40 | toggle: root.toggle 41 | default_imgpaths: ['atlas://rltiles/base/unseen'] 42 | custom_imgs_header: root.custom_imgs_header 43 | custom_imgs_dir: app.prefix + '/custom_pawn_imgs' 44 | data: root.data 45 | : 46 | name: 'spotcfg' 47 | imgpaths: dialog.imgpaths 48 | SpotConfigDialog: 49 | id: dialog 50 | toggle: root.toggle 51 | default_imgpaths: ['atlas://rltiles/floor/floor-stone'] 52 | custom_imgs_header: root.custom_imgs_header 53 | custom_imgs_dir: app.prefix + '/custom_spot_imgs' 54 | data: root.data -------------------------------------------------------------------------------- /elide/elide/logview.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from logging import Handler 4 | 5 | from kivy.logger import Logger 6 | from kivy.properties import NumericProperty, ObjectProperty 7 | from kivy.uix.label import Label 8 | from kivy.uix.recycleview import RecycleView 9 | from kivy.uix.screenmanager import Screen 10 | 11 | from elide.util import load_kv 12 | 13 | 14 | class LogViewHandler(Handler): 15 | def __init__(self, logview: LogView, level=0): 16 | self.logview = logview 17 | super().__init__(level) 18 | 19 | def emit(self, record): 20 | if hasattr(record, "message"): 21 | msg = record.message 22 | elif hasattr(record, "msg"): 23 | msg = record.msg 24 | else: 25 | Logger.warning("Can't format log record") 26 | return 27 | self.logview.data.append({"text": str(msg)}) 28 | 29 | 30 | class LogLabel(Label): 31 | pass 32 | 33 | 34 | class LogView(RecycleView): 35 | """View of a log, not necessarily in a file""" 36 | 37 | level = NumericProperty(10) 38 | 39 | def __init__(self, **kwargs): 40 | super().__init__(**kwargs) 41 | self._handler = LogViewHandler(self, level=int(self.level)) 42 | Logger.addHandler(self._handler) 43 | 44 | def on_level(self, *_): 45 | if not hasattr(self, "_handler"): 46 | return 47 | self._handler.level = int(self.level) 48 | 49 | def on_data(self, *_): 50 | self.scroll_y = 0.0 51 | 52 | 53 | class LogScreen(Screen): 54 | toggle = ObjectProperty() 55 | 56 | def __init__(self, **kw): 57 | load_kv("logview.kv") 58 | super().__init__(**kw) 59 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/food.atlas: -------------------------------------------------------------------------------- 1 | {"food-0.png": {"cram_ration": [70, 154, 32, 32], "clove_of_garlic": [206, 188, 32, 32], "bone": [138, 222, 32, 32], "lump_of_royal_jelly": [70, 86, 32, 32], "burger": [2, 188, 32, 32], "huge_chunk_of_meat": [138, 120, 32, 32], "grape": [70, 120, 32, 32], "banana": [70, 222, 32, 32], "cheese": [104, 188, 32, 32], "meat_ring": [172, 86, 32, 32], "choko": [172, 188, 32, 32], "orange": [36, 52, 32, 32], "sultana": [138, 18, 32, 32], "beef_jerky": [104, 222, 32, 32], "strawberry": [104, 18, 32, 32], "apricot": [36, 222, 32, 32], "candy_bar": [36, 188, 32, 32], "bread0": [172, 222, 32, 32], "sausage": [206, 52, 32, 32], "meatball": [138, 86, 32, 32], "c_ration": [104, 154, 32, 32], "tin": [172, 18, 32, 32], "lembas_wafer": [2, 86, 32, 32], "chocorate": [138, 188, 32, 32], "tripe_ration": [206, 18, 32, 32], "lemon": [36, 86, 32, 32], "pear": [104, 52, 32, 32], "corpse": [2, 154, 32, 32], "honeycomb": [104, 120, 32, 32], "pancake": [70, 52, 32, 32], "cream_pie": [138, 154, 32, 32], "carrot": [70, 188, 32, 32], "meat_stick": [206, 86, 32, 32], "rambutan": [172, 52, 32, 32], "slime_mold": [2, 18, 32, 32], "snozzcumber": [36, 18, 32, 32], "food_ration": [2, 120, 32, 32], "pizza": [138, 52, 32, 32], "apple": [2, 222, 32, 32], "fortune_cookie": [36, 120, 32, 32], "k_ration": [206, 120, 32, 32], "kelp_frond": [172, 120, 32, 32], "sprig_of_wolfsbane": [70, 18, 32, 32], "bread_ration": [206, 222, 32, 32], "corpse_rotten": [36, 154, 32, 32], "melon": [2, 52, 32, 32], "eucalyptus_leaf": [206, 154, 32, 32], "lychee": [104, 86, 32, 32], "egg": [172, 154, 32, 32]}} -------------------------------------------------------------------------------- /lisien/lisien/tests/test_resume.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import pytest 16 | 17 | from lisien import Engine 18 | from lisien.examples.polygons import install 19 | 20 | from .util import make_test_engine_kwargs 21 | 22 | 23 | # TODO: use a test sim that does everything in every cache 24 | @pytest.mark.big 25 | def test_resume(tmp_path, persistent_database, random_seed): 26 | ekwargs = make_test_engine_kwargs( 27 | tmp_path, "serial", persistent_database, random_seed 28 | ) 29 | ekwargs["keyframe_on_close"] = False 30 | with Engine(**ekwargs) as eng: 31 | install(eng) 32 | eng.next_turn() 33 | last_branch, last_turn, last_tick = eng.time 34 | with Engine(**ekwargs) as eng: 35 | assert eng.time == (last_branch, last_turn, last_tick) 36 | curturn = eng.turn 37 | eng.next_turn() 38 | assert eng.turn == curturn + 1 39 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_performance.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | import shutil 17 | from time import monotonic 18 | 19 | import pytest 20 | 21 | from lisien.proxy.manager import EngineProxyManager 22 | 23 | from .data import DATA_DIR 24 | 25 | 26 | @pytest.mark.parquetdb 27 | def test_follow_path(tmp_path): 28 | with ( 29 | EngineProxyManager(tmp_path) as proxman, 30 | proxman.load_archive( 31 | os.path.join(DATA_DIR, "big_grid.lisien"), tmp_path, workers=0 32 | ) as prox, 33 | ): 34 | grid = prox.character["grid"] 35 | them = grid.thing["them"] 36 | straightly = grid.stat["straightly"] 37 | start = monotonic() 38 | them.follow_path(straightly) 39 | elapsed = monotonic() - start 40 | assert elapsed < 20, ( 41 | f"Took too long to follow a path of length {len(straightly)}: {elapsed:.2} seconds" 42 | ) 43 | -------------------------------------------------------------------------------- /lisien/lisien/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | """Proxy objects to access lisien entities from another process. 16 | 17 | Each proxy class is meant to emulate the equivalent lisien class, 18 | and any change you make to a proxy will be made in the corresponding 19 | entity in the lisien core. 20 | 21 | To use these, first instantiate an ``EngineProcessManager``, then 22 | call its ``start`` method with the same arguments you'd give a real 23 | ``Engine``. You'll get an ``EngineProxy``, which acts like the underlying 24 | ``Engine`` for most purposes. 25 | 26 | ``EngineHandle`` is a fairly thin wrapper around a real Lisien ``Engine`` 27 | that exposes all functionality that proxies need in simple methods. 28 | 29 | """ 30 | 31 | from .handle import EngineHandle as EngineHandle 32 | from .manager import EngineProxyManager as EngineProxyManager 33 | -------------------------------------------------------------------------------- /elide/elide/boardview.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from functools import partial 16 | 17 | from kivy.properties import NumericProperty, ObjectProperty 18 | from kivy.uix.stencilview import StencilView 19 | 20 | from .util import logwrap 21 | 22 | wraplog_boardview = partial(logwrap, section="BoardView") 23 | 24 | 25 | class BoardView(StencilView): 26 | board = ObjectProperty() 27 | plane = ObjectProperty() 28 | scale_min = NumericProperty(allownone=True) 29 | scale_max = NumericProperty(allownone=True) 30 | 31 | @wraplog_boardview 32 | def spot_from_dummy(self, dummy): 33 | self.plane.spot_from_dummy(dummy) 34 | 35 | @wraplog_boardview 36 | def pawn_from_dummy(self, dummy): 37 | self.plane.pawn_from_dummy(dummy) 38 | 39 | @wraplog_boardview 40 | def on_touch_down(self, touch): 41 | if not self.collide_point(*touch.pos): 42 | return 43 | return super().on_touch_down(touch) 44 | -------------------------------------------------------------------------------- /lisien/lisien/tests/remake_college.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | import shutil 17 | import tempfile 18 | 19 | from lisien.engine import Engine 20 | from lisien.examples.college import install 21 | 22 | 23 | def main(): 24 | outpath = os.path.join( 25 | os.path.abspath(os.path.dirname(__file__)), 26 | "data", 27 | "college{}.lisien", 28 | ) 29 | if os.path.exists(outpath): 30 | os.remove(outpath) 31 | with Engine( 32 | None, 33 | workers=0, 34 | keyframe_interval=None, 35 | keep_rules_journal=False, 36 | random_seed=69105, 37 | ) as eng: 38 | install(eng) 39 | for i in range(10): 40 | print(i) 41 | eng.next_turn() 42 | exto = outpath.format("10") 43 | print("Done simulating the 10 turn case. Exporting to " + exto) 44 | eng.export(path=exto) 45 | for i in range(10, 24): 46 | print(i) 47 | eng.next_turn() 48 | exto = outpath.format("24") 49 | print("Done simulating 24 turns. Exporting to " + exto) 50 | eng.export(path=exto) 51 | print("All done") 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo 3 | dos2unix -V 4 | alias python=python3.12 5 | VERSION=$(python3.12 check_version.py) 6 | export VERSION 7 | python -m build --version 8 | twine --version 9 | python -m sphinx --version 10 | isort --version 11 | tox --version 12 | ruff --version 13 | pyclean --version 14 | wine git --version 15 | buildozer --version 16 | ls ~/lisien_windows 17 | if [ ! -z "$(git clean -n)" ]; then 18 | echo "Debris in the repository." 19 | git clean -n 20 | exit 1 21 | fi 22 | isort lisien 23 | isort elide 24 | ruff format lisien 25 | ruff format elide 26 | PYTHONPATH=$PWD/lisien:$PWD/elide python -m sphinx . docs/ 27 | for env in $(tox -c elide/tox.ini -l); do if [ -e "/tmp/pytest-of-${USER}" ]; then rm -r "/tmp/pytest-of-${USER}/"; fi; pyclean .; tox -c elide/tox.ini -e $env; done 28 | for env in $(tox -c lisien/tox.ini -l); do if [ -e "/tmp/pytest-of-${USER}" ]; then rm -r "/tmp/pytest-of-${USER}/"; fi; pyclean .; tox -c lisien/tox.ini -e $env; done 29 | rm -rf bin lisien/dist elide/dist 30 | JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 buildozer android update clean debug 31 | cd docs 32 | git add . 33 | git commit -m "Release v${VERSION}" 34 | git push 35 | git push --tags 36 | cd .. 37 | git commit -am "Release v${VERSION}" 38 | git tag "v${VERSION}" 39 | git push 40 | git push --tags 41 | python -m build lisien/ 42 | python -m build elide/ 43 | twine check lisien/dist/* elide/dist/* 44 | twine upload lisien/dist/* elide/dist/* 45 | twine upload --repository codeberg lisien/dist/* elide/dist/* 46 | wine ~/lisien_windows/python/python.exe -m pip install --force-reinstall lisien/ elide/ 'parquetdb @ git+https://github.com/lllangWV/ParquetDB.git' 47 | pyclean ~/lisien_windows 48 | unix2dos -n CHANGES.txt ~/lisien_windows/CHANGES.txt 49 | cp -rf docs ~/lisien_windows/ 50 | python3.12 butler.py 51 | -------------------------------------------------------------------------------- /elide/elide/tests/test_sprite_builder.py: -------------------------------------------------------------------------------- 1 | from ..pallet import Pallet 2 | from .util import idle_until 3 | 4 | 5 | def test_build_pawn(elide_app): 6 | app = elide_app 7 | app.manager.current = "pawncfg" 8 | idle_until( 9 | lambda: "dialog" in app.pawncfg.ids, 10 | 100, 11 | "Never made dialog for pawncfg", 12 | ) 13 | pawn_cfg_dialog = app.pawncfg.ids.dialog 14 | idle_until( 15 | lambda: "builder" in pawn_cfg_dialog.ids, 16 | 100, 17 | "Never made pawn builder", 18 | ) 19 | builder = pawn_cfg_dialog.ids.builder 20 | idle_until(lambda: builder.labels, 100, "Never got any builder labels") 21 | idle_until(lambda: builder.pallets, 100, "Never got any builder pallets") 22 | idle_until( 23 | lambda: len(builder.labels) == len(builder.pallets), 24 | 100, 25 | "Never updated pawn builder", 26 | ) 27 | palbox = builder._palbox 28 | for child in palbox.children: 29 | if not isinstance(child, Pallet): 30 | continue 31 | idle_until( 32 | lambda: child.swatches, 33 | 100, 34 | "Never got swatches for " + child.filename, 35 | ) 36 | if "draconian_m" in child.swatches: 37 | child.swatches["draconian_m"].state = "down" 38 | idle_until( 39 | lambda: child.swatches["draconian_m"] in child.selection, 40 | 100, 41 | "Selection never updated", 42 | ) 43 | if "robe_red" in child.swatches: 44 | child.swatches["robe_red"].state = "down" 45 | idle_until( 46 | lambda: child.swatches["robe_red"] in child.selection, 47 | 100, 48 | "Selection never updated", 49 | ) 50 | idle_until( 51 | lambda: pawn_cfg_dialog.ids.selector.imgpaths, 52 | 100, 53 | "Never got imgpaths", 54 | ) 55 | pawn_cfg_dialog.pressed() 56 | idle_until( 57 | lambda: pawn_cfg_dialog.imgpaths, 100, "Never propagated imgpaths" 58 | ) 59 | assert pawn_cfg_dialog.imgpaths == [ 60 | "atlas://base.atlas/draconian_m", 61 | "atlas://body.atlas/robe_red", 62 | ] 63 | -------------------------------------------------------------------------------- /lisien/lisien/server/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | """A simple local server providing access to a lisien core 16 | 17 | Run this as: 18 | 19 | python3 -m lisien.server 20 | 21 | and it will start an HTTP server at localhost:8080. Send msgpack mappings 22 | to it, with the key 'command' set to the name of one of the methods in 23 | :class:`lisien.handle.EngineHandle` and remaining keys set to arguments 24 | accepted by that method. 25 | 26 | Refer to :class:`lisien.handle.EngineHandle` for documentation on those 27 | methods. 28 | 29 | """ 30 | 31 | from argparse import ArgumentParser 32 | 33 | import cherrypy 34 | 35 | from . import LiSEHandleWebService 36 | 37 | parser = ArgumentParser() 38 | parser.add_argument("--prefix", action="store", default=".") 39 | args = parser.parse_args() 40 | conf = { 41 | "/": { 42 | "request.dispatch": cherrypy.dispatch.MethodDispatcher(), 43 | "tools.sessions.on": True, 44 | "tools.response_headers.on": True, 45 | "tools.response_headers.headers": [ 46 | ("Content-Type", "application/json") 47 | ], 48 | "tools.encode.on": True, 49 | "tools.encode.encoding": "utf-8", 50 | } 51 | } 52 | cherrypy.quickstart(LiSEHandleWebService(args.prefix), "/", conf) 53 | -------------------------------------------------------------------------------- /lisien/lisien/tests/regen_test_xml.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | from functools import partial 17 | from itertools import product 18 | from tempfile import TemporaryDirectory 19 | 20 | from lisien.db import PythonDatabaseConnector 21 | from lisien.engine import Engine 22 | from lisien.tests.data import DATA_DIR 23 | 24 | RANDOM_SEED = 69105 25 | 26 | for turns, sim in product([0, 1], ["kobold", "polygons", "wolfsheep"]): 27 | if sim == "kobold": 28 | from lisien.examples.kobold import inittest as install 29 | elif sim == "polygons": 30 | from lisien.examples.polygons import install 31 | elif sim == "wolfsheep": 32 | from lisien.examples.wolfsheep import install 33 | 34 | install = partial(install, seed=RANDOM_SEED) 35 | else: 36 | raise RuntimeError("Unknown sim", sim) 37 | with TemporaryDirectory() as tmp_path: 38 | prefix = os.path.join(tmp_path, "game") 39 | with Engine( 40 | prefix, 41 | workers=0, 42 | random_seed=RANDOM_SEED, 43 | database=PythonDatabaseConnector(), 44 | keyframe_on_close=False, 45 | ) as eng: 46 | install(eng) 47 | for _ in range(turns): 48 | eng.next_turn() 49 | eng.to_xml( 50 | os.path.join(DATA_DIR, f"{sim}_{turns}.xml"), 51 | name="test_export", 52 | ) 53 | -------------------------------------------------------------------------------- /elide/elide/graph/pawn.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | """Widget representing things that move about from place to place.""" 16 | 17 | from elide.pawnspot import GraphPawnSpot 18 | 19 | from ..pawn import PawnBehavior 20 | 21 | 22 | class Pawn(PawnBehavior, GraphPawnSpot): 23 | """A token to represent a :class:`Thing`. 24 | 25 | :class:`Thing` is the lisien class to represent items that are 26 | located in some :class:`Place` or other. Accordingly, 27 | :class:`Pawn`'s coordinates are never set directly; they are 28 | instead derived from the location of the :class:`Thing` 29 | represented. That means a :class:`Pawn` will appear next to the 30 | :class:`Spot` representing the :class:`Place` that its 31 | :class:`Thing` is in. The exception is if the :class:`Thing` is 32 | currently moving from its current :class:`Place` to another one, 33 | in which case the :class:`Pawn` will appear some distance along 34 | the :class:`Arrow` that represents the :class:`Portal` it's moving 35 | through. 36 | 37 | """ 38 | 39 | def _get_location_wid(self): 40 | return self.board.spot[self.loc_name] 41 | 42 | def __repr__(self): 43 | """Give my ``thing``'s name and its location's name.""" 44 | return "<{}-in-{} at {}>".format(self.name, self.loc_name, id(self)) 45 | -------------------------------------------------------------------------------- /recipes/networkx/patches/no-tests.patch: -------------------------------------------------------------------------------- 1 | --- networkx-3.5/pyproject.toml 2025-05-29 23:34:32.000000000 +1200 2 | +++ networkx-3.5-b/pyproject.toml 2025-10-17 17:47:07.119245684 +1300 3 | @@ -127,7 +127,6 @@ 4 | 'networkx.linalg', 5 | 'networkx.readwrite', 6 | 'networkx.readwrite.json_graph', 7 | - 'networkx.tests', 8 | 'networkx.utils', 9 | ] 10 | platforms = [ 11 | @@ -141,43 +140,13 @@ 12 | attr = 'networkx.__version__' 13 | 14 | [tool.setuptools.package-data] 15 | -networkx = ['tests/*.py'] 16 | -"networkx.algorithms" = ['tests/*.py'] 17 | -"networkx.algorithms.assortativity" = ['tests/*.py'] 18 | -"networkx.algorithms.bipartite" = ['tests/*.py'] 19 | -"networkx.algorithms.centrality" = ['tests/*.py'] 20 | -"networkx.algorithms.community" = ['tests/*.py'] 21 | -"networkx.algorithms.components" = ['tests/*.py'] 22 | -"networkx.algorithms.connectivity" = ['tests/*.py'] 23 | -"networkx.algorithms.coloring" = ['tests/*.py'] 24 | -"networkx.algorithms.minors" = ['tests/*.py'] 25 | -"networkx.algorithms.flow" = [ 26 | - 'tests/*.py', 27 | - 'tests/*.bz2', 28 | -] 29 | -"networkx.algorithms.isomorphism" = [ 30 | - 'tests/*.py', 31 | - 'tests/*.*99', 32 | -] 33 | -"networkx.algorithms.link_analysis" = ['tests/*.py'] 34 | -"networkx.algorithms.approximation" = ['tests/*.py'] 35 | -"networkx.algorithms.operators" = ['tests/*.py'] 36 | -"networkx.algorithms.shortest_paths" = ['tests/*.py'] 37 | -"networkx.algorithms.traversal" = ['tests/*.py'] 38 | -"networkx.algorithms.tree" = ['tests/*.py'] 39 | -"networkx.classes" = ['tests/*.py'] 40 | "networkx.generators" = [ 41 | - 'tests/*.py', 42 | 'atlas.dat.gz', 43 | ] 44 | "networkx.drawing" = [ 45 | 'tests/*.py', 46 | 'tests/baseline/*png', 47 | ] 48 | -"networkx.linalg" = ['tests/*.py'] 49 | -"networkx.readwrite" = ['tests/*.py'] 50 | -"networkx.readwrite.json_graph" = ['tests/*.py'] 51 | -"networkx.utils" = ['tests/*.py'] 52 | 53 | [tool.changelist] 54 | ignored_user_logins = ["dependabot[bot]", "pre-commit-ci[bot]", "web-flow"] 55 | -------------------------------------------------------------------------------- /lisien/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | include-package-data=true 7 | packages = ["lisien", "lisien.examples", "lisien.proxy", "lisien.server", "lisien.tests", "lisien.tests.data"] 8 | 9 | 10 | [project] 11 | name = "lisien" 12 | version = "0.23.1" 13 | authors = [ 14 | { name="Zachary Spector", email="public@zacharyspector.com" }, 15 | ] 16 | description = "Life Simulator Engine" 17 | readme = "README.md" 18 | license = "AGPL-3.0-only" 19 | license-files = ["LICENSE"] 20 | requires-python = "~=3.12" 21 | classifiers = [ 22 | "Programming Language :: Python :: 3", 23 | "Operating System :: OS Independent", 24 | "Topic :: Software Development :: Libraries", 25 | "Intended Audience :: Developers", 26 | "Development Status :: 3 - Alpha" 27 | ] 28 | dependencies = [ 29 | "reslot~=0.10", 30 | "blinker~=1.4", 31 | "msgpack~=1.1.0", 32 | "u-msgpack-python~=2.8.0", 33 | "networkx~=3.5", 34 | "sqlalchemy~=2.0", 35 | "numpy~=2.3.4", 36 | "tblib~=3.2.0", 37 | "parquetdb", 38 | "annotated-types~=0.7.0", 39 | "lxml~=6.0", 40 | ] 41 | 42 | [project.urls] 43 | "Homepage" = "https://codeberg.org/clayote/Lisien" 44 | "Bug Tracker" = "https://codeberg.org/clayote/Lisien/issues" 45 | 46 | [tool.pytest.ini_options] 47 | markers = [ 48 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 49 | "big: marks tests as too big to diagnose specific bugs with", 50 | "sqlite: marks tests that exercise the SQLite database backend", 51 | "parquetdb: marks tests that exercise the ParquetDB database backend", 52 | "parallel: marks tests that use some form of parallelism, though possibly just a thread pool", 53 | "proxy: marks tests on the proxy objects that do not start any subprocess for the proxy object to refer to", 54 | "subprocess: marks tests that use the process pool", 55 | "subthread: marks tests that use the thread pool", 56 | "subinterpreter: marks tests that use the interpreter pool; Python 3.14+ required", 57 | ] 58 | -------------------------------------------------------------------------------- /elide/elide/tests/test_strings_editor.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from kivy.base import EventLoop 4 | from kivy.tests import UnitTestTouch 5 | 6 | from lisien import Engine 7 | 8 | from .util import advance_frames, idle_until 9 | 10 | 11 | def test_strings_editor(prefix, elide_app): 12 | assert "lisien" in elide_app.config 13 | app = elide_app 14 | idle_until( 15 | lambda: hasattr(app, "mainscreen"), 100, "app never got mainscreen" 16 | ) 17 | idle_until( 18 | lambda: app.manager.has_screen("timestream"), 19 | 100, 20 | "timestream never added to manager", 21 | ) 22 | 23 | def app_has_engine(): 24 | return hasattr(app, "engine") 25 | 26 | idle_until(app_has_engine, 600, "app never got engine") 27 | idle_until(lambda: app.strings.children, 100, "strings never got children") 28 | idle_until(lambda: app.strings.edbox, 100, "strings never got edbox") 29 | idle_until( 30 | lambda: "physical" in app.mainscreen.graphboards, 31 | 100, 32 | "never got physical in graphboards", 33 | ) 34 | edbox = app.strings.edbox 35 | strings_list = edbox.ids.strings_list 36 | idle_until(lambda: strings_list.store, 100, "strings_list never got store") 37 | strings_ed = edbox.ids.strings_ed 38 | app.strings.toggle() 39 | advance_frames(10) 40 | touchy = UnitTestTouch(*strings_ed.ids.stringname.center) 41 | touchy.touch_down() 42 | EventLoop.idle() 43 | touchy.touch_up() 44 | EventLoop.idle() 45 | strings_ed.ids.stringname.text = "a string" 46 | idle_until(lambda: strings_ed.name == "a string", 100, "name never set") 47 | touchier = UnitTestTouch(*strings_ed.ids.string.center) 48 | touchier.touch_down() 49 | EventLoop.idle() 50 | touchier.touch_up() 51 | advance_frames(10) 52 | strings_ed.ids.string.text = "its value" 53 | idle_until( 54 | lambda: strings_ed.source == "its value", 100, "source never set" 55 | ) 56 | advance_frames(10) 57 | edbox.dismiss() 58 | app.stop() 59 | with Engine(os.path.join(prefix, "test"), workers=0) as eng: 60 | assert "a string" in eng.string 61 | assert eng.string["a string"] == "its value" 62 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | import sys 17 | 18 | 19 | try: 20 | from android.storage import app_storage_path 21 | 22 | def freeze_support(): ... 23 | 24 | wd = app_storage_path() 25 | connect_string = "sqlite:///{prefix}/world.sqlite3" 26 | logs_dir = os.path.join(wd, "app", ".kivy", "logs") 27 | android = True 28 | except ImportError: 29 | from multiprocessing import freeze_support 30 | 31 | wd = os.path.join(os.getcwd()) 32 | logs_dir = connect_string = None 33 | android = False 34 | sys.path.extend([wd, wd + "/lisien", wd + "/elide"]) 35 | 36 | 37 | if __name__ == "__main__": 38 | freeze_support() 39 | from kivy.logger import Logger 40 | from elide.app import ElideApp 41 | from lisien.proxy.manager import Sub 42 | 43 | Logger.setLevel(10) 44 | 45 | app = ElideApp( 46 | prefix=wd, 47 | games_dir="games", 48 | connect_string=connect_string, 49 | logs_dir=logs_dir, 50 | sub_mode=Sub.thread, 51 | android=android, 52 | ) 53 | try: 54 | app.run() 55 | except BaseException as ex: 56 | import traceback 57 | from io import StringIO 58 | from kivy.logger import Logger 59 | 60 | bogus = StringIO() 61 | traceback.print_exception(ex, file=bogus) 62 | for line in bogus.getvalue().split("\n"): 63 | Logger.error(line) 64 | finally: 65 | app._copy_log_files() 66 | -------------------------------------------------------------------------------- /elide/elide/tests/test_python_editor.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from lisien import Engine 4 | from lisien.examples import sickle 5 | 6 | from .util import idle_until 7 | 8 | 9 | def get_actions_box(elide_app): 10 | app = elide_app 11 | idle_until( 12 | lambda: hasattr(app, "mainscreen") 13 | and app.mainscreen.mainview 14 | and app.mainscreen.statpanel 15 | and hasattr(app.mainscreen, "gridview") 16 | ) 17 | app.funcs.toggle() 18 | idle_until( 19 | lambda: "actions" in app.funcs.ids, 100, "Never got actions box" 20 | ) 21 | actions_box = app.funcs.ids.actions 22 | idle_until(lambda: actions_box.editor, 100, "Never got FuncEditor") 23 | idle_until(lambda: actions_box.storelist, 100, "Never got StoreList") 24 | idle_until(lambda: actions_box.store, 100, "Never got FuncStoreProxy") 25 | return actions_box 26 | 27 | 28 | @pytest.mark.usefixtures("sickle_sim") 29 | def test_show_code(elide_app): 30 | app = elide_app 31 | actions_box = get_actions_box(app) 32 | idle_until( 33 | lambda: actions_box.storelist.data, 100, "Never got actions data" 34 | ) 35 | last = actions_box.storelist.data[-1]["name"] 36 | actions_box.storelist.selection_name = last 37 | idle_until( 38 | lambda: "funname" in actions_box.editor.ids, 39 | 100, 40 | "Never got function input widget", 41 | ) 42 | idle_until( 43 | lambda: actions_box.editor.ids.funname.hint_text, 44 | 100, 45 | "Never got function name", 46 | ) 47 | idle_until( 48 | lambda: "code" in actions_box.editor.ids, 49 | 100, 50 | "Never got code editor widget", 51 | ) 52 | idle_until( 53 | lambda: actions_box.editor.ids.code.text, 54 | 100, 55 | "Never got source code", 56 | ) 57 | 58 | 59 | @pytest.mark.usefixtures("kivy") 60 | def test_create_action(prefix, elide_app): 61 | app = elide_app 62 | actions_box = get_actions_box(app) 63 | actions_box.editor.ids.funname.text = "new_func" 64 | actions_box.editor.ids.code.text = 'return "Hello, world!"' 65 | app.stop() 66 | idle_until(lambda: app.stopped, 100, "Didn't stop the app") 67 | with Engine(app.play_path, workers=0) as eng: 68 | assert hasattr(eng.action, "new_func") 69 | -------------------------------------------------------------------------------- /elide/elide/gen.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from functools import partial 16 | 17 | from kivy.lang import Builder 18 | from kivy.properties import NumericProperty, OptionProperty 19 | from kivy.uix.boxlayout import BoxLayout 20 | from networkx import grid_2d_graph 21 | 22 | from lisien.character import grid_2d_8graph 23 | 24 | from .util import logwrap 25 | 26 | 27 | class GridGeneratorDialog(BoxLayout): 28 | xval = NumericProperty() 29 | yval = NumericProperty() 30 | directions = OptionProperty(None, options=[None, 4, 8]) 31 | 32 | def __init__(self, **kwargs): 33 | if "gen.kv" not in Builder.files: 34 | Builder.load_file("gen.kv") 35 | super().__init__(**kwargs) 36 | 37 | @partial(logwrap, section="GridGeneratorDialog") 38 | def generate(self, engine): 39 | x = int(self.xval) 40 | y = int(self.yval) 41 | if x < 1 or y < 1: 42 | return False 43 | elif self.directions == 4: 44 | # instead, we're running just after game init, before the view is open on it, and we'll make a character ourselves 45 | engine.add_character("physical", grid_2d_graph(x, y)) 46 | return True 47 | elif self.directions == 8: 48 | engine.add_character("physical", grid_2d_8graph(x, y)) 49 | return True 50 | else: 51 | return False 52 | 53 | @logwrap(section="GridGeneratorDialog") 54 | def validate(self): 55 | return self.directions and int(self.xval) and int(self.yval) 56 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/misc.atlas: -------------------------------------------------------------------------------- 1 | {"misc-0.png": {"stethoscope": [410, 376, 32, 32], "mirror": [138, 410, 32, 32], "chest": [308, 478, 32, 32], "wax_candle": [138, 342, 32, 32], "relay": [206, 376, 32, 32], "bugle": [172, 478, 32, 32], "candelabrum_of_invocation": [206, 478, 32, 32], "xeroc": [206, 342, 32, 32], "pda": [138, 376, 32, 32], "misc_lamp": [478, 410, 32, 32], "bell": [36, 478, 32, 32], "bell_of_opening": [70, 478, 32, 32], "expensive_camera": [478, 478, 32, 32], "misc_crystal": [308, 410, 32, 32], "misc_deck": [342, 410, 32, 32], "iron_chain": [308, 444, 32, 32], "misc_rune": [70, 376, 32, 32], "tinning_kit": [478, 376, 32, 32], "transistor": [70, 342, 32, 32], "magic_marker": [104, 410, 32, 32], "sack": [240, 376, 32, 32], "misc_book": [206, 410, 32, 32], "key_of_access": [342, 444, 32, 32], "unicorn_horn": [104, 342, 32, 32], "whistle": [172, 342, 32, 32], "heavy_iron_ball": [172, 444, 32, 32], "ice_box": [274, 444, 32, 32], "land_mine": [444, 444, 32, 32], "schroedinger_box": [308, 376, 32, 32], "misc_altar": [172, 410, 32, 32], "grappling_hook": [104, 444, 32, 32], "flute": [70, 444, 32, 32], "misc_orb": [36, 376, 32, 32], "leash": [2, 410, 32, 32], "towel": [36, 342, 32, 32], "statue": [376, 376, 32, 32], "can_of_grease": [240, 478, 32, 32], "ic": [240, 444, 32, 32], "saddle": [274, 376, 32, 32], "misc_stone": [104, 376, 32, 32], "skeleton_key": [342, 376, 32, 32], "crystal_ball": [376, 478, 32, 32], "brass_lantern": [138, 478, 32, 32], "misc_disc": [376, 410, 32, 32], "beartrap": [2, 478, 32, 32], "tallow_candle": [444, 376, 32, 32], "chemistry": [274, 478, 32, 32], "misc_horn": [444, 410, 32, 32], "pick_axe": [172, 376, 32, 32], "credit_card": [342, 478, 32, 32], "tin_opener": [2, 342, 32, 32], "blindfold": [104, 478, 32, 32], "misc_lantern": [2, 376, 32, 32], "lock_pick": [70, 410, 32, 32], "figurine": [2, 444, 32, 32], "misc_box": [274, 410, 32, 32], "lamp": [410, 444, 32, 32], "harp": [138, 444, 32, 32], "large_box": [478, 444, 32, 32], "horn": [206, 444, 32, 32], "drum": [444, 478, 32, 32], "misc_bottle": [240, 410, 32, 32], "floppy": [36, 444, 32, 32], "kit": [376, 444, 32, 32], "lenses": [36, 410, 32, 32], "misc_fan": [410, 410, 32, 32], "diode": [410, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/hand2.atlas: -------------------------------------------------------------------------------- 1 | {"hand2-0.png": {"fire_cyan": [70, 444, 32, 32], "lantern": [240, 444, 32, 32], "shield_kite3": [2, 410, 32, 32], "shield_long_red": [206, 410, 32, 32], "fire_green": [138, 444, 32, 32], "torch": [444, 376, 32, 32], "shield_middle_unicorn": [444, 410, 32, 32], "shield_skull": [376, 376, 32, 32], "shield_middle_brown": [274, 410, 32, 32], "light_yellow": [342, 444, 32, 32], "book_green": [172, 478, 32, 32], "book_yellow": [444, 478, 32, 32], "fire_dark": [104, 444, 32, 32], "book_magenta": [240, 478, 32, 32], "book_green_dim": [206, 478, 32, 32], "shield_diamond_yellow": [410, 444, 32, 32], "shield_round_small2": [240, 376, 32, 32], "book_blue_dim": [70, 478, 32, 32], "shield_middle_gray": [376, 410, 32, 32], "shield_middle_cyan": [308, 410, 32, 32], "shield_middle_ethn": [342, 410, 32, 32], "shield_round_small": [274, 376, 32, 32], "pj": [376, 444, 32, 32], "book_black": [2, 478, 32, 32], "shield_knight_gray": [104, 410, 32, 32], "book_magenta_dim": [274, 478, 32, 32], "shield_middle_black": [240, 410, 32, 32], "book_yellow_dim": [478, 478, 32, 32], "shield_knight_rw": [138, 410, 32, 32], "shield_shaman": [342, 376, 32, 32], "book_red_dim": [342, 478, 32, 32], "book_sky": [376, 478, 32, 32], "shield_of_resistance": [478, 410, 32, 32], "light_red": [308, 444, 32, 32], "shield_knight_blue": [70, 410, 32, 32], "fire_white": [206, 444, 32, 32], "book_cyan_dim": [138, 478, 32, 32], "torsh2": [478, 376, 32, 32], "book_blue": [36, 478, 32, 32], "light_blue": [274, 444, 32, 32], "shield_middle_round": [410, 410, 32, 32], "shield_kite1": [444, 444, 32, 32], "book_cyan": [104, 478, 32, 32], "shield_kite2": [478, 444, 32, 32], "shield_long_cross": [172, 410, 32, 32], "spark": [410, 376, 32, 32], "shield_round4": [104, 376, 32, 32], "shield_kite4": [36, 410, 32, 32], "shield_round7": [206, 376, 32, 32], "shield_round6": [172, 376, 32, 32], "shield_round5": [138, 376, 32, 32], "fire_white2": [172, 444, 32, 32], "shield_round3": [70, 376, 32, 32], "shield_round2": [36, 376, 32, 32], "shield_round1": [2, 376, 32, 32], "shield_round_white": [308, 376, 32, 32], "book_white": [410, 478, 32, 32], "bullseye": [36, 444, 32, 32], "boromir": [2, 444, 32, 32], "book_red": [308, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/base.atlas: -------------------------------------------------------------------------------- 1 | {"base-0.png": {"draconian_f": [342, 478, 32, 32], "halfling_m": [240, 410, 32, 32], "draconian_m": [70, 444, 32, 32], "unseen": [172, 342, 32, 32], "ogre_mage_f": [342, 376, 32, 32], "halfling_f": [206, 410, 32, 32], "draconian_pale_f": [172, 444, 32, 32], "spriggan_f": [36, 342, 32, 32], "human_m": [308, 410, 32, 32], "naga_f": [240, 376, 32, 32], "draconian_gray_m": [478, 478, 32, 32], "draconian_gray_f": [444, 478, 32, 32], "naga_m": [274, 376, 32, 32], "spriggan_m": [70, 342, 32, 32], "ghoul_m": [104, 410, 32, 32], "mummy_m": [206, 376, 32, 32], "kobold_m": [444, 410, 32, 32], "merfolk_m": [36, 376, 32, 32], "draconian_black_f": [274, 478, 32, 32], "draconian_pale_m": [206, 444, 32, 32], "centaur_m": [36, 478, 32, 32], "kobold_f": [410, 410, 32, 32], "mummy_f": [172, 376, 32, 32], "ghoul_f": [70, 410, 32, 32], "centaur_f": [2, 478, 32, 32], "deep_elf_f": [70, 478, 32, 32], "draconian_black_m": [308, 478, 32, 32], "merfolk_f": [478, 410, 32, 32], "demonspawn_m": [240, 478, 32, 32], "ogre_f": [308, 376, 32, 32], "demonspawn_f": [206, 478, 32, 32], "merfolk_fs": [2, 376, 32, 32], "troll_m": [138, 342, 32, 32], "ogre_m": [410, 376, 32, 32], "draconian_mottled_m": [138, 444, 32, 32], "draconian_green_f": [2, 444, 32, 32], "draconian_purple_m": [274, 444, 32, 32], "draconian_red_f": [308, 444, 32, 32], "draconian_mottled_f": [104, 444, 32, 32], "draconian_red_m": [342, 444, 32, 32], "draconian_purple_f": [240, 444, 32, 32], "draconian_green_m": [36, 444, 32, 32], "elf_f": [2, 410, 32, 32], "demigod_m": [172, 478, 32, 32], "gnome_m": [172, 410, 32, 32], "shadow": [2, 342, 32, 32], "orc_m": [478, 376, 32, 32], "gnome_f": [138, 410, 32, 32], "demigod_f": [138, 478, 32, 32], "elf_m": [36, 410, 32, 32], "draconian_white_m": [410, 444, 32, 32], "ogre_mage_m": [376, 376, 32, 32], "minotaur_f": [104, 376, 32, 32], "deep_elf_m": [104, 478, 32, 32], "merfolk_ms": [70, 376, 32, 32], "minotaur_m": [138, 376, 32, 32], "draconian_white_f": [376, 444, 32, 32], "orc_f": [444, 376, 32, 32], "kenku_m": [376, 410, 32, 32], "human_f": [274, 410, 32, 32], "draconian_gold_m": [410, 478, 32, 32], "troll_f": [104, 342, 32, 32], "dwarf_m": [478, 444, 32, 32], "kenku_f": [342, 410, 32, 32], "dwarf_f": [444, 444, 32, 32], "draconian_gold_f": [376, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/statcfg.kv: -------------------------------------------------------------------------------- 1 | : 2 | pos_hint: {'x': 0, 'y': 0} 3 | : 4 | Label: 5 | text: 'True text:' 6 | TextInput: 7 | id: truetext 8 | hint_text: root.true_text 9 | on_text_validate: root.set_true_text() 10 | Label: 11 | text: 'False text:' 12 | TextInput: 13 | id: falsetext 14 | hint_text: root.false_text 15 | on_text_validate: root.set_false_text() 16 | : 17 | Label: 18 | text: 'Minimum:' 19 | TextInput: 20 | id: minimum 21 | hint_text: str(root.min) 22 | multiline: False 23 | on_text_validate: root.set_min() 24 | Label: 25 | text: 'Maximum:' 26 | TextInput: 27 | id: maximum 28 | hint_text: str(root.max) 29 | multiline: False 30 | on_text_validate: root.set_max() 31 | : 32 | height: 30 33 | Button: 34 | size_hint_x: 0.4 / 3 35 | text: 'del' 36 | on_release: root.deleter(root.key) 37 | Label: 38 | size_hint_x: 0.4 / 3 39 | text: str(root.key) 40 | ControlTypePicker: 41 | size_hint_x: 0.4 / 3 42 | key: root.key 43 | set_control: root.set_control 44 | text: root.config['control'] if 'control' in root.config else 'readout' 45 | ConfigListItemCustomizer: 46 | size_hint_x: 0.6 47 | control: root.config['control'] if 'control' in root.config else 'readout' 48 | config: root.config 49 | key: root.key 50 | set_config: root.set_config 51 | : 52 | name: 'statcfg' 53 | statcfg: cfg 54 | BoxLayout: 55 | orientation: 'vertical' 56 | StatListViewConfigurator: 57 | viewclass: 'ConfigListItem' 58 | id: cfg 59 | app: app 60 | proxy: root.proxy 61 | statlist: root.statlist 62 | size_hint_y: 0.95 63 | RecycleBoxLayout: 64 | default_size: None, dp(56) 65 | default_size_hint: 1, None 66 | size_hint_y: None 67 | height: self.minimum_height 68 | orientation: 'vertical' 69 | BoxLayout: 70 | orientation: 'horizontal' 71 | size_hint_y: 0.05 72 | TextInput: 73 | id: newstatkey 74 | multiline: False 75 | write_tab: False 76 | hint_text: 'New stat' 77 | TextInput: 78 | id: newstatval 79 | multiline: False 80 | write_tab: False 81 | hint_text: 'Value' 82 | Button: 83 | id: newstatbut 84 | text: '+' 85 | on_release: root.new_stat() 86 | Button: 87 | id: closer 88 | text: 'Close' 89 | on_release: root.toggle() -------------------------------------------------------------------------------- /elide/elide/rulesview.kv: -------------------------------------------------------------------------------- 1 | : 2 | text: self.rule.name if self.rule else '' 3 | : 4 | orientation: 'horizontal' 5 | button: rule_button 6 | BoxLayout: 7 | orientation: 'vertical' 8 | size_hint_x: 0.2 9 | Button: 10 | id: up 11 | text: '↑' 12 | font_name: 'Symbola' 13 | on_release: root.move_rule_up() 14 | Button: 15 | id: down 16 | text: '↓' 17 | font_name: 'Symbola' 18 | on_release: root.move_rule_down() 19 | RuleButton: 20 | id: rule_button 21 | rulesview: root.rulesview 22 | ruleslist: root.ruleslist 23 | rule: root.rule 24 | size_hint_x: 0.8 25 | : 26 | viewclass: 'RuleButtonBox' 27 | SelectableRecycleBoxLayout: 28 | default_size: None, dp(56) 29 | default_size_hint: 1, None 30 | height: self.minimum_height 31 | size_hint_y: None 32 | orientation: 'vertical' 33 | : 34 | new_rule_name: rulename.text 35 | ruleslist: ruleslist 36 | rulesview: rulesview 37 | rulebook_name: str(self.rulebook.name) if self.rulebook is not None else '' 38 | entity_name: str(self.entity.name) if self.entity is not None else '' 39 | orientation: 'vertical' 40 | Label: 41 | text: root.entity_name + ' - ' + root.rulebook_name 42 | size_hint_y: 0.05 43 | BoxLayout: 44 | orientation: 'horizontal' 45 | RulesList: 46 | id: ruleslist 47 | rulebook: root.rulebook 48 | entity: root.entity 49 | rulesview: rulesview 50 | size_hint_x: 0.2 51 | RulesView: 52 | id: rulesview 53 | rulebook: root.rulebook 54 | entity: root.entity 55 | size_hint_x: 0.8 56 | BoxLayout: 57 | orientation: 'horizontal' 58 | size_hint_y: 0.07 59 | TextInput: 60 | id: rulename 61 | hint_text: 'New rule name' 62 | write_tab: False 63 | multiline: False 64 | Button: 65 | text: '+' 66 | on_release: root.new_rule() 67 | Button: 68 | id: closebut 69 | text: 'Close' 70 | on_release: root.toggle() 71 | : 72 | name: 'rules' 73 | rulesview: box.rulesview 74 | box: box 75 | RulesBox: 76 | id: box 77 | rulebook: root.rulebook 78 | entity: root.entity 79 | toggle: root.toggle 80 | : 81 | name: 'charrules' -------------------------------------------------------------------------------- /elide/elide/charmenu.kv: -------------------------------------------------------------------------------- 1 | 2 | : 3 | orientation: 'vertical' 4 | dummyplace: dummyplace 5 | dummything: dummything 6 | portaladdbut: portaladdbut 7 | portaldirbut: portaldirbut 8 | Button: 9 | text: 'Logs' 10 | on_release: app.log_screen.toggle() 11 | Button: 12 | text: 'Delete' 13 | disabled: app.edit_locked 14 | on_release: app.delete_selection() 15 | Button: 16 | id: timestreambut 17 | text: 'Timestream' 18 | disabled: app.edit_locked 19 | on_release: root.toggle_timestream() 20 | Button: 21 | id: gridviewbut 22 | text: 'Toggle grid' 23 | on_release: root.toggle_gridview() 24 | Button: 25 | text: 'Strings' 26 | disabled: app.edit_locked 27 | on_release: root.toggle_strings_editor() 28 | Button: 29 | text: 'Python' 30 | disabled: app.edit_locked 31 | on_release: root.toggle_funcs_editor() 32 | Button: 33 | text: 'Rules' 34 | id: rules_button 35 | disabled: app.edit_locked 36 | on_release: root.toggle_rules() 37 | Button: 38 | text: 'Characters' 39 | on_release: root.toggle_chars_screen() 40 | BoxLayout: 41 | Widget: 42 | id: placetab 43 | Dummy: 44 | id: dummyplace 45 | center: placetab.center 46 | prefix: 'place' 47 | disabled: app.edit_locked 48 | on_pos_up: root.spot_from_dummy(self) 49 | Button: 50 | text: 'cfg' 51 | disabled: app.edit_locked 52 | on_release: root.toggle_spot_cfg() 53 | BoxLayout: 54 | Widget: 55 | id: thingtab 56 | Dummy: 57 | id: dummything 58 | center: thingtab.center 59 | prefix: 'thing' 60 | disabled: app.edit_locked 61 | on_pos_up: root.pawn_from_dummy(self) 62 | Button: 63 | text: 'cfg' 64 | disabled: app.edit_locked 65 | on_release: root.toggle_pawn_cfg() 66 | BoxLayout: 67 | orientation: 'vertical' 68 | ToggleButton: 69 | id: portaladdbut 70 | disabled: app.edit_locked 71 | on_state: root._trigger_deselect() 72 | Widget: 73 | id: emptyleft 74 | center_x: portaladdbut.x + portaladdbut.width / 3 75 | center_y: portaladdbut.center_y 76 | size: (0, 0) 77 | Widget: 78 | id: emptyright 79 | center_x: portaladdbut.right - portaladdbut.width / 3 80 | center_y: portaladdbut.center_y 81 | size: (0, 0) 82 | Button: 83 | id: portaldirbut 84 | text: 'One-way' if root.reciprocal_portal else 'Two-way' 85 | disabled: app.edit_locked 86 | on_release: root.toggle_reciprocal() 87 | Button: 88 | text: 'Quit' 89 | id: quit_button 90 | on_release: root.close_game() -------------------------------------------------------------------------------- /check_version.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | import sys 4 | import tomllib 5 | 6 | SOFT_REQUIREMENTS = {"lxml", "parquetdb"} 7 | 8 | DEP_NAME_PAT = r"([a-zA-Z0-9_-]+)([~!<>=]=)?.*" 9 | REQUIREMENTS_PAT = r"requirements *= *(.+)" 10 | 11 | with open("lisien/pyproject.toml", "rb") as inf: 12 | loaded = tomllib.load(inf) 13 | lisien_version_str = loaded["project"]["version"] 14 | deps = { 15 | re.match(DEP_NAME_PAT, dep).group(1) 16 | for dep in loaded["project"]["dependencies"] 17 | } - SOFT_REQUIREMENTS 18 | with open("elide/pyproject.toml", "rb") as inf: 19 | loaded = tomllib.load(inf) 20 | elide_version_str = loaded["project"]["version"] 21 | for dependency in loaded["project"]["dependencies"]: 22 | if not dependency.startswith("lisien"): 23 | deps.add(re.match(DEP_NAME_PAT, dependency).group(1)) 24 | continue 25 | _, ver = dependency.split("==") 26 | if ver != lisien_version_str: 27 | raise RuntimeError( 28 | f"Elide depends on Lisien version {ver}, not {lisien_version_str}" 29 | ) 30 | break 31 | else: 32 | raise RuntimeError("Elide doesn't depend on Lisien") 33 | with open("buildozer.spec", "rt") as inf: 34 | for line in inf: 35 | if reqs := re.match(REQUIREMENTS_PAT, line): 36 | deps.difference_update(reqs.group(1).split(",")) 37 | break 38 | else: 39 | sys.exit("No requirements line in buildozer.spec") 40 | 41 | if deps: 42 | sys.exit(f"Requirements missing from buildozer.spec: {', '.join(deps)}") 43 | 44 | pat = r"(\d+?\.\d+?\.\d+?)(\.post[0-9]+)?" 45 | lisien_version = re.match(pat, lisien_version_str).group(1) 46 | elide_version = re.match(pat, elide_version_str).group(1) 47 | 48 | if not (lisien_version == elide_version): 49 | sys.exit( 50 | f"Version numbers differ. lisien: {lisien_version}, elide: {elide_version}" 51 | ) 52 | 53 | output = subprocess.check_output( 54 | [sys.executable, "-m", "pip", "index", "versions", "lisien"], text=True 55 | ) 56 | vers = () 57 | for line in output.split("\n"): 58 | if line.startswith("Available versions: "): 59 | vers = { 60 | ver.strip() 61 | for ver in line.removeprefix("Available versions: ").split(",") 62 | } 63 | break 64 | 65 | if lisien_version_str in vers: 66 | sys.exit(f"Version {lisien_version_str} is already in PyPI.") 67 | 68 | with open("CHANGES.txt", "rt") as inf: 69 | first_line = next(inf) 70 | if lisien_version_str not in first_line: 71 | sys.exit( 72 | f"Version {lisien_version_str} is not the top entry in CHANGES.txt." 73 | ) 74 | print(lisien_version_str) 75 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/head.atlas: -------------------------------------------------------------------------------- 1 | {"head-0.png": {"fhelm_gray3": [342, 444, 32, 32], "turban_brown": [104, 342, 32, 32], "wizard_white": [444, 342, 32, 32], "wizard_darkgreen": [342, 342, 32, 32], "chain": [444, 478, 32, 32], "wizard_blackred": [206, 342, 32, 32], "straw": [410, 376, 32, 32], "bandana_ybrown": [36, 478, 32, 32], "fhelm_horn2": [376, 444, 32, 32], "band_blue": [70, 478, 32, 32], "feather_blue": [172, 444, 32, 32], "hood_gray": [308, 410, 32, 32], "band_red": [138, 478, 32, 32], "fhelm_horn_yellow": [410, 444, 32, 32], "taiso_blue": [444, 376, 32, 32], "helm_green": [138, 410, 32, 32], "healer": [70, 410, 32, 32], "horn_gray": [172, 376, 32, 32], "blue_horn_gold": [308, 478, 32, 32], "crown_gold": [104, 444, 32, 32], "black_horn": [274, 478, 32, 32], "hood_red": [478, 410, 32, 32], "hood_ybrown": [70, 376, 32, 32], "taiso_red": [2, 342, 32, 32], "mummy": [342, 376, 32, 32], "full_black": [444, 444, 32, 32], "hood_white": [36, 376, 32, 32], "horn_evil": [138, 376, 32, 32], "dyrovepreva": [138, 444, 32, 32], "wizard_blackgold": [172, 342, 32, 32], "feather_green": [206, 444, 32, 32], "feather_yellow": [308, 444, 32, 32], "clown1": [2, 444, 32, 32], "brown_gold": [342, 478, 32, 32], "helm_red": [206, 410, 32, 32], "cone_red": [70, 444, 32, 32], "iron2": [240, 376, 32, 32], "black_horn2": [240, 478, 32, 32], "cap_black1": [376, 478, 32, 32], "wizard_brown": [308, 342, 32, 32], "hat_black": [36, 410, 32, 32], "helm_gimli": [104, 410, 32, 32], "hood_white2": [2, 376, 32, 32], "turban_white": [138, 342, 32, 32], "cap_blue": [410, 478, 32, 32], "hood_orange": [410, 410, 32, 32], "taiso_white": [36, 342, 32, 32], "taiso_yellow": [70, 342, 32, 32], "full_gold": [478, 444, 32, 32], "wizard_red": [410, 342, 32, 32], "helm_plume": [172, 410, 32, 32], "art_dragonhelm": [2, 478, 32, 32], "wizard_bluegreen": [274, 342, 32, 32], "hood_cyan": [274, 410, 32, 32], "gandalf": [2, 410, 32, 32], "hood_green2": [342, 410, 32, 32], "feather_red": [240, 444, 32, 32], "iron3": [274, 376, 32, 32], "cone_blue": [36, 444, 32, 32], "iron1": [206, 376, 32, 32], "band_yellow": [206, 478, 32, 32], "yellow_wing": [478, 342, 32, 32], "wizard_blue": [240, 342, 32, 32], "cheek_red": [478, 478, 32, 32], "horned": [104, 376, 32, 32], "band_magenta": [104, 478, 32, 32], "hood_black2": [240, 410, 32, 32], "iron_red": [308, 376, 32, 32], "taiso_magenta": [478, 376, 32, 32], "hood_red2": [444, 410, 32, 32], "feather_white": [274, 444, 32, 32], "ninja_black": [376, 376, 32, 32], "hood_green": [376, 410, 32, 32], "wizard_purple": [376, 342, 32, 32], "band_white": [172, 478, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/dungeon.atlas: -------------------------------------------------------------------------------- 1 | {"dungeon-0.png": {"dngn_rock_wall_14": [138, 222, 32, 32], "dngn_rock_wall_15": [172, 222, 32, 32], "dngn_rock_wall_11": [36, 222, 32, 32], "dngn_wax_wall": [36, 154, 32, 32], "dngn_rock_wall_10": [2, 222, 32, 32], "dngn_stone_wall": [104, 188, 32, 32], "dngn_silver_statue": [2, 188, 32, 32], "dngn_stone_arch": [70, 188, 32, 32], "dngn_trap_iii": [172, 188, 32, 32], "dngn_transit_pandemonium": [138, 188, 32, 32], "dngn_rock_wall_13": [104, 222, 32, 32], "dngn_rock_wall_12": [70, 222, 32, 32], "dngn_shallow_water": [206, 222, 32, 32], "dngn_trap_mechanical": [2, 154, 32, 32], "dngn_trap_magical": [206, 188, 32, 32], "dngn_sparkling_fountain": [36, 188, 32, 32]}, "dungeon-0.png": {"dngn_altar": [2, 222, 32, 32], "dngn_altar_trog": [36, 188, 32, 32], "dngn_altar_okawaru": [172, 222, 32, 32], "dngn_rock_wall_03": [2, 18, 32, 32], "dngn_rock_wall_02": [206, 52, 32, 32], "dngn_altar_shining_one": [206, 222, 32, 32], "dngn_dry_fountain": [70, 154, 32, 32], "dngn_orange_crystal_statue": [206, 86, 32, 32], "dngn_enter_gehenna": [2, 120, 32, 32], "dngn_granite_statue": [36, 86, 32, 32], "dngn_deep_water": [36, 154, 32, 32], "dngn_enter_abyss": [104, 154, 32, 32], "dngn_rock_stairs_down": [70, 52, 32, 32], "dngn_altar_xom": [104, 188, 32, 32], "dngn_altar_elyvilon": [36, 222, 32, 32], "dngn_rock_wall_05": [70, 18, 32, 32], "dngn_green_crystal_wall": [70, 86, 32, 32], "dngn_blue_fountain": [206, 188, 32, 32], "dngn_altar_kikubaaqudgha": [70, 222, 32, 32], "dngn_rock_wall_00": [138, 52, 32, 32], "dngn_enter": [138, 154, 32, 32], "dngn_rock_wall_04": [36, 18, 32, 32], "dngn_rock_wall_06": [104, 18, 32, 32], "dngn_closed_door": [2, 154, 32, 32], "dngn_enter_labyrinth": [70, 120, 32, 32], "dngn_enter_dis": [206, 154, 32, 32], "dngn_altar_vehumet": [70, 188, 32, 32], "dngn_enter_pandemonium": [104, 120, 32, 32], "dngn_lava": [104, 86, 32, 32], "dngn_metal_wall": [138, 86, 32, 32], "dngn_altar_sif_muna": [2, 188, 32, 32], "dngn_rock_wall_01": [172, 52, 32, 32], "dngn_open_door": [172, 86, 32, 32], "dngn_altar_zin": [172, 188, 32, 32], "dngn_altar_nemelex_xobeh": [138, 222, 32, 32], "dngn_rock_wall_09": [206, 18, 32, 32], "dngn_exit": [2, 86, 32, 32], "dngn_enter_hell": [36, 120, 32, 32], "dngn_rock_wall_07": [138, 18, 32, 32], "dngn_orcish_idol": [2, 52, 32, 32], "dngn_enter_cocytus": [172, 154, 32, 32], "dngn_enter_tartarus": [172, 120, 32, 32], "dngn_altar_makhleb": [104, 222, 32, 32], "dngn_altar_yredelemnul": [138, 188, 32, 32], "dngn_return": [36, 52, 32, 32], "dngn_rock_wall_08": [172, 18, 32, 32], "dngn_entrance": [206, 120, 32, 32], "dngn_enter_shop": [138, 120, 32, 32], "dngn_rock_stairs_up": [104, 52, 32, 32]}} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import os 16 | 17 | from setuptools import find_packages, setup 18 | 19 | here = os.path.dirname(__file__) 20 | 21 | with open(os.path.join(here, "lisien", "pyproject.toml"), "rt") as inf: 22 | for line in inf: 23 | if line.startswith("version"): 24 | _, version, _ = line.split('"') 25 | break 26 | else: 27 | raise ValueError("Couldn't get version") 28 | 29 | deps = {} 30 | vers = {} 31 | for subpkg in ["lisien", "elide"]: 32 | with open(os.path.join(here, subpkg, "pyproject.toml"), "rt") as inf: 33 | for line in inf: 34 | if line.startswith("version"): 35 | _, version, _ = line.split('"') 36 | vers[subpkg] = tuple(map(int, version.split("."))) 37 | if line.startswith("dependencies"): 38 | break 39 | else: 40 | raise ValueError("Couldn't get %s dependencies" % subpkg) 41 | deps[subpkg] = [] 42 | for line in inf: 43 | if line == "]\n": 44 | break 45 | _, dep, _ = line.split('"') 46 | if not dep.startswith("lisien"): 47 | deps[subpkg].append(dep) 48 | else: 49 | raise ValueError("%s dependencies never ended" % subpkg) 50 | datas = [] 51 | with open(os.path.join(here, "elide", "MANIFEST.in"), "rt") as inf: 52 | for line in inf: 53 | if line[: len("include")] == "include": 54 | line = line[len("include ") :] 55 | if line[-1] == "\n": 56 | line = line[:-1] 57 | datas.append(os.path.join(here, "elide", line)) 58 | 59 | setup( 60 | name="lisien_with_elide", 61 | version=".".join(map(str, max((vers["lisien"], vers["elide"])))) + "dev", 62 | packages=find_packages(os.path.join(here, "lisien")) 63 | + find_packages(os.path.join(here, "elide")), 64 | package_dir={ 65 | "lisien": "lisien/lisien", 66 | "elide": "elide/elide", 67 | }, 68 | install_requires=deps["lisien"] + deps["elide"], 69 | package_data={"elide": datas}, 70 | include_package_data=True, 71 | ) 72 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_msgpack.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from lisien import Engine 16 | from lisien.proxy.manager import EngineProxyManager 17 | 18 | 19 | def test_serialize_character(sqleng): 20 | char = sqleng.new_character("physical") 21 | assert sqleng.unpack(sqleng.pack(char)) == char 22 | 23 | 24 | def test_serialize_thing(sqleng): 25 | char = sqleng.new_character("physical") 26 | place = char.new_place("here") 27 | thing = place.new_thing("that") 28 | assert sqleng.unpack(sqleng.pack(thing)) == thing 29 | 30 | 31 | def test_serialize_place(sqleng): 32 | char = sqleng.new_character("physical") 33 | place = char.new_place("here") 34 | assert sqleng.unpack(sqleng.pack(place)) == place 35 | 36 | 37 | def test_serialize_portal(sqleng): 38 | char = sqleng.new_character("physical") 39 | a = char.new_place("a") 40 | b = char.new_place("b") 41 | port = a.new_portal(b) 42 | assert sqleng.unpack(sqleng.pack(port)) == port 43 | 44 | 45 | def test_serialize_function(tmp_path): 46 | with Engine( 47 | tmp_path, random_seed=69105, enforce_end_of_time=False, workers=0 48 | ) as eng: 49 | 50 | @eng.function 51 | def foo(bar: str, bas: str) -> str: 52 | return bar + bas + " is correct" 53 | 54 | procm = EngineProxyManager(workers=0) 55 | try: 56 | engprox = procm.start(tmp_path) 57 | funcprox = engprox.function.foo 58 | assert funcprox("foo", "bar") == "foobar is correct" 59 | finally: 60 | procm.shutdown() 61 | 62 | 63 | def test_serialize_method(tmp_path): 64 | with Engine( 65 | tmp_path, random_seed=69105, enforce_end_of_time=False, workers=0 66 | ) as eng: 67 | 68 | @eng.method 69 | def foo(self, bar: str, bas: str) -> str: 70 | return bar + bas + " is correct" 71 | 72 | procm = EngineProxyManager() 73 | try: 74 | engprox = procm.start(tmp_path, workers=0) 75 | assert engprox.foo("bar", "bas") == "barbas is correct" 76 | finally: 77 | procm.shutdown() 78 | -------------------------------------------------------------------------------- /updbuildozer.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | """Update the lisien and elide packages in .buildozer in-place""" 16 | 17 | import os 18 | import shutil 19 | 20 | pardirs = [] 21 | prefix = ".buildozer/android/platform/" 22 | for plat in os.listdir(prefix): 23 | if "dists" in os.listdir(os.path.join(prefix, plat)): 24 | elide_dist_dir = os.path.join(prefix, plat, "dists", "Elide") 25 | if not os.path.isdir(elide_dist_dir): 26 | continue 27 | for dist_dir in os.listdir(elide_dist_dir): 28 | if dist_dir.startswith("_python_bundle"): 29 | pardirs.append( 30 | os.path.join( 31 | prefix, 32 | plat, 33 | "dists", 34 | "Elide", 35 | dist_dir, 36 | "_python_bundle", 37 | "site-packages", 38 | ) 39 | ) 40 | if "build" not in os.listdir(os.path.join(prefix, plat)): 41 | continue 42 | for build_dir in os.listdir(os.path.join(prefix, plat, "build")): 43 | if build_dir not in {"other_builds", "python-installs"}: 44 | continue 45 | if build_dir == "other_builds": 46 | pardirs.append(os.path.join(prefix, plat, "build", build_dir)) 47 | elif build_dir == "python-installs": 48 | pre = os.path.join(prefix, plat, "build", build_dir, "Elide") 49 | if os.path.exists(pre) and os.path.isdir(pre): 50 | pardirs.extend( 51 | os.path.join(pre, arch) for arch in os.listdir(pre) 52 | ) 53 | print("looking for built packages in", pardirs) 54 | for pardir in pardirs: 55 | for package in os.listdir(pardir): 56 | if package.lower() == "lisien": 57 | abspath = os.path.join(pardir, package) 58 | print("replacing", abspath, "with current lisien") 59 | shutil.rmtree(abspath) 60 | shutil.copytree("lisien/lisien", abspath) 61 | elif package.lower() == "elide": 62 | abspath = os.path.join(pardir, package) 63 | print("replacing", abspath, "with current elide") 64 | shutil.rmtree(abspath) 65 | shutil.copytree("elide/elide", abspath) 66 | -------------------------------------------------------------------------------- /elide/elide/boardscatter.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from functools import partial 16 | 17 | from kivy.graphics.transformation import Matrix 18 | from kivy.uix.scatter import ScatterPlane 19 | 20 | from .util import logwrap 21 | 22 | wraplog_bsp = partial(logwrap, section="BoardScatterPlane") 23 | 24 | 25 | class BoardScatterPlane(ScatterPlane): 26 | @wraplog_bsp 27 | def on_touch_down(self, touch): 28 | if touch.is_mouse_scrolling: 29 | scale = self.scale + ( 30 | 0.05 if touch.button == "scrolldown" else -0.05 31 | ) 32 | if (self.scale_min and scale < self.scale_min) or ( 33 | self.scale_max and scale > self.scale_max 34 | ): 35 | return 36 | rescale = scale * 1.0 / self.scale 37 | self.apply_transform( 38 | Matrix().scale(rescale, rescale, rescale), 39 | post_multiply=True, 40 | anchor=self.to_local(*touch.pos), 41 | ) 42 | return self.dispatch("on_transform_with_touch", touch) 43 | return super().on_touch_down(touch) 44 | 45 | @wraplog_bsp 46 | def apply_transform(self, trans, post_multiply=False, anchor=(0, 0)): 47 | super().apply_transform( 48 | trans, post_multiply=post_multiply, anchor=anchor 49 | ) 50 | self._last_transform = trans, post_multiply, anchor 51 | 52 | @wraplog_bsp 53 | def on_transform_with_touch(self, touch): 54 | x, y = self.pos 55 | w = self.board.width * self.scale 56 | h = self.board.height * self.scale 57 | if hasattr(self, "_last_transform") and ( 58 | w < self.parent.width or h < self.parent.height 59 | ): 60 | trans, post_multiply, anchor = self._last_transform 61 | super().apply_transform(trans.inverse(), post_multiply, anchor) 62 | return 63 | if x > self.parent.x: 64 | self.x = self.parent.x 65 | if y > self.parent.y: 66 | self.y = self.parent.y 67 | if x + w < self.parent.right: 68 | self.x = self.parent.right - w 69 | if y + h < self.parent.top: 70 | self.y = self.parent.top - h 71 | -------------------------------------------------------------------------------- /elide/elide/util.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from functools import partial, wraps 16 | 17 | from kivy.lang import Builder 18 | from kivy.logger import Logger 19 | from kivy.resources import resource_find 20 | from kivy.uix.behaviors import FocusBehavior 21 | from kivy.uix.recycleboxlayout import RecycleBoxLayout 22 | from kivy.uix.recycleview.layout import LayoutSelectionBehavior 23 | 24 | from lisien.util import format_call_sig 25 | 26 | 27 | class SelectableRecycleBoxLayout( 28 | FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout 29 | ): 30 | pass 31 | 32 | 33 | def load_kv(filename: str) -> None: 34 | """Load a kv file unless it's already been loaded""" 35 | filename = resource_find(filename) 36 | if not filename: 37 | raise FileNotFoundError(filename) 38 | if filename in Builder.files: 39 | return 40 | Builder.load_file(filename) 41 | 42 | 43 | def dummynum(character, name): 44 | """Count how many nodes there already are in the character whose name 45 | starts the same. 46 | 47 | """ 48 | num = 0 49 | for nodename in character.node: 50 | nodename = str(nodename) 51 | if nodename[: len(name)] != name: 52 | continue 53 | try: 54 | nodenum = int(nodename.lstrip(name)) 55 | except ValueError: 56 | continue 57 | num = max((nodenum, num)) 58 | return num 59 | 60 | 61 | def logwrap(func=None, *, section="ElideApp"): 62 | if func is None: 63 | return partial(logwrap, section=section) 64 | 65 | @wraps(func) 66 | def fn(*args, **kwargs): 67 | Logger.debug(section + ": " + format_call_sig(func, *args, **kwargs)) 68 | try: 69 | ret = func(*args, **kwargs) 70 | finally: 71 | for handler in Logger.handlers: 72 | # ensure any files get sync'd 73 | if hasattr(handler, "fd"): 74 | handler.fd.flush() 75 | return ret 76 | 77 | return fn 78 | 79 | 80 | def devour(s): 81 | """Iterate over items in s while removing them""" 82 | while s: 83 | yield s.pop() 84 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_neighborhood.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from itertools import product 16 | 17 | import networkx as nx 18 | import pytest 19 | 20 | from lisien.character import grid_2d_8graph 21 | 22 | 23 | @pytest.fixture 24 | def three3(engine): 25 | return engine.new_character("3x3", grid_2d_8graph(3, 3)) 26 | 27 | 28 | def test_sim_start(three3): 29 | @three3.place.rule(neighborhood=1, always=True) 30 | def did_it_run(place): 31 | if "it_ran" in place: 32 | place.character.stat["it_ran"] = ( 33 | place.character.stat.get("it_ran", 0) + 1 34 | ) 35 | else: 36 | place["it_ran"] = True 37 | 38 | eng = three3.engine 39 | assert eng.action.get_source("did_it_run") 40 | eng.next_turn() 41 | assert three3.place[1, 1]["it_ran"] 42 | assert "it_ran" not in three3.stat 43 | eng.next_turn() 44 | assert three3.place[1, 1]["it_ran"] 45 | assert three3.stat["it_ran"] == 9 46 | eng.next_turn() 47 | assert three3.stat["it_ran"] == 9 # still! 48 | 49 | 50 | @pytest.mark.parametrize( 51 | ("branched", "rulebook"), product(*[(True, False)] * 2) 52 | ) 53 | def test_rule_neighborhood(serial_engine, branched, rulebook): 54 | """Test a rule applied to all nodes of a character with a neighborhood""" 55 | char = serial_engine.new_character("char", nx.grid_2d_graph(5, 5)) 56 | 57 | @serial_engine.rule 58 | def it_ran(node): 59 | node["it_ran"] = True 60 | 61 | @it_ran.trigger 62 | def should_it_run(node): 63 | node["trigger_evaluated"] = True 64 | for neighbor in node.neighbors(): 65 | if neighbor.get("should_run"): 66 | return True 67 | return False 68 | 69 | it_ran.neighborhood = 1 70 | assert it_ran.neighborhood == 1 71 | if rulebook: 72 | serial_engine.rulebook["it_ran"] = [it_ran] 73 | 74 | serial_engine.next_turn() 75 | if rulebook: 76 | char.place.rulebook = "it_ran" 77 | else: 78 | char.place.rule(it_ran) 79 | char.place[3, 3]["should_run"] = True 80 | assert it_ran.neighborhood == 1 81 | if branched: 82 | serial_engine.branch = "eeeee" 83 | serial_engine.next_turn() 84 | 85 | for nabor in [(2, 3), (4, 3), (3, 4), (3, 2)]: 86 | assert char.place[nabor]["it_ran"] 87 | 88 | for outer in [(1, 1), (4, 4)]: 89 | assert "trigger_evaluated" not in char.place[outer] 90 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_time_travel.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from collections import defaultdict 16 | 17 | 18 | def test_build_keyframe_window(null_engine): 19 | null_engine._branch_parents = defaultdict( 20 | set, {"lol": {"trunk"}, "omg": {"trunk", "lol"}, "trunk": {None}} 21 | ) 22 | null_engine._keyframes_loaded = { 23 | ("lol", 5, 241), 24 | ("lol", 9, 37), 25 | ("lol", 10, 163), 26 | ("lol", 10, 2187), 27 | ("trunk", 0, 0), 28 | ("trunk", 0, 2), 29 | ("trunk", 0, 3), 30 | ("trunk", 5, 240), 31 | ("trunk", 8, 566), 32 | ("trunk", 10, 578), 33 | ("trunk", 10, 3139), 34 | } 35 | for b, r, t in null_engine._keyframes_loaded: 36 | if b in null_engine._keyframes_dict: 37 | if r in null_engine._keyframes_dict[b]: 38 | null_engine._keyframes_dict[b][r].add(t) 39 | else: 40 | null_engine._keyframes_dict[b][r] = {t} 41 | else: 42 | null_engine._keyframes_dict[b] = {r: {t}} 43 | null_engine._branches_d.update( 44 | { 45 | "lol": ("trunk", 5, 240, 10, 3877), 46 | "omg": ("lol", 5, 241, 5, 241), 47 | "trunk": (None, 0, 0, 10, 3284), 48 | } 49 | ) 50 | assert null_engine._build_keyframe_window("lol", 5, 241) == ( 51 | ("lol", 5, 241), 52 | ("lol", 9, 37), 53 | ) 54 | assert null_engine._build_keyframe_window("omg", 5, 241) == ( 55 | ("lol", 5, 241), 56 | None, 57 | ) 58 | assert null_engine._build_keyframe_window("lol", 5, 242) == ( 59 | ("lol", 5, 241), 60 | ("lol", 9, 37), 61 | ) 62 | assert null_engine._build_keyframe_window("omg", 5, 241) == ( 63 | ("lol", 5, 241), 64 | None, 65 | ) 66 | assert null_engine._build_keyframe_window("omg", 6, 0) == ( 67 | ("lol", 5, 241), 68 | None, 69 | ) 70 | assert null_engine._build_keyframe_window("lol", 5, 240) == ( 71 | ("trunk", 5, 240), 72 | ("lol", 5, 241), 73 | ) 74 | assert null_engine._build_keyframe_window("trunk", 0, 2) == ( 75 | ("trunk", 0, 2), 76 | ("trunk", 0, 3), 77 | ) 78 | 79 | 80 | def test_bookmark(engine): 81 | engine.bookmark("a") 82 | engine.next_turn() 83 | engine.bookmark("b") 84 | engine.branch = "branch" 85 | engine.bookmark("c") 86 | assert engine.time == ("branch", 1, 0) 87 | engine.bookmark("b") 88 | assert engine.time == ("trunk", 1, 0) 89 | engine.bookmark("a") 90 | assert engine.time == ("trunk", 0, 0) 91 | -------------------------------------------------------------------------------- /lisien/lisien/examples/pathfind.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import networkx as nx 4 | 5 | 6 | def install(eng, seed=None, width=25, height=25): 7 | eng.eternal["width"] = width 8 | eng.eternal["height"] = height 9 | if seed is not None: 10 | random.seed(seed) 11 | grid: nx.Graph = nx.grid_2d_graph(width, height) 12 | 13 | for node in list(grid): 14 | if random.random() < 0.1: 15 | grid.remove_node(node) 16 | elif random.random() < 0.01: 17 | grid.add_node(f"{node}_inhabitant", location=node) 18 | 19 | phys = eng.new_character("physical", grid) 20 | 21 | @eng.function 22 | def find_path_somewhere(node): 23 | from math import sqrt 24 | 25 | from networkx.algorithms import astar_path 26 | 27 | x, y = node.location.name 28 | width = node.engine.eternal["width"] 29 | height = node.engine.eternal["height"] 30 | destx = width - int(x) 31 | desty = height - int(y) 32 | while (destx, desty) not in node.character.place: 33 | if destx < width - 1: 34 | destx += 1 35 | elif desty < height - 1: 36 | destx = 0 37 | desty += 1 38 | else: 39 | destx = desty = 0 40 | ret = astar_path( 41 | node.character, 42 | node.location.name, 43 | (destx, desty), 44 | lambda a, b: sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2), 45 | ) 46 | node.engine.debug( 47 | f"{node.name}'s shortest path to {destx, desty} is {ret}" 48 | ) 49 | return ret 50 | 51 | @phys.rule(big=True) 52 | def go_places(char): 53 | from time import monotonic 54 | 55 | from networkx.exception import NetworkXNoPath 56 | 57 | def log_as_completed(fut): 58 | try: 59 | char.engine.debug( 60 | f"Got path for {fut.thing.name}: {fut.result()}" 61 | ) 62 | except NetworkXNoPath: 63 | char.engine.debug(f"No path for {fut.thing.name}") 64 | 65 | futs = [] 66 | start_all = monotonic() 67 | for thing in char.thing.values(): 68 | fut = char.engine.submit( 69 | char.engine.function.find_path_somewhere, thing 70 | ) 71 | fut.thing = thing 72 | fut.add_done_callback(log_as_completed) 73 | futs.append(fut) 74 | for fut in futs: 75 | try: 76 | result = fut.result() 77 | thing = fut.thing 78 | start = monotonic() 79 | thing.follow_path(result, check=False) 80 | char.engine.debug( 81 | f"followed path for thing {thing.name} in {monotonic() - start:.2} seconds" 82 | ) 83 | except NetworkXNoPath: 84 | char.engine.debug(f"got no path for thing {fut.thing.name}") 85 | continue 86 | char.engine.debug( 87 | f"followed all paths in {monotonic() - start_all:.2} seconds" 88 | ) 89 | 90 | @go_places.trigger 91 | def turn_one_only(char): 92 | return char.engine.turn == 1 93 | 94 | 95 | if __name__ == "__main__": 96 | from tempfile import mkdtemp 97 | 98 | from lisien import Engine 99 | 100 | td = mkdtemp() 101 | with Engine(td) as eng: 102 | install(eng) 103 | for _ in range(10): 104 | eng.next_turn() 105 | print("View the sim with:") 106 | print("python -m elide " + td) 107 | -------------------------------------------------------------------------------- /elide/elide/tests/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import partial 4 | from typing import Callable 5 | 6 | from blinker import Signal 7 | from kivy.app import App 8 | from kivy.base import EventLoop 9 | 10 | 11 | def all_spots_placed(board, char=None): 12 | if char is None: 13 | char = board.character 14 | for place in char.place: 15 | if place not in board.spot: 16 | return False 17 | return True 18 | 19 | 20 | def all_pawns_placed(board, char=None): 21 | if char is None: 22 | char = board.character 23 | for thing in char.thing: 24 | if thing not in board.pawn: 25 | return False 26 | return True 27 | 28 | 29 | def all_arrows_placed(board, char=None): 30 | if char is None: 31 | char = board.character 32 | for orig, dests in char.portal.items(): 33 | if orig not in board.arrow: 34 | return False 35 | arrows = board.arrow[orig] 36 | for dest in dests: 37 | if dest not in arrows: 38 | return False 39 | return True 40 | 41 | 42 | def board_is_arranged(board, char=None): 43 | if char is None: 44 | char = board.character 45 | return ( 46 | all_spots_placed(board, char) 47 | and all_pawns_placed(board, char) 48 | and all_arrows_placed(board, char) 49 | ) 50 | 51 | 52 | _idle_until_sig = Callable[ 53 | [ 54 | Callable[[], bool] | None, 55 | int | None, 56 | str | None, 57 | ], 58 | Callable[[], bool] | partial["_idle_until_sig"], 59 | ] 60 | 61 | 62 | def idle_until( 63 | condition: Callable[[], bool] | None = None, 64 | timeout: int | None = 100, 65 | message: str | None = None, 66 | ) -> partial[_idle_until_sig] | Callable[[], bool]: 67 | """Advance frames until ``condition()`` is true 68 | 69 | With integer ``timeout``, give up after that many frames, 70 | raising ``TimeoutError``. You can customize its ``message``. 71 | 72 | """ 73 | if not (timeout or condition): 74 | raise ValueError("Need timeout or condition") 75 | if condition is None: 76 | return partial(idle_until, timeout=timeout, message=message) 77 | if timeout is None: 78 | while not condition(): 79 | EventLoop.idle() 80 | return condition 81 | for _ in range(timeout): 82 | if condition(): 83 | return condition 84 | EventLoop.idle() 85 | if message is None: 86 | if hasattr(condition, "__name__"): 87 | message = f"{condition.__name__} timed out" 88 | else: 89 | message = "Timed out" 90 | raise TimeoutError(message) 91 | 92 | 93 | idle100 = partial(idle_until, timeout=100) 94 | 95 | 96 | class ListenableDict(dict, Signal): 97 | def __init__(self): 98 | Signal.__init__(self) 99 | 100 | 101 | def advance_frames(frames: int) -> None: 102 | from kivy.base import EventLoop 103 | 104 | for _ in range(frames): 105 | EventLoop.idle() 106 | 107 | 108 | def transition_over(): 109 | # Even though there's "no transition," the screen manager still considers 110 | # a transition to be active for a few frames, and will refuse input for 111 | # the duration 112 | return not App.get_running_app().manager.transition.is_active 113 | -------------------------------------------------------------------------------- /elide/elide/tests/test_character_switcher.py: -------------------------------------------------------------------------------- 1 | from kivy.tests.common import UnitTestTouch 2 | 3 | from .util import advance_frames, idle_until, transition_over 4 | 5 | 6 | def test_character_switcher(polygons_sim, elide_app): 7 | app = elide_app 8 | idle_until( 9 | lambda: app.manager.current == "mainscreen", 10 | 100, 11 | "Never switched to mainscreen", 12 | ) 13 | idle_until(lambda: app.mainscreen.boardview, 100, "never got boardview") 14 | idle_until( 15 | lambda: app.mainscreen.boardview.board.spot, 100, "never got spots" 16 | ) 17 | physspots = len(app.mainscreen.boardview.board.spot) 18 | app.mainscreen.charmenu.charmenu.toggle_chars_screen() 19 | idle_until( 20 | lambda: app.manager.current == "chars", 21 | 100, 22 | "Never switched to chars", 23 | ) 24 | boxl = app.chars.ids.charsview.ids.boxl 25 | idle_until( 26 | lambda: len(boxl.children) == 3, 27 | 100, 28 | "Didn't get all three characters", 29 | ) 30 | idle_until(transition_over) 31 | for charb in boxl.children: 32 | if charb.text == "triangle": 33 | touch = UnitTestTouch( 34 | *charb.parent.parent.to_parent(*charb.center) 35 | ) 36 | touch.touch_down() 37 | advance_frames(5) 38 | idle_until( 39 | lambda: charb.state == "down", 40 | 100, 41 | "Button press did not work", 42 | ) 43 | touch.touch_up() 44 | break 45 | else: 46 | assert False, 'No button for "triangle" character' 47 | idle_until( 48 | lambda: app.chars.ids.charsview.character_name == "triangle", 49 | 100, 50 | "Never propagated character_name", 51 | ) 52 | app.chars.toggle() 53 | idle_until( 54 | lambda: app.manager.current == "mainscreen", 55 | 100, 56 | "Didn't switch back to mainscreen", 57 | ) 58 | idle_until( 59 | lambda: not app.mainscreen.boardview.board.spot, 60 | 100, 61 | "Didn't clear out spots, {} left".format( 62 | len(app.mainscreen.boardview.board.spot) 63 | ), 64 | ) 65 | app.mainscreen.charmenu.charmenu.toggle_chars_screen() 66 | idle_until( 67 | lambda: app.manager.current == "chars", 68 | 100, 69 | "Never switched to chars", 70 | ) 71 | idle_until(transition_over) 72 | for charb in boxl.children: 73 | if charb.text == "physical": 74 | pos = charb.parent.parent.to_parent( 75 | *charb.parent.to_parent(*charb.to_parent(*charb.center)) 76 | ) 77 | touch = UnitTestTouch(*pos) 78 | touch.touch_down() 79 | idle_until( 80 | lambda: charb.state == "down", 81 | 100, 82 | "Button press did not work", 83 | ) 84 | advance_frames(5) 85 | touch.touch_up() 86 | break 87 | else: 88 | assert False, 'No button for "physical" character' 89 | idle_until( 90 | lambda: app.chars.ids.charsview.character_name == "physical", 91 | 100, 92 | "Never chose character_name", 93 | ) 94 | idle_until( 95 | lambda: app.character_name == "physical", 96 | 100, 97 | "Never propagated 'physical' back to app.character_name", 98 | ) 99 | app.chars.toggle() 100 | idle_until(transition_over) 101 | idle_until( 102 | lambda: len(app.mainscreen.boardview.board.spot) == physspots, 103 | 100, 104 | "Never got physical back", 105 | ) 106 | -------------------------------------------------------------------------------- /elide/elide/dummy.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from functools import partial 16 | 17 | from kivy.logger import Logger 18 | from kivy.properties import ( 19 | NumericProperty, 20 | ObjectProperty, 21 | ReferenceListProperty, 22 | StringProperty, 23 | ) 24 | 25 | from .kivygarden.texturestack import ImageStack 26 | from .util import logwrap 27 | 28 | wraplog_Dummy = partial(logwrap, section="Dummy") 29 | 30 | 31 | class Dummy(ImageStack): 32 | """A widget that looks like the ones on the graph, which, when dragged 33 | onto the graph, creates one of them. 34 | 35 | """ 36 | 37 | _touch = ObjectProperty(None, allownone=True) 38 | name = StringProperty() 39 | prefix = StringProperty() 40 | num = NumericProperty() 41 | x_start = NumericProperty(0) 42 | y_start = NumericProperty(0) 43 | pos_start = ReferenceListProperty(x_start, y_start) 44 | x_down = NumericProperty(0) 45 | y_down = NumericProperty(0) 46 | pos_down = ReferenceListProperty(x_down, y_down) 47 | x_up = NumericProperty(0) 48 | y_up = NumericProperty(0) 49 | pos_up = ReferenceListProperty(x_up, y_up) 50 | x_center_up = NumericProperty(0) 51 | y_center_up = NumericProperty(0) 52 | center_up = ReferenceListProperty(x_center_up, y_center_up) 53 | right_up = NumericProperty(0) 54 | top_up = NumericProperty(0) 55 | 56 | @logwrap(section="Dummy") 57 | def on_paths(self, *args, **kwargs): 58 | super().on_paths(*args, **kwargs) 59 | Logger.debug("Dummy: {} got paths {}".format(self.name, self.paths)) 60 | 61 | @logwrap(section="Dummy") 62 | def on_touch_down(self, touch): 63 | """If hit, record my starting position, that I may return to it in 64 | ``on_touch_up`` after creating a real :class:`graph.Spot` or 65 | :class:`graph.Pawn` instance. 66 | 67 | """ 68 | if not self.collide_point(*touch.pos): 69 | return False 70 | self.pos_start = self.pos 71 | self.pos_down = (self.x - touch.x, self.y - touch.y) 72 | touch.grab(self) 73 | self._touch = touch 74 | return True 75 | 76 | @logwrap(section="Dummy") 77 | def on_touch_move(self, touch): 78 | """Follow the touch""" 79 | if touch is not self._touch: 80 | return False 81 | self.pos = (touch.x + self.x_down, touch.y + self.y_down) 82 | return True 83 | 84 | @logwrap(section="Dummy") 85 | def on_touch_up(self, touch): 86 | """Return to ``pos_start``, but first, save my current ``pos`` into 87 | ``pos_up``, so that the layout knows where to put the real 88 | :class:`graph.Spot` or :class:`graph.Pawn` instance. 89 | 90 | """ 91 | if touch is not self._touch: 92 | return False 93 | self.pos_up = self.pos 94 | self.pos = self.pos_start 95 | self._touch = None 96 | return True 97 | -------------------------------------------------------------------------------- /elide/elide/tests/test_gridboard.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import pytest 3 | from kivy.core.window import Window 4 | from kivy.lang import Builder 5 | 6 | from elide.grid.board import GridBoard, GridBoardView 7 | from lisien.facade import EngineFacade 8 | 9 | from .util import all_pawns_placed, all_spots_placed, idle_until 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def grid_kv(): 14 | Builder.load_file("grid/board.kv") 15 | yield 16 | Builder.unload_file("grid/board.kv") 17 | 18 | 19 | @pytest.mark.usefixtures("kivy") 20 | def test_layout_grid(): 21 | spots_wide = 3 22 | spots_tall = 3 23 | spot_width = 32 24 | spot_height = 32 25 | graph = nx.grid_2d_graph(spots_wide, spots_tall) 26 | eng = EngineFacade(None) 27 | char = eng.new_character("grid", graph) 28 | char.place[1, 1].add_thing("something") 29 | otherthing = char.place[2, 2].new_thing("otherthing") 30 | assert len(char.thing) == 2 31 | board = GridBoard(character=char) 32 | view = GridBoardView(board=board) 33 | Window.add_widget(view) 34 | idle_until( 35 | lambda: all_spots_placed(board, char) 36 | and all_pawns_placed(board, char), 37 | 100, 38 | "Spots and pawns not placed", 39 | ) 40 | otherthing["location"] = (0, 0) 41 | board.spot_plane.data = list(map(board.make_spot, char.place.values())) 42 | board.spot_plane.redraw() 43 | board.pawn_plane.data = list(map(board.make_pawn, char.thing.values())) 44 | 45 | def arranged(): 46 | for x in range(spots_wide): 47 | for y in range(spots_tall): 48 | spot = board.spot[x, y] 49 | if spot.x != x * spot_width or spot.y != y * spot_height: 50 | return False 51 | return True 52 | 53 | idle_until(arranged, 100) 54 | idle_until(lambda: board.pawn_plane._stack_index) 55 | this = board.pawn["something"] 56 | that = board.pawn["otherthing"] 57 | print(this.pos, board.spot[1, 1].pos) 58 | idle_until(lambda: this.pos == board.spot[1, 1].pos) 59 | idle_until(lambda: that.pos == board.spot[0, 0].pos) 60 | assert this.x == board.spot[1, 1].x 61 | assert this.y == board.spot[1, 1].y 62 | assert that.x == board.spot[0, 0].x 63 | assert that.y == board.spot[0, 0].y 64 | 65 | 66 | @pytest.mark.usefixtures("line_shaped_graphs") 67 | def test_character_switch_grid(elide_app): 68 | app = elide_app 69 | idle_until( 70 | lambda: hasattr(app, "mainscreen") 71 | and app.mainscreen.mainview 72 | and app.mainscreen.statpanel 73 | and hasattr(app.mainscreen, "gridview") 74 | ) 75 | app.mainscreen.charmenu.toggle_gridview() 76 | idle_until( 77 | lambda: app.mainscreen.gridview in app.mainscreen.mainview.children 78 | ) 79 | idle_until(lambda: app.mainscreen.gridview.board.children) 80 | assert len(app.mainscreen.gridview.board.spot) == 10 81 | assert all( 82 | spot.y == 0 for spot in app.mainscreen.gridview.board.spot.values() 83 | ) 84 | idle_until( 85 | lambda: not all( 86 | spot.x == 0 for spot in app.mainscreen.gridview.board.spot.values() 87 | ), 88 | 100, 89 | ) 90 | app.character_name = "tall" 91 | idle_until( 92 | lambda: all( 93 | spot.x == 0 for spot in app.mainscreen.gridview.board.spot.values() 94 | ), 95 | 1000, 96 | "Never got the new board", 97 | ) 98 | idle_until( 99 | lambda: not all( 100 | spot.y == 0 for spot in app.mainscreen.gridview.board.spot.values() 101 | ), 102 | 1000, 103 | "New board arranged weird", 104 | ) 105 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_thing.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import networkx as nx 16 | import pytest 17 | 18 | 19 | @pytest.fixture(scope="function") 20 | def something(engine): 21 | yield ( 22 | engine.new_character("physical") 23 | .new_place("somewhere") 24 | .new_thing("something") 25 | ) 26 | 27 | 28 | def test_contents(something): 29 | pl1 = something.character.place["somewhere"] 30 | pl2 = something.character.new_place("somewhere2") 31 | assert something.location == something.character.node["somewhere"] 32 | assert something.name in pl1.content 33 | assert something.name not in pl2.content 34 | assert [something] == list(pl1.contents()) 35 | assert [] == list(pl2.contents()) 36 | 37 | 38 | def test_future_contents(something): 39 | engine = something.engine 40 | somewhere = something.location 41 | with engine.plan(): 42 | engine.turn = 1 43 | something.location = None 44 | engine.turn = 2 45 | somebody = somewhere.new_thing("somebody") 46 | engine.turn = 0 47 | someone = somewhere.new_thing("someone") 48 | sometick = engine.tick 49 | assert engine.turn == 0 50 | assert len(somewhere.contents()) == 1 51 | assert something in somewhere.contents() 52 | assert someone not in somewhere.contents() 53 | engine.tick = sometick 54 | assert len(somewhere.contents()) == 2 55 | assert something in somewhere.contents() 56 | assert somebody not in somewhere.contents() 57 | assert someone in somewhere.contents() 58 | engine.turn = 1 59 | engine.tick = engine.turn_end_plan() 60 | assert len(somewhere.contents()) == 1 61 | assert something not in somewhere.contents() 62 | assert someone in somewhere.contents() 63 | assert somebody not in somewhere.contents() 64 | engine.turn = 2 65 | engine.tick = engine.turn_end_plan() 66 | assert len(somewhere.contents()) == 2 67 | assert somebody in somewhere.contents() 68 | assert someone in somewhere.contents() 69 | assert something not in somewhere.contents() 70 | 71 | 72 | def test_travel(engine): 73 | phys = engine.new_character("physical", data=nx.grid_2d_graph(8, 8)) 74 | del phys.place[1, 1] 75 | del phys.place[6, 1] 76 | thing1 = phys.place[0, 0].new_thing(1) 77 | thing2 = phys.place[7, 0].new_thing(2) 78 | thing1.travel_to(phys.place[7, 7]) 79 | thing2.travel_to(phys.place[0, 7]) 80 | for _ in range(15): 81 | engine.next_turn() 82 | assert thing1.location == phys.place[7, 7] 83 | assert thing2.location == phys.place[0, 7] 84 | thing1.go_to_place(phys.place[6, 7]) 85 | thing2.go_to_place(phys.place[1, 7]) 86 | engine.next_turn() 87 | engine.next_turn() 88 | assert thing1.location == phys.place[6, 7] 89 | assert thing2.location == phys.place[1, 7] 90 | 91 | 92 | def test_neighbors_no_successors(sqleng): 93 | phys = sqleng.new_character("physical") 94 | here = phys.new_place("here") 95 | phys.new_place("there") 96 | assert list(here.neighbors()) == [] 97 | -------------------------------------------------------------------------------- /elide/elide/elide.kv: -------------------------------------------------------------------------------- 1 | 2 | #: import os os 3 | : 4 | size_hint_y: None 5 | font_size: 50 6 | height: self.texture_size[1] + 10 7 | : 8 | orientation: 'vertical' 9 | init_board: app.init_board 10 | starter: app.start_subprocess 11 | Label: 12 | text: 'Generate an initial map?' 13 | Button: 14 | id: drop 15 | text: 'None' 16 | on_release: root.generator_dropdown.open(drop) 17 | Widget: 18 | id: controls 19 | : 20 | size_hint_x: 0.6 21 | BoxLayout: 22 | orientation: 'vertical' 23 | Label: 24 | text: 'Generate an initial map?' 25 | font_size: 50 26 | size_hint_y: None 27 | WorldStartConfigurator: 28 | id: worldstart 29 | dismiss: root.dismiss 30 | Label: 31 | text: 'Please name your game' 32 | font_size: 50 33 | size_hint_y: None 34 | BoxLayout: 35 | orientation: 'horizontal' 36 | size_hint_y: 0.2 37 | TextInput: 38 | id: game_name 39 | multiline: False 40 | size_hint_x: 0.8 41 | Button: 42 | text: 'Start' 43 | id: start_new_game_button 44 | on_release: root.validate_and_start() 45 | 46 | : 47 | name: 'main' 48 | BoxLayout: 49 | orientation: 'horizontal' 50 | Widget: 51 | size_hint_x: 0.2 52 | BoxLayout: 53 | orientation: 'vertical' 54 | Label: 55 | text: 'Elide' 56 | font_size: 80 57 | size_hint_y: None 58 | height: self.texture_size[1] 59 | Button: 60 | text: 'New game' 61 | id: new_game_button 62 | font_size: 50 63 | on_release: root.new_game() 64 | Button: 65 | text: 'Load game' 66 | id: load_game_button 67 | font_size: 50 68 | on_release: root.load_game() 69 | Button: 70 | text: 'Import game' 71 | id: import_game_button 72 | font_size: 50 73 | on_release: root.import_game() 74 | Button: 75 | text: 'Export game' 76 | id: export_game_button 77 | font_size: 50 78 | on_release: root.export_game() 79 | Widget: 80 | size_hint_x: 0.2 81 | : 82 | size_hint_x: 0.6 83 | BoxLayout: 84 | orientation: 'vertical' 85 | Label: 86 | text: root.headline 87 | size_hint_y: 0.1 88 | font_size: self.height 89 | GameList: 90 | id: game_list 91 | path: root.path 92 | picker: root 93 | size_hint_y: 0.8 94 | Button: 95 | text: 'Cancel' 96 | on_release: root.dismiss() 97 | size_hint_y: 0.1 98 | font_size: self.height 99 | : 100 | size_hint_x: 0.6 101 | BoxLayout: 102 | orientation: 'vertical' 103 | Label: 104 | text: root.headline 105 | size_hint_y: 0.1 106 | font_size: self.height 107 | RelativeLayout: 108 | id: chooser_goes_here 109 | BoxLayout: 110 | orientation: 'horizontal' 111 | size_hint_y: 0.1 112 | Button: 113 | text: 'Cancel' 114 | on_release: root.dismiss() 115 | Button: 116 | text: 'OK' 117 | id: ok_button 118 | on_release: root.pick(root._file_chooser.selection) 119 | : 120 | size_hint_x: 0.6 121 | BoxLayout: 122 | orientation: 'vertical' 123 | Label: 124 | text: root.headline 125 | size_hint_y: 0.1 126 | font_size: self.height 127 | GameList: 128 | id: game_list 129 | path: root.path 130 | picker: root 131 | size_hint_y: 0.8 132 | BoxLayout: 133 | orientation: 'horizontal' 134 | size_hint_y: 0.1 135 | Button: 136 | text: 'Cancel' 137 | on_release: root.dismiss() 138 | Button: 139 | text: 'OK' 140 | id: ok_button 141 | on_release: root.pick(root._game_list.selection) 142 | -------------------------------------------------------------------------------- /lisien/lisien/server/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import logging 16 | import threading 17 | from queue import Queue 18 | 19 | import cherrypy 20 | 21 | from ..handle import EngineHandle 22 | 23 | 24 | class LiSEHandleWebService(object): 25 | exposed = True 26 | 27 | def __init__(self, *args, **kwargs): 28 | if "logger" in kwargs: 29 | self.logger = kwargs["logger"] 30 | else: 31 | self.logger = kwargs["logger"] = logging.getLogger(__name__) 32 | self.cmdq = kwargs["cmdq"] = Queue() 33 | self.outq = kwargs["outq"] = Queue() 34 | self._handle_thread = threading.Thread( 35 | target=self._run_handle_forever, 36 | args=args, 37 | kwargs=kwargs, 38 | daemon=True, 39 | ) 40 | self._handle_thread.start() 41 | 42 | @staticmethod 43 | def _run_handle_forever(*args, **kwargs): 44 | cmdq = kwargs.pop("cmdq") 45 | outq = kwargs.pop("outq") 46 | logger = kwargs.pop("logger") 47 | setup = kwargs.pop("setup", None) 48 | logq = Queue() 49 | 50 | def log(typ, data): 51 | if typ == "command": 52 | (cmd, args) = data 53 | logger.debug( 54 | "lisien thread {}: calling {}{}".format( 55 | threading.get_ident(), cmd, tuple(args) 56 | ) 57 | ) 58 | else: 59 | logger.debug( 60 | "lisien thread {}: returning {} (of type {})".format( 61 | threading.get_ident(), data, repr(type(data)) 62 | ) 63 | ) 64 | 65 | def get_log_forever(logq): 66 | (level, data) = logq.get() 67 | getattr(logger, level)(data) 68 | 69 | engine_handle = EngineHandle(*args, logq=logq, **kwargs) 70 | if setup: 71 | setup(engine_handle._real) 72 | handle_log_thread = threading.Thread( 73 | target=get_log_forever, args=(logq,), daemon=True 74 | ) 75 | handle_log_thread.start() 76 | while True: 77 | inst = cmdq.get() 78 | if inst == "shutdown": 79 | handle_log_thread.join() 80 | cmdq.close() 81 | outq.close() 82 | return 0 83 | cmd = inst.pop("command") 84 | silent = inst.pop("silent", False) 85 | log("command", (cmd, args)) 86 | response = getattr(engine_handle, cmd)(**inst) 87 | if silent: 88 | continue 89 | log("result", response) 90 | outq.put(engine_handle._real.listify(response)) 91 | 92 | @cherrypy.tools.accept(media="application/json") 93 | @cherrypy.tools.json_out() 94 | def GET(self): 95 | return cherrypy.session["LiSE_response"] 96 | 97 | @cherrypy.tools.json_out() 98 | def POST(self, **kwargs): 99 | silent = kwargs.get("silent", False) 100 | self.cmdq.put(kwargs) 101 | if silent: 102 | return None 103 | response = self.outq.get() 104 | cherrypy.session["LiSE_response"] = response 105 | return response 106 | 107 | def PUT(self, silent=False, **kwargs): 108 | silent = silent 109 | self.cmdq.put(kwargs) 110 | if not silent: 111 | cherrypy.session["LiSE_response"] = self.outq.get() 112 | 113 | def DELETE(self): 114 | cherrypy.session.pop("LiSE_response", None) 115 | -------------------------------------------------------------------------------- /elide/elide/card.kv: -------------------------------------------------------------------------------- 1 | 2 | : 3 | canvas: 4 | Color: 5 | rgba: root.color 6 | Rectangle: 7 | texture: root.texture 8 | pos: root.pos 9 | size: root.size 10 | Color: 11 | rgba: root.outline_color 12 | Line: 13 | points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y] 14 | Color: 15 | rgba: [1, 1, 1, 1] 16 | : 17 | color: [0, 0, 0, 0] 18 | outline_color: [1, 1, 1, 1] 19 | : 20 | headline: headline 21 | midline: midline 22 | footer: footer 23 | art: art 24 | foreground: foreground 25 | canvas: 26 | Color: 27 | rgba: root.background_color 28 | Rectangle: 29 | texture: root.background_texture 30 | pos: root.pos 31 | size: root.size 32 | Color: 33 | rgba: root.outline_color 34 | Line: 35 | points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y] 36 | Color: 37 | rgba: [1, 1, 1, 1] 38 | BoxLayout: 39 | size_hint: 0.9, 0.9 40 | pos_hint: {'x': 0.05, 'y': 0.05} 41 | orientation: 'vertical' 42 | canvas: 43 | Color: 44 | rgba: root.content_outline_color 45 | Line: 46 | points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y] 47 | Color: 48 | rgba: [1, 1, 1, 1] 49 | Label: 50 | id: headline 51 | text: root.headline_text 52 | markup: root.headline_markup 53 | font_name: root.headline_font_name 54 | font_size: root.headline_font_size 55 | color: root.headline_color 56 | size_hint: (None, None) 57 | size: self.texture_size 58 | ColorTextureBox: 59 | id: art 60 | color: root.art_color 61 | texture: root.art_texture 62 | outline_color: root.art_outline_color if root.show_art else [0, 0, 0, 0] 63 | size_hint: (1, 1) if root.show_art else (None, None) 64 | size: (0, 0) 65 | Label: 66 | id: midline 67 | text: root.midline_text 68 | markup: root.midline_markup 69 | font_name: root.midline_font_name 70 | font_size: root.midline_font_size 71 | color: root.midline_color 72 | size_hint: (None, None) 73 | size: self.texture_size 74 | ColorTextureBox: 75 | id: foreground 76 | color: root.foreground_color 77 | outline_color: root.foreground_outline_color 78 | texture: root.foreground_texture 79 | Label: 80 | id: main_text 81 | color: root.text_color 82 | markup: root.markup 83 | font_name: root.font_name 84 | font_size: root.font_size 85 | text_size: foreground.size 86 | size_hint: (None, None) 87 | size: self.texture_size 88 | pos: foreground.pos 89 | valign: 'top' 90 | Button: 91 | id: editbut 92 | background_normal: 'atlas://data/images/defaulttheme/button' if root.editable else '' 93 | background_down: 'atlas://data/images/defaulttheme/button_pressed' if root.editable else '' 94 | color: (1., 1., 1., 1.) if root.editable else (0., 0., 0., 0.) 95 | background_color: (1., 1., 1., 1.) if root.editable else (0., 0., 0., 0.) 96 | text_size: self.size 97 | size: self.texture_size 98 | size_hint: (None, None) 99 | font_name: 'DejaVuSans' 100 | font_size: 30 101 | x: foreground.right - self.width - (.1 * self.width) 102 | y: foreground.top - self.height - (.1 * self.height) 103 | text: '✐' if root.editable else '' 104 | on_press: root.edit_func(root) 105 | disabled: not root.editable 106 | Label: 107 | id: footer 108 | text: root.footer_text 109 | markup: root.footer_markup 110 | font_name: root.footer_font_name 111 | font_size: root.footer_font_size 112 | color: root.footer_color 113 | size_hint: (None, None) 114 | size: self.texture_size 115 | : 116 | ScrollBarBar: 117 | id: bar 118 | color: root.bar_color if root.scrolling else root.bar_inactive_color 119 | texture: root.bar_texture -------------------------------------------------------------------------------- /lisien/lisien/tests/test_place.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import networkx as nx 16 | import pytest 17 | 18 | from lisien.exc import AmbiguousLeaderError 19 | 20 | 21 | @pytest.fixture(scope="function") 22 | def someplace(engy): 23 | yield engy.new_character("physical").new_place("someplace") 24 | 25 | 26 | def test_contents(someplace): 27 | stuff = [someplace.new_thing(i) for i in range(10)] 28 | assert len(someplace.content) == len(stuff) 29 | assert len(someplace.contents()) == len(stuff) 30 | for i in range(10): 31 | assert i in someplace.content 32 | assert someplace.content[i] == stuff[i] 33 | for that in stuff: 34 | assert that in someplace.contents() 35 | for that in someplace.contents(): 36 | assert that in stuff 37 | elsewhere = someplace.character.new_place("elsewhere") 38 | fust = elsewhere.new_thing(11) 39 | assert fust not in someplace.contents() 40 | assert 11 not in someplace.content 41 | with pytest.raises(KeyError): 42 | someplace.content[11] 43 | 44 | 45 | def test_portal(someplace): 46 | assert not someplace.portal 47 | assert "there" not in someplace.portal 48 | there = someplace.character.new_place("there") 49 | assert not there.preportal 50 | assert "someplace" not in there.preportal 51 | someplace.character.new_portal("someplace", "there") 52 | assert someplace.portal 53 | assert "there" in someplace.portal 54 | assert "there" not in someplace.preportal 55 | assert there.preportal 56 | assert "someplace" in there.preportal 57 | assert "someplace" not in there.portal 58 | someplace.character.remove_edge("someplace", "there") 59 | assert "there" not in someplace.portal 60 | assert "someplace" not in there.preportal 61 | 62 | 63 | def test_user(someplace): 64 | with pytest.raises(AmbiguousLeaderError): 65 | someplace.leader.only 66 | someone = someplace.engine.new_character("someone") 67 | someone.add_unit(someplace) 68 | assert someplace.leader.only is someone 69 | assert "someone" in someplace.leader 70 | assert someplace.leader["someone"] is someone 71 | noone = someplace.engine.new_character("noone") 72 | assert "noone" not in someplace.leader 73 | noone.add_unit(someplace) 74 | with pytest.raises(AmbiguousLeaderError): 75 | someplace.leader.only 76 | assert "noone" in someplace.leader 77 | assert someplace.leader["noone"] is noone 78 | 79 | 80 | def test_rulebook(someplace): 81 | assert someplace.rulebook.name == ("physical", "someplace") 82 | someplace.rulebook = "imadeitup" 83 | assert someplace.rulebook.name == "imadeitup" 84 | 85 | 86 | def test_deletion_after_keyframe(engine): 87 | phys = engine.new_character("physical", data=nx.grid_2d_graph(8, 8)) 88 | del phys.place[5, 5] 89 | assert (5, 5) not in phys.place 90 | assert (5, 5) not in list(phys.place) 91 | while engine.turn < 20: 92 | engine.next_turn() 93 | assert (5, 5) not in phys.place 94 | assert (5, 5) not in list(phys.place) 95 | 96 | 97 | def test_clear(engy): 98 | phys = engy.new_character("physical") 99 | place = phys.new_place("here") 100 | place["a"] = 1 101 | place["b"] = 2 102 | assert place["a"] == 1 103 | assert place["b"] == 2 104 | place.clear() 105 | assert "a" not in place 106 | with pytest.raises(KeyError): 107 | print(place["b"]) 108 | -------------------------------------------------------------------------------- /lisien/lisien/tests/test_contents.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | import pytest 16 | 17 | 18 | @pytest.fixture(scope="function") 19 | def char(engy): 20 | yield engy.new_character("chara") 21 | 22 | 23 | @pytest.fixture 24 | def chara_chron(engine): 25 | yield engine.new_character("chara") 26 | 27 | 28 | def test_many_things_in_place(char): 29 | place = char.new_place(0) 30 | things = [place.new_thing(i) for i in range(1, 10)] 31 | for thing in things: 32 | assert thing in place.contents() 33 | for that in place.content: 34 | assert place.content[that].location == place 35 | things.sort(key=lambda th: th.name) 36 | contents = sorted(place.contents(), key=lambda th: th.name) 37 | assert things == contents 38 | 39 | 40 | def test_contents_over_time(chara_chron): 41 | chara = chara_chron 42 | place = chara.new_place(0) 43 | correct_contents = set() 44 | for i in range(10): 45 | chara.engine.next_turn() 46 | place.new_thing(chara.engine.turn) 47 | del chara.thing[chara.engine.turn] 48 | assert set(place.content.keys()) == correct_contents 49 | place.new_thing(chara.engine.turn) 50 | correct_contents.add(chara.engine.turn) 51 | assert set(place.content.keys()) == correct_contents 52 | del chara.thing[9] 53 | correct_contents.remove(9) 54 | assert set(place.content.keys()) == correct_contents 55 | del chara.thing[8] 56 | correct_contents.remove(8) 57 | assert set(place.content.keys()) == correct_contents 58 | chara.engine.turn = 5 59 | assert 10 not in chara.thing, list(chara.thing) 60 | assert 5 in chara.thing, list(chara.thing) 61 | chara.engine.branch = "bb" 62 | del chara.thing[5] 63 | assert set(place.content.keys()) == {1, 2, 3, 4} 64 | 65 | 66 | def test_contents_in_plan(chara_chron): 67 | chara = chara_chron 68 | place = chara.new_place(0) 69 | correct_contents = {1, 2, 3, 4, 5} 70 | for th in correct_contents: 71 | place.new_thing(th) 72 | engine = chara.engine 73 | with engine.plan(): 74 | for i in range(6, 15): 75 | engine.turn += 1 76 | assert set(place.content) == correct_contents 77 | place.new_thing(i) 78 | del chara.thing[i] 79 | assert set(place.content) == correct_contents 80 | place.new_thing(i) 81 | correct_contents.add(i) 82 | assert set(place.content) == correct_contents 83 | engine.turn = 4 84 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7, 8, 9} 85 | assert engine.turn == 0 86 | assert set(place.content) == {1, 2, 3, 4, 5} 87 | engine.next_turn() 88 | engine.next_turn() 89 | engine.tick = engine.turn_end_plan() 90 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7} 91 | # this does not contradict the plan 92 | place.new_thing(15) 93 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7, 15} 94 | engine.next_turn() 95 | engine.tick = engine.turn_end_plan() 96 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7, 8, 15} 97 | engine.next_turn() 98 | engine.next_turn() 99 | assert engine.turn == 5 100 | engine.tick = engine.turn_end_plan() 101 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15} 102 | # this neither 103 | there = chara.new_place("there") 104 | # but this does 105 | chara.thing[9].location = there 106 | assert set(place.content) == {1, 2, 3, 4, 5, 6, 7, 8, 10, 15} 107 | engine.turn = 10 108 | assert set(place.content) == (correct_contents - {9}) | {15} 109 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/body.atlas: -------------------------------------------------------------------------------- 1 | {"body-0.png": {"scalemail2": [2, 240, 32, 32], "dragonarm_cyan": [274, 444, 32, 32], "banded2": [138, 478, 32, 32], "robe_blue_white": [206, 308, 32, 32], "leather_heavy": [342, 376, 32, 32], "leather_red": [444, 376, 32, 32], "mesh_black": [104, 342, 32, 32], "troll_hide": [70, 206, 32, 32], "metal_blue": [172, 342, 32, 32], "robe_white": [274, 274, 32, 32], "belt1": [206, 478, 32, 32], "belt2": [240, 478, 32, 32], "robe_of_night": [2, 274, 32, 32], "gimli": [342, 410, 32, 32], "robe_red": [172, 274, 32, 32], "plate": [478, 342, 32, 32], "dragonsc_brown": [478, 444, 32, 32], "robe_gray2": [376, 308, 32, 32], "crystal_plate": [172, 444, 32, 32], "dragonsc_magenta": [104, 410, 32, 32], "bplate_metal1": [410, 478, 32, 32], "half_plate3": [478, 410, 32, 32], "half_plate2": [444, 410, 32, 32], "robe_yellow": [376, 274, 32, 32], "skirt_onep_grey": [478, 240, 32, 32], "china_red": [104, 444, 32, 32], "lears_chain_mail": [206, 376, 32, 32], "slit_black": [2, 206, 32, 32], "shirt_check": [206, 240, 32, 32], "arwen": [104, 478, 32, 32], "dragonarm_green": [342, 444, 32, 32], "shirt_black3": [70, 240, 32, 32], "robe_white_green": [308, 274, 32, 32], "saruman": [478, 274, 32, 32], "robe_white_red": [342, 274, 32, 32], "legolas": [36, 342, 32, 32], "robe_black_red": [104, 308, 32, 32], "dress_white": [206, 410, 32, 32], "green_susp": [410, 410, 32, 32], "merry": [70, 342, 32, 32], "robe_purple": [36, 274, 32, 32], "shoulder_pad": [444, 240, 32, 32], "leather_green": [308, 376, 32, 32], "robe_black_gold": [36, 308, 32, 32], "shirt_white2": [342, 240, 32, 32], "robe_brown": [308, 308, 32, 32], "scalemail": [36, 240, 32, 32], "shirt_white1": [308, 240, 32, 32], "gandalf_g": [308, 410, 32, 32], "armor_mummy": [70, 478, 32, 32], "plate_black": [444, 342, 32, 32], "dragonsc_cyan": [2, 410, 32, 32], "robe_black": [2, 308, 32, 32], "animal_skin": [2, 478, 32, 32], "dragonarm_white": [410, 444, 32, 32], "bloody": [308, 478, 32, 32], "sailor": [410, 274, 32, 32], "robe_black_hood": [70, 308, 32, 32], "aragorn": [36, 478, 32, 32], "green_chain": [376, 410, 32, 32], "robe_red3": [138, 274, 32, 32], "robe_red2": [104, 274, 32, 32], "leather2": [240, 376, 32, 32], "pj": [342, 342, 32, 32], "plate_and_cloth2": [376, 342, 32, 32], "robe_magenta": [478, 308, 32, 32], "shirt_black": [138, 240, 32, 32], "shirt_vest": [274, 240, 32, 32], "robe_cyan": [342, 308, 32, 32], "monk_black": [206, 342, 32, 32], "leather_jacket": [376, 376, 32, 32], "jacket3": [70, 376, 32, 32], "jacket2": [36, 376, 32, 32], "monk_blue": [240, 342, 32, 32], "susp_black": [36, 206, 32, 32], "mesh_red": [138, 342, 32, 32], "leather_stud": [2, 342, 32, 32], "dress_green": [172, 410, 32, 32], "chainmail2": [478, 478, 32, 32], "chainmail3": [2, 444, 32, 32], "karate": [172, 376, 32, 32], "dragonsc_blue": [444, 444, 32, 32], "shirt_hawaii": [240, 240, 32, 32], "jacket_stud": [104, 376, 32, 32], "boromir": [342, 478, 32, 32], "plate_and_cloth": [410, 342, 32, 32], "robe_rainbow": [70, 274, 32, 32], "pipin": [308, 342, 32, 32], "sam": [444, 274, 32, 32], "robe_white2": [240, 274, 32, 32], "robe_brown2": [240, 308, 32, 32], "robe_brown3": [274, 308, 32, 32], "leather_short": [478, 376, 32, 32], "dragonsc_white": [138, 410, 32, 32], "half_plate": [2, 376, 32, 32], "shirt_black_and_cloth": [104, 240, 32, 32], "robe_green_gold": [444, 308, 32, 32], "dragonarm_magenta": [376, 444, 32, 32], "robe_blue_green": [172, 308, 32, 32], "shirt_white3": [376, 240, 32, 32], "china_red2": [70, 444, 32, 32], "shirt_white_yellow": [410, 240, 32, 32], "dragonsc_green": [70, 410, 32, 32], "chunli": [138, 444, 32, 32], "leather_armour": [274, 376, 32, 32], "edison": [240, 410, 32, 32], "robe_blue": [138, 308, 32, 32], "frodo": [274, 410, 32, 32], "bikini_red": [274, 478, 32, 32], "breast_black": [444, 478, 32, 32], "dragonsc_gold": [36, 410, 32, 32], "karate2": [138, 376, 32, 32], "bplate_green": [376, 478, 32, 32], "chainmail": [36, 444, 32, 32], "shirt_blue": [172, 240, 32, 32], "robe_red_gold": [206, 274, 32, 32], "vest_red": [104, 206, 32, 32], "neck": [274, 342, 32, 32], "dragonarm_gold": [308, 444, 32, 32], "leather_metal": [410, 376, 32, 32], "banded": [172, 478, 32, 32], "dragonarm_blue": [206, 444, 32, 32], "dragonarm_brown": [240, 444, 32, 32], "robe_green": [410, 308, 32, 32]}} -------------------------------------------------------------------------------- /lisien/lisien/tests/test_rule_poller.py: -------------------------------------------------------------------------------- 1 | # This file is part of Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | def test_character_rule_poll(engine): 16 | phys = engine.new_character("physical") 17 | notphys = engine.new_character("ethereal") 18 | 19 | @phys.rule(always=True) 20 | def hello(char): 21 | char.stat["run"] = True 22 | 23 | @notphys.rule 24 | def goodbye(char): 25 | char.stat["run"] = True 26 | 27 | engine.next_turn() 28 | 29 | assert "run" in phys.stat 30 | assert "run" not in notphys.stat 31 | 32 | 33 | def test_unit_rule_poll(engine): 34 | phys = engine.new_character("physical") 35 | notphys = engine.new_character("ethereal") 36 | 37 | unit = phys.new_place("unit") 38 | notunit1 = notphys.new_place("notunit") 39 | notunit2 = phys.new_place("notunit") 40 | notphys.add_unit(unit) 41 | 42 | @notphys.unit.rule(always=True) 43 | def rule1(unit): 44 | unit["run"] = True 45 | 46 | @phys.unit.rule 47 | def rule2(unit): 48 | unit["run"] = True 49 | 50 | engine.next_turn() 51 | 52 | assert unit["run"] 53 | assert "run" not in notunit1 54 | assert "run" not in notunit2 55 | 56 | 57 | def test_character_thing_rule_poll(engine): 58 | phys = engine.new_character("physical") 59 | notphys = engine.new_character("ethereal") 60 | 61 | there = phys.new_place("there") 62 | this = there.new_thing("this") 63 | that = there.new_thing("that") 64 | 65 | yonder = notphys.new_place("yonder") 66 | thother = yonder.new_thing("thother") 67 | 68 | @phys.thing.rule(always=True) 69 | def rule1(thing): 70 | thing["run"] = True 71 | 72 | @phys.thing.rule 73 | def rule2(thing): 74 | thing["notrun"] = False 75 | 76 | engine.next_turn() 77 | 78 | assert this["run"] 79 | assert that["run"] 80 | assert "notrun" not in that 81 | assert "run" not in thother 82 | assert "run" not in there 83 | assert "run" not in yonder 84 | 85 | 86 | def test_character_place_rule_poll(engine): 87 | phys = engine.new_character("physical") 88 | notphys = engine.new_character("ethereal") 89 | 90 | here = phys.new_place("here") 91 | there = phys.new_place("there") 92 | 93 | nowhere = notphys.new_place("nowhere") 94 | 95 | @phys.place.rule(always=True) 96 | def rule1(place): 97 | place["run"] = True 98 | 99 | @notphys.place.rule 100 | def rule2(place): 101 | place["notrun"] = False 102 | 103 | engine.next_turn() 104 | 105 | assert here["run"] 106 | assert there["run"] 107 | assert "run" not in nowhere 108 | assert "notrun" not in here 109 | assert "notrun" not in there 110 | 111 | 112 | def test_character_portal_rule_poll(engine): 113 | phys = engine.new_character("physical") 114 | nonphys = engine.new_character("ethereal") 115 | 116 | place0 = phys.new_place(0) 117 | place1 = phys.new_place(1) 118 | portl0 = place0.new_portal(place1) 119 | place2 = phys.new_place(2) 120 | portl1 = place1.new_portal(place2) 121 | 122 | nonplace0 = nonphys.new_place(0) 123 | nonplace1 = nonphys.new_place(1) 124 | nonportl = nonplace0.new_portal(nonplace1) 125 | 126 | @phys.portal.rule(always=True) 127 | def rule0(portal): 128 | portal["run"] = True 129 | 130 | @phys.portal.rule 131 | def rule1(portal): 132 | portal["notrun"] = False 133 | 134 | engine.next_turn() 135 | 136 | assert portl0["run"] 137 | assert portl1["run"] 138 | assert "run" not in nonportl 139 | assert "notrun" not in portl0 140 | assert "notrun" not in portl1 141 | assert "notrun" not in nonportl 142 | -------------------------------------------------------------------------------- /elide/elide/assets/rltiles/weapon.atlas: -------------------------------------------------------------------------------- 1 | {"weapon-0.png": {"needle": [274, 342, 32, 32], "bow": [478, 478, 32, 32], "aklys": [2, 478, 32, 32], "axe": [172, 478, 32, 32], "katana2": [342, 376, 32, 32], "crossbow_bolt": [172, 444, 32, 32], "long_sword2": [2, 342, 32, 32], "spwpn_sceptre_of_asmodeus": [410, 274, 32, 32], "double_sword": [444, 444, 32, 32], "spwpn_scythe_of_curses": [478, 274, 32, 32], "glaive": [70, 376, 32, 32], "dart": [274, 444, 32, 32], "dwarvish_short_sword": [2, 410, 32, 32], "fauchard": [410, 410, 32, 32], "giant_club": [2, 376, 32, 32], "orcish_great_sword": [478, 342, 32, 32], "orcish_glaive": [444, 342, 32, 32], "yumi": [138, 206, 32, 32], "elven_bow": [104, 410, 32, 32], "battle_axe3": [274, 478, 32, 32], "blowgun": [410, 478, 32, 32], "elven_dagger": [172, 410, 32, 32], "elven_broadsword": [138, 410, 32, 32], "two_handed_sword": [444, 240, 32, 32], "short_sword": [444, 308, 32, 32], "rubber_hose": [206, 308, 32, 32], "spiked_flail": [274, 274, 32, 32], "orcish_long_sword": [2, 308, 32, 32], "stiletto": [274, 240, 32, 32], "battle_axe": [308, 478, 32, 32], "elven_arrow": [70, 410, 32, 32], "ranseur": [172, 308, 32, 32], "spwpn_mace_of_variability": [376, 274, 32, 32], "dwarvish_mattock": [478, 444, 32, 32], "morningstar2": [206, 342, 32, 32], "short_sword2": [410, 308, 32, 32], "triple_sword": [376, 240, 32, 32], "silver_saber": [70, 274, 32, 32], "executioner_axe2": [308, 410, 32, 32], "orcish_arrow": [342, 342, 32, 32], "battle_axe2": [240, 478, 32, 32], "sabre2": [274, 308, 32, 32], "ancient_sword": [36, 478, 32, 32], "arrow": [104, 478, 32, 32], "ancus": [70, 478, 32, 32], "war_axe": [2, 206, 32, 32], "katana": [410, 376, 32, 32], "hammer2": [172, 376, 32, 32], "javelin": [308, 376, 32, 32], "bec_de_corbin": [342, 478, 32, 32], "mace_large": [172, 342, 32, 32], "scimitar": [342, 308, 32, 32], "athame": [138, 478, 32, 32], "flail": [478, 410, 32, 32], "katana3": [376, 376, 32, 32], "spwpn_vampires_tooth": [206, 240, 32, 32], "sling": [138, 274, 32, 32], "partisan": [104, 308, 32, 32], "falchion": [376, 410, 32, 32], "flail2": [444, 410, 32, 32], "voulge": [478, 240, 32, 32], "ya": [104, 206, 32, 32], "giant_spiked_club": [36, 376, 32, 32], "demon_trident": [376, 444, 32, 32], "halberd": [138, 376, 32, 32], "elven_spear": [240, 410, 32, 32], "spwpn_sceptre_of_torment": [444, 274, 32, 32], "hand_crossbow": [274, 376, 32, 32], "worm_tooth": [70, 206, 32, 32], "quarterstaff": [138, 308, 32, 32], "war_hammer": [36, 206, 32, 32], "mace2": [104, 342, 32, 32], "scalpel": [308, 308, 32, 32], "dart-p": [308, 444, 32, 32], "tsurugi": [410, 240, 32, 32], "orcish_bow": [376, 342, 32, 32], "morning_star": [240, 342, 32, 32], "sword_orcish": [308, 240, 32, 32], "guisarme": [104, 376, 32, 32], "elven_short_sword": [206, 410, 32, 32], "spwpn_singing_sword": [2, 240, 32, 32], "hand_axe": [240, 376, 32, 32], "mace": [138, 342, 32, 32], "spwpn_glaive_of_prune": [308, 274, 32, 32], "spear2": [172, 274, 32, 32], "demon_whip": [410, 444, 32, 32], "spetum": [240, 274, 32, 32], "orcish_spear": [70, 308, 32, 32], "hammer": [206, 376, 32, 32], "bill_guisarme": [376, 478, 32, 32], "silver_spear": [104, 274, 32, 32], "needle-p": [308, 342, 32, 32], "dagger": [240, 444, 32, 32], "runesword": [240, 308, 32, 32], "orcish_dagger": [410, 342, 32, 32], "crossbow": [138, 444, 32, 32], "silver_dagger": [36, 274, 32, 32], "executioner_axe": [342, 410, 32, 32], "trident": [342, 240, 32, 32], "dwarvish_spear": [36, 410, 32, 32], "silver_arrow": [2, 274, 32, 32], "crysknife": [206, 444, 32, 32], "bullwhip": [36, 444, 32, 32], "orcish_short_sword": [36, 308, 32, 32], "demon_blade": [342, 444, 32, 32], "spwpn_knife_of_accuracy": [342, 274, 32, 32], "spwpn_staff_of_olgreb": [70, 240, 32, 32], "shuriken": [478, 308, 32, 32], "bardiche": [206, 478, 32, 32], "scythe": [376, 308, 32, 32], "spwpn_sword_of_power": [138, 240, 32, 32], "knife": [444, 376, 32, 32], "spwpn_sword_of_cerebov": [104, 240, 32, 32], "lance": [478, 376, 32, 32], "crossbow2": [104, 444, 32, 32], "eveningstar": [274, 410, 32, 32], "long_sword": [36, 342, 32, 32], "spwpn_sword_of_zonguldrok": [172, 240, 32, 32], "broadsword": [2, 444, 32, 32], "spear": [206, 274, 32, 32], "boomerang": [444, 478, 32, 32], "spwpn_wrath_of_trog": [240, 240, 32, 32], "spwpn_staff_of_dispater": [36, 240, 32, 32], "club": [70, 444, 32, 32], "lucern_hammer": [70, 342, 32, 32]}} -------------------------------------------------------------------------------- /elide/elide/stepper.py: -------------------------------------------------------------------------------- 1 | # This file is part of Elide, frontend to Lisien, a framework for life simulation games. 2 | # Copyright (c) Zachary Spector, public@zacharyspector.com 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, version 3. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | from kivy.app import App 16 | from kivy.clock import mainthread 17 | from kivy.graphics import Color, Line 18 | from kivy.properties import NumericProperty, ObjectProperty, StringProperty 19 | from kivy.uix.button import Button 20 | from kivy.uix.label import Label 21 | from kivy.uix.recycleview import RecycleView 22 | 23 | from .util import logwrap 24 | 25 | 26 | class RuleStepper(RecycleView): 27 | name = StringProperty() 28 | 29 | @logwrap(section="RuleStepper") 30 | def from_rules_handled_turn(self, rules_handled_turn): 31 | data = [ 32 | { 33 | "widget": "RuleStepperRuleButton", 34 | "name": "start", 35 | "end_tick": 0, 36 | "height": 40, 37 | } 38 | ] 39 | all_rules = [] 40 | for rbtyp, rules in rules_handled_turn.items(): 41 | for tick, rule in rules.items(): 42 | all_rules.append((tick, rbtyp, rule)) 43 | all_rules.sort() 44 | prev_tick = 0 45 | last_entity = None 46 | last_rulebook = None 47 | lasttyp = None 48 | for tick, rbtyp, (entity, rulebook, rule) in all_rules: 49 | if tick == prev_tick: 50 | continue # Rules that aren't triggered are still "handled". Ignore them. 51 | if lasttyp != rbtyp: 52 | data.append({"widget": "RulebookTypeLabel", "name": rbtyp}) 53 | lasttyp = rbtyp 54 | rulebook_per_entity = rbtyp in {"thing", "place", "portal"} 55 | if not rulebook_per_entity: 56 | if rulebook != last_rulebook: 57 | last_rulebook = rulebook 58 | data.append({"widget": "RulebookLabel", "name": rulebook}) 59 | if entity != last_entity: 60 | last_entity = entity 61 | data.append({"widget": "EntityLabel", "name": entity}) 62 | if rulebook_per_entity: 63 | if rulebook != last_rulebook: 64 | rulebook = last_rulebook 65 | data.append({"widget": "RulebookLabel", "name": rulebook}) 66 | data.append( 67 | { 68 | "widget": "RuleStepperRuleButton", 69 | "name": rule, 70 | "start_tick": prev_tick, 71 | "end_tick": tick, 72 | "height": 40, 73 | } 74 | ) 75 | prev_tick = tick 76 | self.data = data 77 | 78 | 79 | class RuleStepperRuleButton(Button): 80 | name = StringProperty() 81 | start_tick = NumericProperty() 82 | end_tick = NumericProperty() 83 | tick = NumericProperty() 84 | set_tick = ObjectProperty() 85 | 86 | def __init__(self, **kwargs): 87 | super(RuleStepperRuleButton, self).__init__(**kwargs) 88 | app = App.get_running_app() 89 | binds = app._bindings 90 | for att in ("pos", "size", "tick"): 91 | binds[ 92 | "RuleStepperRuleButton", 93 | self.name, 94 | self.start_tick, 95 | self.end_tick, 96 | "pos", 97 | ].add(self.fbind(att, self.upd_line)) 98 | 99 | @logwrap(section="RuleStepperRuleButton") 100 | def on_release(self, *args): 101 | self.set_tick(self.end_tick) 102 | self.tick = self.end_tick 103 | 104 | @mainthread 105 | @logwrap(section="RuleStepperRuleButton") 106 | def upd_line(self, *_): 107 | if hasattr(self, "color_inst"): 108 | if self.tick == self.end_tick: 109 | self.color_inst.rgba = [1, 0, 0, 1] 110 | self.line.points = [self.x, self.y, self.right, self.y] 111 | else: 112 | self.color_inst.rgba = [0, 0, 0, 0] 113 | else: 114 | with self.canvas: 115 | self.color_inst = Color( 116 | rgba=( 117 | [1, 0, 0, 1] 118 | if self.tick in (self.start_tick, self.end_tick) 119 | else [0, 0, 0, 0] 120 | ) 121 | ) 122 | self.line = Line( 123 | points=[self.x, self.top, self.right, self.top] 124 | ) 125 | 126 | 127 | class EntityLabel(Label): 128 | name = ObjectProperty() 129 | 130 | 131 | class RulebookLabel(Label): 132 | name = ObjectProperty() # rulebooks may have tuples for names 133 | 134 | 135 | class RulebookTypeLabel(Label): 136 | name = StringProperty() 137 | -------------------------------------------------------------------------------- /lisien/lisien/examples/polygons.py: -------------------------------------------------------------------------------- 1 | # Parable of the Polygons is public domain. 2 | # This implementation is part of lisien, a framework for life simulation games. 3 | # Copyright (c) Zachary Spector, public@zacharyspector.com 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | """Implementation of Parable of the Polygons http://ncase.me/polygons/""" 17 | 18 | from operator import attrgetter 19 | 20 | from lisien.character import grid_2d_8graph 21 | 22 | 23 | def install(eng): 24 | @eng.function 25 | def cmp_neighbor_shapes(poly, cmp, stat): 26 | """Compare the proportion of neighboring polys with the same shape as this one 27 | 28 | Count the neighboring polys that are the same shape as this one, and return how that compares with 29 | some stat on the poly's user. 30 | 31 | """ 32 | from operator import attrgetter 33 | 34 | home = poly.location 35 | similar = 0 36 | n = 0 37 | for neighbor_home in sorted(home.neighbors(), key=attrgetter("name")): 38 | # assume only 1 poly per home for now; this is faithful to the original 39 | try: 40 | neighbor = next( 41 | iter( 42 | sorted( 43 | neighbor_home.contents(), key=attrgetter("name") 44 | ) 45 | ) 46 | ) 47 | except StopIteration: 48 | continue 49 | if neighbor.leader is poly.leader: 50 | similar += 1 51 | if n == 0: 52 | # You'd always want to move if you had *no* neighbors, I guess 53 | return True 54 | return cmp(poly.character.stat[stat], similar / n) 55 | 56 | @eng.rule(neighborhood=1) 57 | def relocate(poly): 58 | """Move to a random unoccupied place""" 59 | if "unoccupied" not in poly.engine.universal: 60 | # .values() sets, like sets generally, are unordered. 61 | # You have to sort them yourself if you want determinism. 62 | poly.engine.universal["unoccupied"] = sorted( 63 | [ 64 | place 65 | for place in poly.character.place.values() 66 | if not place.content 67 | ], 68 | key=attrgetter("name"), 69 | ) 70 | unoccupied = poly.engine.universal["unoccupied"] 71 | newloc = unoccupied.pop(poly.engine.randrange(0, len(unoccupied))) 72 | while not newloc: # the unoccupied location may have been deleted 73 | newloc = unoccupied.pop(poly.engine.randrange(0, len(unoccupied))) 74 | unoccupied.append(poly.location) 75 | poly.location = newloc 76 | 77 | @relocate.trigger 78 | def similar_neighbors(poly): 79 | """Trigger when my neighborhood fails to be enough like me""" 80 | from operator import ge 81 | 82 | return poly.engine.function.cmp_neighbor_shapes( 83 | poly, ge, "min_sameness" 84 | ) 85 | 86 | @relocate.trigger 87 | def dissimilar_neighbors(poly): 88 | """Trigger when my neighborhood gets too much like me""" 89 | from operator import lt 90 | 91 | return poly.engine.function.cmp_neighbor_shapes( 92 | poly, lt, "max_sameness" 93 | ) 94 | 95 | eng.rulebook["parable"] = [relocate] 96 | 97 | physical = eng.new_character( 98 | "physical", 99 | min_sameness=0.1, 100 | max_sameness=0.9, 101 | _config={ 102 | "min_sameness": {"control": "slider", "min": 0.0, "max": 1.0}, 103 | "max_sameness": {"control": "slider", "min": 0.0, "max": 1.0}, 104 | }, 105 | data=grid_2d_8graph(20, 20), 106 | ) 107 | square = eng.new_character("square") 108 | triangle = eng.new_character("triangle") 109 | square.unit.rulebook = triangle.unit.rulebook = "parable" 110 | 111 | empty = sorted(physical.place.values(), key=attrgetter("name")) 112 | eng.shuffle(empty) 113 | # distribute 30 of each shape randomly among the empty places 114 | for i in range(1, 31): 115 | square.add_unit( 116 | empty.pop().new_thing( 117 | "square%i" % i, _image_paths=["atlas://polygons/meh_square"] 118 | ) 119 | ) 120 | for i in range(1, 31): 121 | triangle.add_unit( 122 | empty.pop().new_thing( 123 | "triangle%i" % i, 124 | _image_paths=["atlas://polygons/meh_triangle"], 125 | ) 126 | ) 127 | 128 | 129 | if __name__ == "__main__": 130 | import shutil 131 | from tempfile import TemporaryDirectory 132 | 133 | from lisien import Engine 134 | 135 | with TemporaryDirectory() as td: 136 | with Engine( 137 | td, 138 | random_seed=69105, 139 | connect_string=f"sqlite:///{td}/world.sqlite3", 140 | workers=0, 141 | ) as eng: 142 | install(eng) 143 | archive_filename = shutil.make_archive("polygons", "zip", td) 144 | print("Exported to " + str(archive_filename)) 145 | --------------------------------------------------------------------------------