diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index f958556..fd7b292 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,7 +1,20 @@
+
+
+
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
index c6e2461..b3b66c4 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,7 +5,15 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Python: Module",
+ "name": "Sample: Talk of the Town",
+ "type": "python",
+ "request": "launch",
+ "program": "samples/talktown.py",
+ "console": "integratedTerminal",
+ "justMyCode": true
+ },
+ {
+ "name": "Python: Module - Neighborly",
"type": "python",
"request": "launch",
"module": "neighborly",
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..15d9011
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,46 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres mostly to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+There may be minor-version updates that contain breaking changes, but do not warrant
+incrementing to a completely new version number.
+
+## [0.9.4]
+
+**0.9.4 is not compatible with 0.9.3**
+
+### Added
+
+- `Building` class to identify when a business currently exists within the town vs.
+ when it is archived within the ECS for story sifting.
+- Systems to update business components when they are pending opening, open for business, and closed for business and
+ awaiting demolition.
+- New status components to identify Businesses at different phases in their lifecycle:
+ `ClosedForBusiness`, `OpenForBusiness`, `PendingOpening`
+- New PyGame UI elements for displaying information about a GameObject
+- Strings may be used as world seeds
+- `CHANGELOG.md` file
+
+### Updated
+
+- PyGame sample to use the new API
+- Docstrings for `Simulation` and `SimulationBuilder` classes
+- `SimulationBuilder` class
+- Moved isort configuration to `pyproject.toml`
+
+### Removed
+
+- Jupyter notebook and pygame samples
+- samples category from dependencies within `setup.cfg`
+- `events`, `town`, `land grid`, and `relationships` fields from `NeighborlyJsonExporter`.
+ These are duplicated when serializing the resources.
+- `SimulationBuilder.add_system()` and `SimulationBuilder.add_resource()`. To add
+ these, users need to encapsulate their content within a plugin
+- Flake8 configuration from `setup.cfg`
+
+### Fixed
+
+- Bug in Business operating hours regex that did not recognize AM/PM strings
+- `setup.cfg` did not properly include data files in the wheel build.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 0d803e1..642f711 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -116,7 +116,7 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
+. Translations are available at
+.
diff --git a/MANIFEST.in b/MANIFEST.in
index ad9e7c3..6c4e563 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,4 @@
include LICENSE
+include CHANGELOG.md
include CODE_OF_CONDUCT.md
-recursive-include src/neighborly/plugins/default_plugin/data *
+recursive-include src/neighborly/plugins/defaults/data *
diff --git a/README.md b/README.md
index aa4060f..bf6d3f5 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,112 @@
Neighborly
-
Social simulation engine for procedurally generating towns of characters
-
-
-
+
+
+
+
+
# Overview
-Neighborly is a social simulation engine for procedurally generating towns of characters. It simulates
-the lives of each character, their jobs, routines, relationships, and life events. Neighborly utilizes
-an entity-component system architecture, and enables users to specify custom character types, businesses,
-occupations, and life events.
+Neighborly is a Python framework for generating and forward simulating towns of
+characters over large periods of time (decades to centuries). It uses a character-driven
+social simulation that forward-simulates the lives of each character, their jobs,
+routines, relationships, and life events. Users can specify custom characters,
+residential/commercial buildings, occupations, life events, social actions, and more.
+
+Currently, _Neighborly_ works best as narrative data generator. When the simulation
+ends, users can save the history of events, characters, relationships, and other stuff.
-Neighborly takes lessons learned from working with
+Neighborly was inspired by lessons learned from working with
[_Talk of the Town_](https://github.com/james-owen-ryan/talktown)
-and aims to give people better documentation, simpler interfaces, and more opportunities for extension and content authoring.
+and aims to give people better documentation, simpler interfaces, and more opportunities
+for extension and content authoring.
-# Core Features
+## Core Features
-* Create custom Character Archetypes and have them all interact within the same simulation
-* Create custom Business and Occupation definitions
-* Configure simulation data using YAML or in code with Python
+* Create custom character, buildings, life events, and social actions
+* Commandline interface (CLI) tool
+* Configure the CLI using YAML text files
* Plugin architecture allows users to modularize and share their custom content
-* Low fidelity simulation simulates the macro events in character's lives (relationship milestones, job changes, victories, tragic events)
* Export simulation state to JSON for further data processing
-# Tutorials and How-to Guides
+# How to use
-I plan to add these after I have finished implementing Neighborly's core
-functionality. I will try to align them with the sample projects, but we
-will see how the first pre-release looks. For now, loosely refer to the
-samples. Although, they too lag behind breaking changes to the core codebase.
+Below are instructions for installing Neighborly and the options one has for using it
+in their projects. If you want examples of how to use Neighborly and how to extend it
+with custom content, please refer to
+[Neighborly's wiki](https://github.com/ShiJbey/neighborly/wiki) and the sample scripts
+in the [_samples_ directory](https://github.com/ShiJbey/neighborly/tree/main/samples).
-# Installing from PyPI
+## Installation
-Neighborly is available to install via pip.
+Neighborly is available to install from PyPI.
```bash
pip install neighborly
```
-# Running the commandline tool
-
-Neighborly can be run as a module from the commandline. By default, it runs a
-builtin version of **Talk of the Town**. You can configure the simulation settings
-by creating a `neighborlyconfig.yaml` file in the same directory where you're
-running the application. When world generation concludes, Neighborly will write
-the final simulation data to a JSON file with the name of the town and the
-seed used for random number generation.
+Or you can install it by cloning the main branch of this repo and installing that.
```bash
-python -m neighborly
+git clone https://github.com/ShiJbey/neighborly.git
-# Please use the following command for additional help with running Neighborly's CLI
-python -m neighborly --help
+cd neighborly
+
+python -m pip install .
```
+## Using as a library
+
+Neighborly can be used as a library within a Python script or package.
+The `samples` directory contains python scripts that use Neighborly this
+way. Please refer to them when creating new Plugins and other content.
+
+## Running the CLI
+
+Neighborly can be run as a module `$ python -m neighborly` or commandline `$ neighborly`
+script. If you require additional help while running, please use
+`python -m neighborly --help` or `neighborly --help`.
+
+By default, Neighborly runs a builtin version of **Talk of the Town**. However, you can
+configure the simulation settings by creating a `neighborlyconfig.yaml` file in
+the same directory where you're running the CLI. Please refer to the
+[wiki](https://github.com/ShiJbey/neighborly/wiki/Neighborly-CLI) for a list of
+valid configuration settings.
+
+When world generation concludes, Neighborly can write the final simulation data
+to a JSON file with the name of the town and the seed used for random number
+generation.
+
+## Running the Samples
-# Installing for local development
+Neighborly provides sample simulations to show users how to customize
+it to create new story world themes.
-If you wish to download a Neighborly for local development, follow the these instructions.
+```bash
+# Make sure that you've activated your python virtual environment
+# Replace .py with the name of the
+# sample you want to run
+python ./samples/.py
+```
+
+## Installing for local development
+
+If you wish to download a Neighborly for local development, you need to clone/fork this
+repository and install using the _editable_ flag (-e). Please see the instructions
+below.
```bash
# Step One: Clone Repository
@@ -92,12 +127,12 @@ python -m venv venv
python -m pip install -e "."
```
-# Running the Tests
+## Running the Tests
The tests are currently out-of-date and may refer to systems
and logic that no longer exists in Neighborly. The codebase
-changes so frequently that it hasn't been worth the time.
-As modules become more established, I will add proper tests for them.
+changes so frequently that it hasn't been worth the time.
+As modules become more established, I will add proper tests for them.
Feel free to contribute tests by forking the repo, adding your test(s), and
submitting a pull request with a description of your test cases. Your commits
should only contain changes to files within the `tests` directory. If you
@@ -112,25 +147,16 @@ python -m pip install -e ".[tests]"
# Step 2: Run Pytest
pytest
-```
-
-# Running the Samples
-
-Please follow the steps below to run the sample simulations.
-We also have examples for using Neighborly in a IPython
-notebook and with PyGame.
-```bash
-# Step 1: Install dependencies for samples
-python -m pip install -e ".[samples]"
-
-# Step 2: Run desired sample
-python ./samples/.py
+# Step3 : (Optional) Generate a test coverage report
+pytest --cov=neighborly tests/
```
# Documentation
-Neighborly uses [Numpy-style](https://numpydoc.readthedocs.io/en/latest/format.html) docstrings in code and full documentation can be found in the [Wiki](https://github.com/ShiJbey/neighborly/wiki).
+Neighborly uses [Numpy-style](https://numpydoc.readthedocs.io/en/latest/format.html)
+docstrings in code and full documentation can be found in the
+[Wiki](https://github.com/ShiJbey/neighborly/wiki).
When adding docstrings for existing or new bits of code please use the following
references for how to format your contributions:
@@ -138,13 +164,8 @@ references for how to format your contributions:
* [Sphinx Napoleon Plugin for processing Numpy Docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html)
* [Example Numpy Style Docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/example_numpy.html#example-numpy)
-
# Contributing
-If you are interested in contributing to Neighborly, feel free to fork this repository, make your changes, and submit a pull-request. Please keep in mind that this project is a tool for creativity and learning. We have a [code of conduct](./CODE_OF_CONDUCT.md) to encourage healthy collaboration, and will enforce it if we need to.
-
-**WARNING::** This repository's structure in high flux. Parts of the code get shifted to make the APIs cleaner for use.
-
Here are some ways that people can contribute to Neighborly:
1. Proposing/Implementing new features
@@ -154,34 +175,45 @@ Here are some ways that people can contribute to Neighborly:
5. Filing issues
6. Contributing tutorials/how-tos to the wiki
7. Fixing grammar and spelling in the wiki
-8. Creating new samples
+8. Creating new samples/plugins
+
+If you are interested in contributing to Neighborly, there are multiple ways to get
+involved, and not all of them require you to be proficient with GitHub. Interested
+parties can contribute to the core code base of Neighborly and/or create nre content
+in the way of plugins. I love feedback, and if you have any questions, create a new
+issue, and I will do my best to answer. If you want to contribute to the core code,
+free to fork this repository, make your changes, and submit a pull-request with a
+description of your contribution. Please keep in mind that this project is a
+tool for creativity and learning. I have a [code of conduct](./CODE_OF_CONDUCT.md) to
+encourage healthy collaboration, and will enforce it if I need to.
## Code Style
-Neighborly does not have a set-in-stone code style yet, but I have started integrating
-isort, black, and flake8 into the development workflow in VSCode.
-
-You can follow [these instructions](https://black.readthedocs.io/en/stable/integrations/editors.html) for setting up both black and isort. And I found this gist helpful for getting [flake8 working in PyCharm](https://gist.github.com/tossmilestone/23139d870841a3d5cba2aea28da1a895).
+Neighborly uses [_Black_](https://black.readthedocs.io/en/stable/) to handle code style
+and sorts imports using [_isort_](https://pycqa.github.io/isort/). You can follow
+[these instructions](https://black.readthedocs.io/en/stable/integrations/editors.html)
+for setting up both black and isort.
# Notes
## Non-Deterministic Behavior
-The goal of having a seeded pseudo random simulation is so that users experience deterministic behavior when using the
-same starting seed. We try to remove all forms of non-determinism, but some slip through. The known areas are listed
-below. If you find any, please make a new issue with details of the behavior.
+The goal of having a seeded pseudo random simulation is so that users experience
+deterministic behavior when using the same starting seed. I try to remove all forms of
+non-determinism, but some slip through. The known areas are listed below. If you find
+any, please make a new issue with details of the behavior.
-* Names may not be consistent when using the same seed. Currently, names are generated
- using [Tracery](https://github.com/aparrish/pytracery). We would need to create a custom version that uses an RNG
- instance instead of the global random module to generate names.
+* Neighborly uses [Tracery](https://github.com/aparrish/pytracery) to generate names for
+characters and locations, and these names may not be consistent despite using the same
+rng seed value.
## DMCA Statement
-Upon receipt of a notice alleging copyright infringement, I will take whatever action it deems
-appropriate within its sole discretion, including removal of the allegedly infringing materials.
-
-The repo image is something fun that I made. I love _The Simpsons_, and I couldn't think of anything more neighborly
-than Ned Flanders. If the copyright owner for _The Simpsons_ would like me to take it down,
-please contact me.
+Upon receipt of a notice alleging copyright infringement, I will take whatever action it
+deems appropriate within its sole discretion, including removal of the allegedly
+infringing materials.
-The same takedown policy applies to any code samples inspired by TV shows, movies, and games.
+The repo image is something fun that I made. I love _The Simpsons_, and I couldn't think
+of anyone more neighborly than Ned Flanders. If the copyright owner for _The Simpsons_
+would like me to take it down, please contact me. The same takedown policy applies to
+any code samples inspired by TV shows, movies, and games.
diff --git a/pyproject.toml b/pyproject.toml
index 374b58c..7f2ef2c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,3 +4,12 @@ requires = [
"wheel"
]
build-backend = "setuptools.build_meta"
+
+[tool.black]
+line-length = 88
+
+[tool.isort]
+profile = "black"
+default_section = "THIRDPARTY"
+known_first_party = "neighborly"
+src_paths = ["src/neighborly", "test", "samples"]
diff --git a/samples/demon_slayer.py b/samples/demon_slayer.py
index a1bcdcb..45f797b 100644
--- a/samples/demon_slayer.py
+++ b/samples/demon_slayer.py
@@ -40,25 +40,40 @@
import math
import time
from enum import IntEnum
-from typing import Optional, Tuple, List, Any, Dict
+from typing import Any, Dict, List, Optional, Tuple
from ordered_set import OrderedSet
-from neighborly.builtin.statuses import Deceased
+from neighborly import Plugin, Simulation, SimulationBuilder
+from neighborly.builtin.components import (
+ Active,
+ Age,
+ CanAge,
+ CanGetPregnant,
+ CurrentLocation,
+ Deceased,
+ LifeStages,
+)
+from neighborly.builtin.helpers import move_to_location
+from neighborly.core import query
from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import Component, World, GameObject
+from neighborly.core.ecs import Component, GameObject, World
from neighborly.core.engine import NeighborlyEngine
-from neighborly.core.life_event import LifeEventType, EventRoleType, LifeEvent, LifeEventLibrary, EventResult, \
- LifeEventLog
+from neighborly.core.event import Event
+from neighborly.core.life_event import (
+ ILifeEvent,
+ LifeEvent,
+ LifeEventRoleType,
+ LifeEvents,
+)
from neighborly.core.location import Location
-from neighborly.plugins.default_plugin import DefaultPlugin
-from neighborly.plugins.talktown import TalkOfTheTownPlugin
-from neighborly.plugins.weather_plugin import WeatherPlugin
-from neighborly.simulation import Plugin, Simulation, SimulationBuilder
+from neighborly.exporter import NeighborlyJsonExporter
+from neighborly.plugins import defaults, talktown, weather
class DemonSlayerRank(IntEnum):
"""Various ranks within the DemonSlayerCorp"""
+
Mizunoto = 0
Mizunoe = 1
Kanoto = 2
@@ -74,6 +89,7 @@ class DemonSlayerRank(IntEnum):
class BreathingStyle(IntEnum):
"""Various breathing styles for demon slayers"""
+
Flower = 0
Love = 1
Flame = 2
@@ -104,7 +120,7 @@ class DemonSlayer(Component):
This slayer's power level (used to calculate
chance of winning a battle).
breathing_style: BreathingStyle
- What style of breathing does this character use
+ What style of breathing does this entity use
"""
__slots__ = "rank", "kills", "power_level", "breathing_style"
@@ -127,14 +143,15 @@ def to_dict(self) -> Dict[str, Any]:
**super().to_dict(),
"rank": str(self.rank.name),
"power_level": self.power_level,
- "breathing_style": str(self.breathing_style)
+ "breathing_style": str(self.breathing_style),
}
def on_archive(self) -> None:
if self.rank == DemonSlayerRank.Hashira:
# Remove the hashira from the DemonSlayerCorp
self.gameobject.world.get_resource(DemonSlayerCorps).retire_hashira(
- self.gameobject.id)
+ self.gameobject.id
+ )
self.gameobject.remove_component(type(self))
@@ -217,12 +234,13 @@ def has_vacancy(self, breathing_style: BreathingStyle) -> bool:
def to_dict(self) -> Dict[str, Any]:
return {
"hashira": list(self.hashira),
- "former_hashira": list(self.former_hashira)
+ "former_hashira": list(self.former_hashira),
}
class DemonRank(IntEnum):
"""The various ranks held by demons"""
+
LowerDemon = 0
Demon = 1
BloodDemon = 2
@@ -267,8 +285,7 @@ def __init__(
@classmethod
def create(cls, world: World, **kwargs) -> Component:
return cls(
- power_level=kwargs.get("power_level", 0),
- turned_by=kwargs.get("turned_by")
+ power_level=kwargs.get("power_level", 0), turned_by=kwargs.get("turned_by")
)
def to_dict(self) -> Dict[str, Any]:
@@ -277,7 +294,7 @@ def to_dict(self) -> Dict[str, Any]:
"rank": str(self.rank.name),
"power_level": self.power_level,
"kills": self.kills,
- "turned_by": self.turned_by if self.turned_by else -1
+ "turned_by": self.turned_by if self.turned_by else -1,
}
def on_archive(self) -> None:
@@ -316,7 +333,7 @@ class DemonKingdom:
"_lower_moons",
"_upper_moons",
"_former_upper_moons",
- "_former_lower_moons"
+ "_former_lower_moons",
)
def __init__(self) -> None:
@@ -374,7 +391,7 @@ def to_dict(self) -> Dict[str, Any]:
"lower_moons": list(self.lower_moons),
"former_lower_moons": list(self.lower_moons),
"upper_moons": list(self.upper_moons),
- "former_upper_moons": list(self.former_upper_moons)
+ "former_upper_moons": list(self.former_upper_moons),
}
@@ -416,6 +433,7 @@ def to_dict(self) -> Dict[str, Any]:
# UTILITY FUNCTIONS
########################################
+
def probability_of_winning(rating_a: int, rating_b: int) -> float:
"""
Return the probability of a defeating b
@@ -423,9 +441,9 @@ def probability_of_winning(rating_a: int, rating_b: int) -> float:
Parameters
----------
rating_a: int
- Rating of character A
+ Rating of entity A
rating_b: int
- Rating of character B
+ Rating of entity B
"""
return 1.0 / (1 + math.pow(10, (rating_a - rating_b) / ELO_SCALE))
@@ -435,9 +453,10 @@ def update_power_level(
loser_rating: int,
winner_expectation: float,
loser_expectation: float,
- k: int = 16
+ k: int = 16,
) -> Tuple[int, int]:
"""
+ Perform ELO calculation for new ratings
Parameters
----------
@@ -516,20 +535,13 @@ def power_level_to_demon_rank(power_level: int) -> DemonRank:
return DemonRank.LowerDemon
-def at_same_location(a: GameObject, b: GameObject) -> bool:
- """Return True if these characters are at the same location"""
- return (
- a.get_component(GameCharacter).location
- == b.get_component(GameCharacter).location
- )
-
-
########################################
# CUSTOM LIFE EVENTS
########################################
-def become_demon_slayer(probability: float = 1) -> LifeEventType:
- def bind_character(world: World, event: LifeEvent):
+
+def become_demon_slayer(probability: float = 1) -> ILifeEvent:
+ def bind_character(world: World, event: Event, candidate: Optional[GameObject]):
candidates = []
@@ -538,68 +550,61 @@ def bind_character(world: World, event: LifeEvent):
continue
if character.gameobject.has_component(Demon):
continue
- if character.age >= character.character_def.life_stages["teen"]:
+ if (
+ character.gameobject.get_component(Age).value
+ >= character.gameobject.get_component(LifeStages).stages["teen"]
+ ):
candidates.append(character.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
+ def execute(world: World, event: Event):
character = world.get_gameobject(event["Character"])
character.add_component(DemonSlayer.create(world))
- return EventResult(generated_events=[event])
- return LifeEventType(
+ return LifeEvent(
"BecameDemonSlayer",
probability=probability,
- roles=[EventRoleType("Character", binder_fn=bind_character)],
- execute_fn=execute
+ roles=[LifeEventRoleType("Character", binder_fn=bind_character)],
+ effect=execute,
)
-def demon_slayer_promotion(probability: float = 1.0) -> LifeEventType:
+def demon_slayer_promotion(probability: float = 1.0) -> ILifeEvent:
"""Demon slayer is promoted to the next rank"""
- def bind_demon_slayer(world: World, event: LifeEvent):
+ def bind_demon_slayer(world: World, event: Event, candidate: Optional[GameObject]):
candidates: List[GameObject] = []
for _, demon_slayer in world.get_component(DemonSlayer):
- power_level_rank = power_level_to_slayer_rank(
- demon_slayer.power_level
- )
+ power_level_rank = power_level_to_slayer_rank(demon_slayer.power_level)
if power_level_rank < demon_slayer.rank:
candidates.append(demon_slayer.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
- slayer = world.get_gameobject(event["Slayer"]).get_component(
- DemonSlayer
- )
+ def execute(world: World, event: Event):
+ slayer = world.get_gameobject(event["Slayer"]).get_component(DemonSlayer)
power_level_rank = power_level_to_slayer_rank(slayer.power_level)
slayer.rank = power_level_rank
- return EventResult(generated_events=[event])
- return LifeEventType(
+ return LifeEvent(
"DemonSlayerPromotion",
probability=probability,
- roles=[EventRoleType("Slayer", binder_fn=bind_demon_slayer)],
- execute_fn=execute
+ roles=[LifeEventRoleType("Slayer", binder_fn=bind_demon_slayer)],
+ effect=execute,
)
-def challenge_for_power(probability: float = 1.0) -> LifeEventType:
- def bind_challenger(world: World, event: LifeEvent):
+def challenge_for_power(probability: float = 1.0) -> ILifeEvent:
+ def bind_challenger(world: World, event: Event, candidate: Optional[GameObject]):
"""Get a challenger demon that has someone above them"""
candidates: List[GameObject] = []
for _, demon in world.get_component(Demon):
@@ -607,13 +612,11 @@ def bind_challenger(world: World, event: LifeEvent):
candidates.append(demon.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def bind_opponent(world: World, event: LifeEvent):
+ def bind_opponent(world: World, event: Event, candidate: Optional[GameObject]):
"""Find an opponent for the challenger"""
challenger = world.get_gameobject(event["Challenger"]).get_component(Demon)
candidates: List[GameObject] = []
@@ -625,103 +628,111 @@ def bind_opponent(world: World, event: LifeEvent):
candidates.append(demon.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
+ def execute(world: World, event: Event):
"""Execute the battle"""
challenger = world.get_gameobject(event["Challenger"]).get_component(Demon)
opponent = world.get_gameobject(event["Opponent"]).get_component(Demon)
rng = world.get_resource(NeighborlyEngine).rng
- death_event_type = LifeEventLibrary.get("Death")
- generated_events = [event]
+ _death_event_type = LifeEvents.get("Death")
- slayer_success_chance = probability_of_winning(
- opponent.power_level, challenger.power_level
- )
-
- demon_success_chance = probability_of_winning(
- challenger.power_level, opponent.power_level
- )
+ opponent_success_chance = probability_of_winning(opponent.power_level, challenger.power_level)
+ challenger_success_chance = probability_of_winning(challenger.power_level, opponent.power_level)
- if rng.random() < slayer_success_chance:
+ if rng.random() < opponent_success_chance:
# Demon slayer wins
new_slayer_pl, _ = update_power_level(
opponent.power_level,
challenger.power_level,
- slayer_success_chance,
- demon_success_chance
+ opponent_success_chance,
+ challenger_success_chance,
)
opponent.power_level = new_slayer_pl
- death_event = death_event_type.instantiate(world, Deceased=challenger.gameobject)
+ death_event = _death_event_type.instantiate(
+ world, Deceased=challenger.gameobject
+ )
if death_event:
- death_event_type.execute(world, death_event)
- generated_events.append(death_event)
-
+ _death_event_type.execute(world, death_event)
+ # Update Power Ranking
else:
# Demon wins
_, new_demon_pl = update_power_level(
challenger.power_level,
opponent.power_level,
- demon_success_chance,
- slayer_success_chance,
+ challenger_success_chance,
+ opponent_success_chance,
)
challenger.power_level = new_demon_pl
- death_event = death_event_type.instantiate(world, Deceased=opponent.gameobject)
+ death_event = _death_event_type.instantiate(
+ world, Deceased=opponent.gameobject
+ )
if death_event:
- death_event_type.execute(world, death_event)
- generated_events.append(death_event)
-
- return EventResult(generated_events=generated_events)
+ _death_event_type.execute(world, death_event)
+ # Update Power Ranking
- return LifeEventType(
+ return LifeEvent(
"ChallengeForPower",
roles=[
- EventRoleType("Challenger", binder_fn=bind_challenger),
- EventRoleType("Opponent", binder_fn=bind_opponent)
+ LifeEventRoleType("Challenger", binder_fn=bind_challenger),
+ LifeEventRoleType("Opponent", binder_fn=bind_opponent),
],
probability=probability,
- execute_fn=execute
+ effect=execute,
)
-def devour_human(probability: float = 1.0) -> LifeEventType:
- def execute(world: World, event: LifeEvent):
+def devour_human(probability: float = 1.0) -> ILifeEvent:
+ def execute(world: World, event: Event):
demon = world.get_gameobject(event["Demon"])
victim = world.get_gameobject(event["Victim"])
if victim.has_component(DemonSlayer):
- battle_event_type = LifeEventLibrary.get("Battle")
- battle_event = battle_event_type.instantiate(world, Demon=demon, Slayer=victim)
+ battle_event_type = LifeEvents.get("Battle")
+ battle_event = battle_event_type.instantiate(
+ world, Demon=demon, Slayer=victim
+ )
if battle_event:
battle_event_type.execute(world, battle_event)
- return EventResult(generated_events=[battle_event, event])
+
else:
demon.get_component(Demon).power_level += 1
demon.get_component(Demon).rank = power_level_to_demon_rank(
demon.get_component(Demon).power_level
)
- death_event = LifeEventLibrary.get("Death").instantiate(world, Deceased=victim)
- return EventResult(generated_events=[death_event, event])
+ _death_event_type = LifeEvents.get("Death")
+ _death_event_type.try_execute_event(world, Deceased=victim)
+
+ def bind_demon(world: World, event: Event, candidate: Optional[GameObject] = None):
+ q = query.Query(("Demon",), [query.where(query.has_components(Demon))])
+ candidate_id = candidate.id if candidate else None
+ results = q.execute(world, Demon=candidate_id)
+ if results:
+ return world.get_gameobject(
+ world.get_resource(NeighborlyEngine).rng.choice(results)[0]
+ )
- def bind_victim(world: World, event: LifeEvent):
+ def bind_victim(world: World, event: Event, candidate: Optional[GameObject] = None):
"""Get all people at the same location who are not demons"""
demon = world.get_gameobject(event["Demon"])
+
+ if not demon.has_component(CurrentLocation):
+ return None
+
demon_location = world.get_gameobject(
- demon.get_component(GameCharacter).location
+ demon.get_component(CurrentLocation).location
).get_component(Location)
candidates: List[GameObject] = []
- for character_id in demon_location.characters_present:
+ for character_id in demon_location.entities:
character = world.get_gameobject(character_id)
if character_id == demon.id:
continue
@@ -733,32 +744,30 @@ def bind_victim(world: World, event: LifeEvent):
candidates.append(character)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- return LifeEventType(
+ return LifeEvent(
"DevourHuman",
probability=probability,
roles=[
- EventRoleType("Demon", components=[Demon]),
- EventRoleType("Victim", binder_fn=bind_victim)
+ LifeEventRoleType("Demon", binder_fn=bind_demon),
+ LifeEventRoleType("Victim", binder_fn=bind_victim),
],
- execute_fn=execute
+ effect=execute,
)
-def battle(probability: float = 1.0) -> LifeEventType:
+def battle(probability: float = 1.0) -> ILifeEvent:
"""Have a demon fight a demon slayer"""
- def execute(world: World, event: LifeEvent):
+ def execute(world: World, event: Event):
"""Choose a winner based on their expected success"""
demon = world.get_gameobject(event["Demon"]).get_component(Demon)
slayer = world.get_gameobject(event["Slayer"]).get_component(DemonSlayer)
rng = world.get_resource(NeighborlyEngine).rng
- death_event_type = LifeEventLibrary.get("Death")
+ _death_event_type = LifeEvents.get("Death")
slayer_success_chance = probability_of_winning(
slayer.power_level, demon.power_level
@@ -774,18 +783,18 @@ def execute(world: World, event: LifeEvent):
slayer.power_level,
demon.power_level,
slayer_success_chance,
- demon_success_chance
+ demon_success_chance,
)
slayer.power_level = new_slayer_pl
slayer.rank = power_level_to_slayer_rank(slayer.power_level)
- death_event = death_event_type.instantiate(world, Deceased=demon.gameobject)
+ death_event = _death_event_type.instantiate(
+ world, Deceased=demon.gameobject
+ )
if death_event:
- death_event_type.execute(world, death_event)
-
- return EventResult(generated_events=[death_event, event])
+ _death_event_type.execute(world, death_event)
else:
# Demon wins
@@ -799,26 +808,46 @@ def execute(world: World, event: LifeEvent):
demon.power_level = new_demon_pl
demon.rank = power_level_to_demon_rank(demon.power_level)
- death_event = death_event_type.instantiate(world, Deceased=slayer.gameobject)
+ death_event = _death_event_type.instantiate(
+ world, Deceased=slayer.gameobject
+ )
if death_event:
- death_event_type.execute(world, death_event)
+ _death_event_type.execute(world, death_event)
+
+ def bind_demon(world: World, event: Event, candidate: Optional[GameObject] = None):
+ q = query.Query(("Demon",), [query.where(query.has_components(Demon))])
+ candidate_id = candidate.id if candidate else None
+ results = q.execute(world, Demon=candidate_id)
+ if results:
+ return world.get_gameobject(
+ world.get_resource(NeighborlyEngine).rng.choice(results)[0]
+ )
- return EventResult(generated_events=[death_event, event])
+ def bind_demon_slayer(
+ world: World, event: Event, candidate: Optional[GameObject] = None
+ ):
+ q = query.Query(("DemonSlayer",), [query.where(query.has_components(Demon))])
+ candidate_id = candidate.id if candidate else None
+ results = q.execute(world, DemonSlayer=candidate_id)
+ if results:
+ return world.get_gameobject(
+ world.get_resource(NeighborlyEngine).rng.choice(results)[0]
+ )
- return LifeEventType(
+ return LifeEvent(
"Battle",
probability=probability,
roles=[
- EventRoleType("Demon", components=[Demon]),
- EventRoleType("Slayer", components=[DemonSlayer]),
+ LifeEventRoleType("Demon", bind_demon),
+ LifeEventRoleType("Slayer", bind_demon_slayer),
],
- execute_fn=execute
+ effect=execute,
)
-def turn_into_demon(probability: float = 1.0) -> LifeEventType:
- def bind_new_demon(world: World, event: LifeEvent):
+def turn_into_demon(probability: float = 1.0) -> ILifeEvent:
+ def bind_new_demon(world: World, event: Event, candidate: Optional[GameObject]):
candidates: List[GameObject] = []
for _, character in world.get_component(GameCharacter):
if character.gameobject.has_component(Demon):
@@ -826,46 +855,48 @@ def bind_new_demon(world: World, event: LifeEvent):
if character.gameobject.has_component(DemonSlayer):
continue
- if character.age >= character.character_def.life_stages["teen"]:
+ if (
+ character.gameobject.get_component(Age).value
+ >= character.gameobject.get_component(LifeStages).stages["teen"]
+ ):
candidates.append(character.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
+ def execute(world: World, event: Event):
character = world.get_gameobject(event["Character"])
character.add_component(Demon.create(world))
- return EventResult(generated_events=[event])
+ character.remove_component(CanAge)
+ character.remove_component(CanGetPregnant)
- return LifeEventType(
+ return LifeEvent(
"TurnIntoDemon",
probability=probability,
- roles=[EventRoleType("Character", binder_fn=bind_new_demon)],
- execute_fn=execute
+ roles=[LifeEventRoleType("Character", binder_fn=bind_new_demon)],
+ effect=execute,
)
-def death_event_type() -> LifeEventType:
- def execute(world: World, event: LifeEvent):
+def death_event_type() -> ILifeEvent:
+ def execute(world: World, event: Event):
deceased = world.get_gameobject(event["Deceased"])
deceased.add_component(Deceased())
- deceased.archive()
- return EventResult(generated_events=[event])
+ deceased.remove_component(Active)
+ move_to_location(world, deceased, None)
- return LifeEventType(
+ return LifeEvent(
"Death",
- roles=[EventRoleType("Deceased")],
- execute_fn=execute,
- probability=0.0
+ roles=[LifeEventRoleType("Deceased")],
+ effect=execute,
+ probability=0,
)
-def promotion_to_lower_moon(probability: float = 1.0) -> LifeEventType:
- def bind_demon(world: World, event: LifeEvent):
+def promotion_to_lower_moon(probability: float = 1.0) -> ILifeEvent:
+ def bind_demon(world: World, event: Event, candidate: Optional[GameObject]):
demon_kingdom = world.get_resource(DemonKingdom)
@@ -878,29 +909,24 @@ def bind_demon(world: World, event: LifeEvent):
candidates.append(demon.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
- demon = world.get_gameobject(event["Demon"]).get_component(
- Demon
- )
+ def execute(world: World, event: Event):
+ demon = world.get_gameobject(event["Demon"]).get_component(Demon)
demon.rank = DemonRank.LowerMoon
- return EventResult(generated_events=[event])
- return LifeEventType(
+ return LifeEvent(
"PromotedToLowerMoon",
probability=probability,
- roles=[EventRoleType("Demon", binder_fn=bind_demon)],
- execute_fn=execute
+ roles=[LifeEventRoleType("Demon", binder_fn=bind_demon)],
+ effect=execute,
)
-def promotion_to_upper_moon(probability: float = 1.0) -> LifeEventType:
- def bind_demon(world: World, event: LifeEvent):
+def promotion_to_upper_moon(probability: float = 1.0) -> ILifeEvent:
+ def bind_demon(world: World, event: Event, candidate: Optional[None]):
demon_kingdom = world.get_resource(DemonKingdom)
@@ -913,24 +939,19 @@ def bind_demon(world: World, event: LifeEvent):
candidates.append(demon.gameobject)
if candidates:
- return world.get_resource(NeighborlyEngine).rng.choice(
- candidates
- )
+ return world.get_resource(NeighborlyEngine).rng.choice(candidates)
return None
- def execute(world: World, event: LifeEvent):
- demon = world.get_gameobject(event["Demon"]).get_component(
- Demon
- )
+ def execute(world: World, event: Event):
+ demon = world.get_gameobject(event["Demon"]).get_component(Demon)
demon.rank = DemonRank.UpperMoon
- return EventResult(generated_events=[event])
- return LifeEventType(
+ return LifeEvent(
"PromotedToUpperMoon",
probability=probability,
- roles=[EventRoleType("Demon", binder_fn=bind_demon)],
- execute_fn=execute
+ roles=[LifeEventRoleType("Demon", binder_fn=bind_demon)],
+ effect=execute,
)
@@ -938,17 +959,18 @@ def execute(world: World, event: LifeEvent):
# Plugin
########################################
+
class DemonSlayerPlugin(Plugin):
def setup(self, sim: Simulation, **kwargs) -> None:
- LifeEventLibrary.add(promotion_to_upper_moon())
- LifeEventLibrary.add(promotion_to_lower_moon())
- LifeEventLibrary.add(turn_into_demon(0.3))
- LifeEventLibrary.add(battle(0.7))
- LifeEventLibrary.add(devour_human(0.3))
- LifeEventLibrary.add(challenge_for_power(0.4))
- LifeEventLibrary.add(demon_slayer_promotion(0.7))
- LifeEventLibrary.add(become_demon_slayer(0.3))
- LifeEventLibrary.add(death_event_type())
+ LifeEvents.add(promotion_to_upper_moon())
+ LifeEvents.add(promotion_to_lower_moon())
+ LifeEvents.add(turn_into_demon(0.8))
+ LifeEvents.add(battle(0.7))
+ LifeEvents.add(devour_human(0.3))
+ LifeEvents.add(challenge_for_power(0.4))
+ LifeEvents.add(demon_slayer_promotion(0.7))
+ LifeEvents.add(become_demon_slayer(0.3))
+ LifeEvents.add(death_event_type())
sim.world.add_resource(DemonSlayerCorps())
sim.world.add_resource(DemonKingdom())
@@ -957,25 +979,34 @@ def setup(self, sim: Simulation, **kwargs) -> None:
# MAIN FUNCTION
########################################
+EXPORT_WORLD = False
+
+
def main():
sim = (
SimulationBuilder()
- .add_plugin(DefaultPlugin())
- .add_plugin(WeatherPlugin())
- .add_plugin(TalkOfTheTownPlugin())
+ .add_plugin(defaults.get_plugin())
+ .add_plugin(weather.get_plugin())
+ .add_plugin(talktown.get_plugin())
.add_plugin(DemonSlayerPlugin())
.build()
)
- sim.world.get_resource(LifeEventLog).subscribe(lambda e: print(str(e)))
-
st = time.time()
- sim.establish_setting()
+ sim.run_for(50)
elapsed_time = time.time() - st
- print(f"World Date: {sim.time.to_iso_str()}")
+ print(f"World Date: {sim.date.to_iso_str()}")
print("Execution time: ", elapsed_time, "seconds")
+ if EXPORT_WORLD:
+ output_path = f"{sim.seed}_{sim.town.name.replace(' ', '_')}.json"
+
+ with open(output_path, "w") as f:
+ data = NeighborlyJsonExporter().export(sim)
+ f.write(data)
+ print(f"Simulation data written to: '{output_path}'")
+
if __name__ == "__main__":
main()
diff --git a/samples/inheritance_system.py b/samples/inheritance_system.py
new file mode 100644
index 0000000..9252b9d
--- /dev/null
+++ b/samples/inheritance_system.py
@@ -0,0 +1,81 @@
+"""
+This is another attempt at improving the entity generation process. As I have gained
+a better understanding of how I should model characters, it has helped realize the
+problems with previous interfaces. For this iteration, we are breaking apart the pieces
+of characters into more individual components and placing probabilities on those
+components being present at spawn-time.
+"""
+from __future__ import annotations
+
+import random
+from typing import List, Optional, Set
+
+from neighborly.builtin.helpers import IInheritable, generate_child, inheritable
+from neighborly.core.archetypes import BaseCharacterArchetype
+from neighborly.core.ecs import Component, GameObject, World
+from neighborly.core.engine import NeighborlyEngine
+from neighborly.plugins.defaults import DefaultNameDataPlugin
+from neighborly.simulation import SimulationBuilder
+
+
+@inheritable(always_inherited=True)
+class FurColor(Component, IInheritable):
+
+ __slots__ = "values"
+
+ def __init__(self, colors: List[str]) -> None:
+ super().__init__()
+ self.values: Set[str] = set(colors)
+
+ def pprint(self) -> None:
+ print(f"{self.__class__.__name__}:\n" f"\tcolors: {self.values}")
+
+ @classmethod
+ def from_parents(cls, *components: FurColor) -> FurColor:
+ all_colors = set()
+ for parent in components:
+ for color in parent.values:
+ all_colors.add(color)
+
+ return FurColor(list(all_colors))
+
+
+class FuzzCharacterArchetype(BaseCharacterArchetype):
+ def create(self, world: World, **kwargs) -> GameObject:
+ gameobject = super().create(world, **kwargs)
+
+ fur_color = random.choice(
+ ["Red", "Green", "Blue", "Yellow", "Orange", "White", "Black", "Purple"]
+ )
+
+ gameobject.add_component(FurColor([fur_color]))
+
+ if world.get_resource(NeighborlyEngine).rng.random() < 0.3:
+ gameobject.add_component(HasHorns())
+
+ return gameobject
+
+
+@inheritable(inheritance_chance=(0.5, 0.7))
+class HasHorns(Component, IInheritable):
+ @classmethod
+ def from_parents(
+ cls, parent_a: Optional[Component], parent_b: Optional[Component]
+ ) -> Component:
+ return HasHorns()
+
+
+def main():
+ sim = SimulationBuilder().add_plugin(DefaultNameDataPlugin()).build()
+
+ c1 = FuzzCharacterArchetype().create(sim.world)
+ c2 = FuzzCharacterArchetype().create(sim.world)
+ c3 = generate_child(sim.world, c1, c2)
+
+ c1.pprint()
+ c2.pprint()
+ c3.pprint()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/john_wick.py b/samples/john_wick.py
index e04523a..247d08b 100644
--- a/samples/john_wick.py
+++ b/samples/john_wick.py
@@ -3,7 +3,7 @@
Author: Shi Johnson-Bey
John Wick is a movie franchise staring Keanu Reeves, in which
-his character, John Wick, is part of an underground society of
+his entity, John Wick, is part of an underground society of
assassins and hit-people. The member of the criminal underworld
follow specific social norms that regular civilians don't. All
favors come at the cost of coins, and no work-for-hire killing
@@ -19,36 +19,37 @@
from dataclasses import dataclass
from typing import List, Optional
-from neighborly.builtin.statuses import Adult
-from neighborly.core.archetypes import (
- BusinessArchetype,
- BusinessArchetypeLibrary,
- CharacterArchetype,
+from neighborly import (
+ Component,
+ GameObject,
+ Plugin,
+ SimDateTime,
+ Simulation,
+ SimulationBuilder,
+ World,
)
+from neighborly.builtin.components import Active, Adult, Deceased
+from neighborly.builtin.role_filters import friendship_lt
+from neighborly.core import query
+from neighborly.core.archetypes import BaseBusinessArchetype, BusinessArchetypes
+from neighborly.core.business import IBusinessType
from neighborly.core.character import GameCharacter
-from neighborly.core.ecs import Component, GameObject, World
from neighborly.core.engine import NeighborlyEngine
+from neighborly.core.event import Event, EventRole
from neighborly.core.life_event import (
- EventResult,
- EventRole,
- EventRoleType,
+ ILifeEvent,
LifeEvent,
- LifeEventLibrary,
- LifeEventLog,
- LifeEventType,
+ LifeEventRoleType,
+ LifeEvents,
+ PatternLifeEvent,
)
-from neighborly.core.relationship import RelationshipGraph
-from neighborly.core.residence import Resident
-from neighborly.core.time import SimDateTime
-from neighborly.plugins.default_plugin import DefaultPlugin
-from neighborly.plugins.talktown import TalkOfTheTownPlugin
-from neighborly.plugins.weather_plugin import WeatherPlugin
-from neighborly.simulation import Plugin, Simulation, SimulationBuilder
+from neighborly.exporter import NeighborlyJsonExporter
+from neighborly.plugins import defaults, talktown, weather
@dataclass
class Assassin(Component):
- """Assassin component to be attached to a character
+ """Assassin component to be attached to an entity
Assassins mark people who are part of the criminal
underworld and who may exchange coins for assassinations
@@ -59,24 +60,13 @@ class Assassin(Component):
kills: int = 0
-def assassin_character_archetype() -> CharacterArchetype:
- return CharacterArchetype(
- name="Assassin",
- lifespan=85,
- life_stages={
- "child": 0,
- "teen": 13,
- "young_adult": 18,
- "adult": 30,
- "elder": 65,
- },
- extra_components={Assassin: {}},
- )
+class Hotel(IBusinessType):
+ pass
-def continental_hotel() -> BusinessArchetype:
- return BusinessArchetype(
- name="The Continental Hotel",
+def continental_hotel() -> BaseBusinessArchetype:
+ return BaseBusinessArchetype(
+ name_format="The Continental Hotel",
max_instances=1,
min_population=40,
employee_types={
@@ -84,13 +74,16 @@ def continental_hotel() -> BusinessArchetype:
"Concierge": 1,
"Bartender": 2,
},
+ business_type=Hotel,
)
-def become_an_assassin(probability: float = 0.3) -> LifeEventType:
+def become_an_assassin(probability: float = 0.3) -> ILifeEvent:
"""Turns ordinary people into assassins"""
- def bind_character(world: World, event: LifeEvent) -> Optional[GameObject]:
+ def bind_character(
+ world: World, event: Event, candidate: Optional[None]
+ ) -> Optional[GameObject]:
candidates: List[GameObject] = []
for gid, (character, _) in world.get_components(GameCharacter, Adult):
if not character.gameobject.has_component(Assassin):
@@ -99,58 +92,27 @@ def bind_character(world: World, event: LifeEvent) -> Optional[GameObject]:
if candidates:
return world.get_resource(NeighborlyEngine).rng.choice(candidates)
- def execute(world: World, event: LifeEvent):
+ def execute(world: World, event: Event):
new_assassin = world.get_gameobject(event["Character"])
new_assassin.add_component(Assassin())
- return EventResult(generated_events=[event])
- return LifeEventType(
+ return LifeEvent(
name="BecomeAssassin",
probability=probability,
- execute_fn=execute,
- roles=[EventRoleType(name="Character", binder_fn=bind_character)],
+ effect=execute,
+ roles=[LifeEventRoleType(name="Character", binder_fn=bind_character)],
)
def hire_assassin_event(
- dislike_threshold: int, probability: float = 0.2
-) -> LifeEventType:
- def bind_client(world: World, event: LifeEvent) -> Optional[GameObject]:
- """Find someone who hates another character"""
- rel_graph = world.get_resource(RelationshipGraph)
- candidates: List[int] = []
- for gid, _ in world.get_components(Component, Resident):
- for relationship in rel_graph.get_relationships(gid):
- if relationship.friendship < dislike_threshold:
- candidates.append(gid)
-
- if candidates:
- return world.get_gameobject(
- world.get_resource(NeighborlyEngine).rng.choice(candidates)
- )
-
- def bind_target(world: World, event: LifeEvent) -> Optional[GameObject]:
- """Find someone that the client would want dead"""
- rel_graph = world.get_resource(RelationshipGraph)
- candidates: List[int] = []
- for relationship in rel_graph.get_relationships(event["Client"]):
- if relationship.friendship < dislike_threshold:
- candidate = world.get_gameobject(relationship.target)
- if candidate.has_component(Resident):
- candidates.append(relationship.target)
-
- if candidates:
- return world.get_gameobject(
- world.get_resource(NeighborlyEngine).rng.choice(candidates)
- )
-
- def execute_fn(world: World, event: LifeEvent):
- event_log = world.get_resource(LifeEventLog)
+ dislike_threshold: float = 0.3, probability: float = 0.2
+) -> ILifeEvent:
+ def execute_fn(world: World, event: Event):
date_time = world.get_resource(SimDateTime)
assassin = world.get_gameobject(event["Assassin"])
assassin.get_component(Assassin).kills += 1
- death_event = LifeEvent(
+ Event(
name="Death",
timestamp=date_time.to_iso_str(),
roles=[
@@ -158,52 +120,66 @@ def execute_fn(world: World, event: LifeEvent):
],
)
- world.get_gameobject(event["Target"]).archive()
+ world.get_gameobject(event["Target"]).add_component(Deceased())
- return EventResult(generated_events=[event, death_event])
-
- return LifeEventType(
+ return PatternLifeEvent(
name="HireAssassin",
probability=probability,
- roles=[
- EventRoleType(name="Client", components=[GameCharacter, Adult]),
- EventRoleType(name="Target", binder_fn=bind_target),
- EventRoleType(name="Assassin", components=[Assassin, Adult]),
- ],
- execute_fn=execute_fn,
+ effect=execute_fn,
+ pattern=query.Query(
+ find=("Client", "Target", "Assassin"),
+ clauses=[
+ query.where(query.has_components(GameCharacter, Active), "Client"),
+ query.where(query.has_components(GameCharacter, Active), "Target"),
+ query.where(query.has_components(Assassin, Active), "Assassin"),
+ query.where(friendship_lt(dislike_threshold), "Client", "Target"),
+ query.ne_(("Client", "Target")),
+ query.ne_(("Target", "Assassin")),
+ query.ne_(("Client", "Assassin")),
+ ],
+ ),
)
class JohnWickPlugin(Plugin):
def setup(self, sim: Simulation, **kwargs) -> None:
- LifeEventLibrary.add(hire_assassin_event(-30))
- LifeEventLibrary.add(become_an_assassin())
- BusinessArchetypeLibrary.add(continental_hotel())
+ LifeEvents.add(hire_assassin_event(-30))
+ LifeEvents.add(become_an_assassin())
+ BusinessArchetypes.add("The Continental Hotel", continental_hotel())
+
+
+EXPORT_WORLD = False
def main():
sim = (
SimulationBuilder(
seed=random.randint(0, 999999),
- world_gen_start=SimDateTime(year=1839, month=8, day=19),
- world_gen_end=SimDateTime(year=1979, month=8, day=19),
+ starting_date=SimDateTime(year=1990, month=0, day=0),
+ print_events=True,
)
- .add_plugin(DefaultPlugin())
- .add_plugin(WeatherPlugin())
- .add_plugin(TalkOfTheTownPlugin())
+ .add_plugin(defaults.get_plugin())
+ .add_plugin(weather.get_plugin())
+ .add_plugin(talktown.get_plugin())
.add_plugin(JohnWickPlugin())
.build()
)
- sim.world.get_resource(LifeEventLog).subscribe(lambda e: print(str(e)))
-
st = time.time()
- sim.establish_setting()
+ sim.run_for(40)
elapsed_time = time.time() - st
- print(f"World Date: {sim.time.to_iso_str()}")
+ print(f"World Date: {sim.date.to_iso_str()}")
print("Execution time: ", elapsed_time, "seconds")
+ if EXPORT_WORLD:
+ output_path = f"{sim.seed}_{sim.town.name.replace(' ', '_')}.json"
+
+ with open(output_path, "w") as f:
+ data = NeighborlyJsonExporter().export(sim)
+ f.write(data)
+ print(f"Simulation data written to: '{output_path}'")
+
if __name__ == "__main__":
main()
diff --git a/samples/notebooks/gui.py b/samples/notebooks/gui.py
deleted file mode 100644
index 3588752..0000000
--- a/samples/notebooks/gui.py
+++ /dev/null
@@ -1,140 +0,0 @@
-import threading
-import time
-from enum import Enum
-from typing import Optional, Protocol
-
-import ipywidgets as widgets
-from IPython.display import display
-
-from neighborly.simulation import Simulation
-
-
-class SimulationGUIWidget(Protocol):
- def update(self, *args, **kwargs) -> None:
- raise NotImplementedError()
-
-
-class GameCharacterWidget:
- """Displays information about a GameCharacter instance"""
-
- ...
-
-
-class LocationWidget:
- """Displays information about a Location instance"""
-
- ...
-
-
-class RoutineWidget:
- """Displays information about a Routine instance"""
-
- ...
-
-
-class RelationshipWidget:
- """Displays information about a Relationship instance"""
-
-
-class SimulationState(Enum):
- """Tracks if the simulation is running or paused"""
-
- PAUSED = 0
- STEPPING = 1
- RUNNING = 2
- STOPPED = 3
-
-
-def create_character_tab():
- """Create the GUI tab that"""
-
-
-class SimulationGUI:
- """Ipywidget GUI that displays information about a Neighborly Simulation instance"""
-
- def __init__(self, simulation: Simulation) -> None:
- self.simulation: Simulation = simulation
- self.simulation_thread = threading.Thread(target=self.run_simulation)
- self.simulation_running = True
- self.simulation_paused = True
- self.simulation_stepping = False
- self.active_widget: Optional[SimulationGUIWidget] = None
-
- # Create GUI
- self.date_text = widgets.Text(
- value=self.simulation.time.to_date_str(),
- disabled=True,
- layout=widgets.Layout(width="100%"),
- )
-
- self.play_button = widgets.Button(
- description="Play",
- disabled=False,
- )
- self.play_button.on_click(self.play_simulation)
-
- self.step_button = widgets.Button(
- description="Step",
- disabled=False,
- )
- self.step_button.on_click(self.step_simulation)
-
- self.pause_button = widgets.Button(
- description="Pause",
- disabled=True,
- )
- self.pause_button.on_click(self.pause_simulation)
-
- self.characters_tab = widgets.VBox(
- [widgets.HTML(value=f"