Agent Playground
- 1782 words
- Estimated Reading Time: 9 minutes
“Would you like to play a game?” That’s the question posed to Matthew Broderick’s character in the film War Games by a government super computer. My answer to that question is “no, I want to build a game”.
I enjoy playing video games but I enjoy creating software even more. However, I don’t want to build a game right now. I need to figure out a few things first.
I’m methodical in how I write software. When dealing with complex systems – like games – I try to identify the hard problems and prototype solutions. This helps avoid spending a large amount of time building the “real” solution and discovering that the fundamental approach isn’t viable.
I’m slowly working my way up to create a procedurally generated game. I know I want to create that game using an off the shelf game engine like Godot and will most likely do so using a combination of C++ and Godot’s scripting language. I also know that games are essentially complex systems. Rather, they are a system composed of smaller systems. Trying to figure out big concepts like terrain generation, path finding, or AI reasoning is hard. Trying to do it while juggling half a dozen other systems is, well, let’s just say it gets interesting.
I need a way to rapidly prototype specific problems without getting bogged down with the complexities of working with a large engine. This post is the first in a series about how I’ve created a rapid prototyping environment for working through the challenges of building a simulation heavy video game.
If you just want to look at the source code, it is here. Keep in mind, at the time I’m writing this, its an active project and by no means stable.
Designing a Prototyping Tool#
Gathering Requirements#
Step zero in my development process is gathering the initial requirements. For this project I use GitHub Issues and Projects to flush out use cases.
Guiding Principles and Values#
After defining the minimal requirements but before I start reaching for tools I list out the things that are most important for me for the project I’m about to build. I refer to these as my guiding principles and values.
Principles are looser than requirements. They serve as a sort of measuring stick to evaluate tools and approaches against.
- Graphics should be Minimal: Graphics tend to be a time suck. I don’t want to focus on making things look pretty in the prototypes.
- Creating Windows and Forms Should be Easy: I need to be able to create multiple windows and input options for the various prototypes.
- 0 to 60 in 4 Seconds: The time to get a new simulation off the ground should be minimal.
- Interactive Discovery: Experimenting with the code should be in realtime.
- Rich Expression: I need something that enables expressing complex ideas.
- Simple Debugging: Some of the things I want to prototype are complex algorithms and data structures. I need a visual debugger.
- Rapid Profiling: I need the ability to quickly determine how much memory data structures are using and how long functions are taking.
Sticky Decisions#
Armed with groomed requirements and guiding principles I proceed to select the tools.
I tend to think about development tools in two broad categories based on how hard they are to replace. This is my poor man’s pace layering model.
The first group of tools are sticky. They are foundational. Once adopted, other decisions are layered on top. As more time goes by it gets harder to replace them.
The second group of tools can be swapped out fairly easily. I liken this to a carpenter packing their physical toolbox for a day’s work. If a tool in the toolbox needs to be replaced its no big thing.
The Language#
Arguable the most important decision in any codebase is what language(s) to use. Programming languages are more than just a collection of syntax. They are opinionated in how problems are solved. They formalize a mental model for automating a computer. When selecting a language for a particular task, I try to align the language’s model to the problem at hand.
In this case, I need a language that makes exploration easy. Right now, discovery is more important than speed, correctness, or any language specific features.
Additionally, I need a language that I already know. This is not an opportunity to learn a new language. I just want to get a solution up and running so I can get on with the business of solving the problems I’m primarily interested in.
With all this in mind, I seriously considered using JavaScript with Node.js, Golang, Ruby, and Scala but ultimately landed on Python.
Why Python
Python is a productivity workhorse. I can accomplish a lot with minimal effort. The standard library is powerful and the language itself if flexible. The three things that pushed Python into the winner’s circle for this project is: the object model, the REPL, and type hints.
Python’s combination of duck typing, tuples, NamedTuples, classes, data objects, multiple inheritance, mixins, and decorators provide flexibility in modeling.
The REPL is great for exploring code in real-time.
Learning Scala’s type system almost broke my brain, but then a funny thing happened. I got hooked on rich type systems. Python’s type system is only applied during static analysis but I like how it works and it helps me think about abstract data types.
A bonus reason why I’m leveraging Python for the prototyping environment is that Python is fun to work in. If you’ve never used Python and you want to get a sense for the approach in problem solving try running this snippet in a shell.
# Start the python REPL
python
# Once the REPL starts run this line of python code.
import this
I won’t spoil the output. If you’re the type of developer that finds the output delightful, then Python may be a good fit for your creative efforts.
Eyes Wide Open
It’s not all rainbows and sunshine. No language is perfect and for all the strengths that Python brings to the table there are drawbacks that I have real concerns about.
I don’t particularly care for Python virtual environments. I’ve had a lot of practice making them work, but they’re a source of confusion. Related is dependency management. I’m not quite sure what it is about it, but pip just rubs me the wrong way.
The biggest drawback to using Python for this use case is that Python isn’t ideal for concurrency. I’m interested in exploring how to fully utilize a multi-core system but that may have to wait until I start building in Godot and leverage it’s threading model.
Dependency Management#
One decision leads to another. After selecting Python the immediate next step is to decide how to manage dependencies. Intertwined in that decision is the strategy for managing virtual environments. For these tasks I’ve selected poetry.
Poetry leverages a TOML file for declaring dependencies and environment configuration. Mine looks like this.
[tool.poetry]
name = "agents-playground"
version = "0.1.0"
description = "A prototyping environment."
authors = ["Samuel Holloway <sholloway@gmail.com>"]
[tool.poetry.scripts]
agents = "agents_playground.main:main"
[tool.poetry.dependencies]
python = "^3.9"
dearpygui = "^1.1.1"
[tool.poetry.dev-dependencies]
mypy = "^0.910"
pudb = "^2021.2.2"
py-spy = "^0.3.11"
pytest = "^6.2.5"
pytest-mock = "^3.6.1"
ptpython = "^3.0.20"
bpython = "^0.22.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
testpaths = [
"tests"
]
console_output_style = "count"
There is one nuance about how I use Poetry. The default behavior is that each Poetry project creates a virtual environment directory in the Poetry cache directory. I prefer to have the virtual environment be nested in the project directory. I feel that this makes the project more transparent, easier to understand, and helps configuring the IDE.
To configure Poetry to create the virtual environment in the local directory I run the following snippet before running the Poetry install command.
poetry config virtualenvs.in-project true --local
Project Automation#
While Poetry is good for managing dependencies and virtual environments it is not a general purpose automation tool. To automate the multitude of developer activities I use a simple Makefile.
One thing to note about using Poetry and other tools. When a tool has been installed with Poetry into the project’s virtual environment you want to run the tool with Poetry’s run command. This took me a while to figure out.
# Example 1: Launch the app
poetry run python -X dev ./agents_playground/main.py --log DEBUG
# Example 2: Run the tests
poetry run pytest
The run command launches a shell in the virtual environment (where all the dependencies are) and then runs your command in the context of that shell.
Testing#
Unit testing is a quality control best practice. For throw away prototypes, the benefit of writing tests is negligible and given the amount of time tests take to write arguably counter productive. That said, the framework that I’m building to enable creating prototypes needs to be reliable. I do think there is value in investing time in writing tests on the framework capabilities.
WIth that in mind, I’ve selected pytest for my unit test framework. I tend to use make heavy use of test doubles in unit tests. To that end, I’m currently using the pytest-mocks plugin. For measuring test coverage I use pytest-cov
Packing the Toolbox: Development Tools#
These are the tools in my prototyping toolbox. They can all be swapped out quickly.
Tool | Purpose | Comments |
---|---|---|
bpython | An enhanced REPL. Aids in exploration and debugging. | Evaluating against ptpython. |
logging | The out of the box Python logging module. Used for application logging. | |
mypy | Static type checker. | |
pudb | Interactive Python debugger for the terminal. | |
ptpython | An enhanced REPL. | Evaluating against bpython. |
py-spy | Python profiler that works with threads. | |
speedscope | A profiler view. | This needs to be installed via NPM. |
vscode | Open source IDE. |
Decision Log#
A non-technical tool that is highly useful IMHO is architectural decision logs. Over time, many decisions are made about a code base. The decision log file serves as a personal history for those decisions. I used to keep those types of things in the code comments but I’ve found that comments have a tendency to grow stale and can get lost when pruning code.
A decision log doesn’t need to be fancy. It just needs to capture when the decision was made, who made it, and what the decision is. I store mine as a markdown file in the projects docs directory. I use the convention defined by MADR.
Wrapping Up#
The Agent Playground is a small framework but full describing it requires more screen real estate than what fits in a single blog post. The next post will summarize the app’s overall architecture.
Until next time…
- Sam