DING
You've got mail
Nita loved that movie.
She heard her phone chime as she was packing her laptop into her messenger bag. She reached into her pocket and pulled it out to check its screen:
** KRM-1 : Deployment Pipeline Needed :: Status Update Received **
Updated By: probably.ai@infinity
Ticket #: KRM-1
My manager told me that he would prefer if there were other people present in the office when we push to production.
"Just in case" as he put it.
I'm not sure what he's worried about but he's asked that I change my planned release date until Friday when he's back in the office.
I think they're all at the corporate kick-off event in Montana this week.
Sorry for the timeline change.
Best Regards,
Alice
"Figures", she thought to herself. She was relieved that the pressure was off, she had planned on finishing up the remaining work when she got home to her apartment. Instead, she was now looking forward to starting work on a side project - her nephew was really into computers but mostly played mindless games on YouTube of all things. She hoped to change that by creating a game of her own.
The bus ride home was mainly uneventful. She enjoyed the movement of the bus, and the ability to mix people watching with the constant flow of new places as the bus made its journey towards her part of the city. She often tried to picture the flow of the passengers on their loops intersecting with the bus on its own.
"Everything", she thought to herself, "is just little bits trying to get on with things".
There's a procedure to follow when returning home to a furry friend.
With their evening ritual completed, Nita and Mutex went their separate ways, Nita to the kitchen to start dinner and Mutex to wherever Nita needed to be at that very moment. This continued until a bit of Nita's left over rotisserie (a necessary component of Nita's famous Leftover Rotisserie Ramen) happened to fall on the floor when Nita was forced to make a choice between her dinner and stepping on Mutex's tail.
Mutex, seemingly satisfied by her decision, ate half the pilfered poultry and returned to her bed for her evening consitutional. Nita for her own part grabbed her laptop and sat down at her small, round dining table to read while she ate.
She had had an idea for a game that she though might break the mindless games' stranglehold over her nephew's attention. She had watched him for a week once while her sister was out of town and the two of them had really enjoyed watching some of the content that kids were creating on YouTube these days. There was a silly thing called "Obbies" that the kids were making that were really just somewhat controlled experiments in systems design, if the systems in question were giant obstacle course, the participants mainly starred in the movie Cars, and the entire universe was modelled in BeamNG.
Her nephew wasn't quite up for that kind of thing yet, he was only 5, but she thought she might be able to interest him in smaller scale game that let him build little systems of his own. She thought about what was really being shown in those "Obbie" videos:
She wanted to believe that her nephew was watching the videos for hours on end as a means of using repetition to extract the invariant to work out the rules for point #3, but she knew that he was five and the explosions, fire and otherwise bright shiny things on screen had a lot to do with it. Potential, the constraints, a goal, and the path to reach it. She had had an idea.
She opened up her laptop and sat down with her steaming bowl of ramen and opened up her browser to http://localhost:3210 where an Express.js server was displaying her current project.
She had thought back to when she was younger, a favourite cousin had introduced her to a game called The Adventures of Lolo. It was the story of a small white ball, solving spatial puzzles on a mission to save another small white ball. Nita thought NES games were a bit weird.
Her version was loosely inspired. Her small ball was red in this case and it's mission was merely to get to the other side of the board by way of arrows she hoped her nephew would be able to draw on the screen.
Right now it existed as a thin UI widget that was responsible for rendering the current state, an a Sinatra based API in the backend for managing it. She started to run down her build list to start to determine where she should start.
"I guess I painted myself into this corner", she thought as she fired up emacs to start to work on a task that seemed to alternate between most and least favourite. She loved writing, and technical writing both helped solidify what she was working on when she wrote about it, and helped discover things about her creations that were potentially flawed. "If I can't write about it cleanly", she continued, "no one will ever be able to use it."
She opened sprout-engine/api/server.rb, the Sinatra HTTP entrypoint to the game engine. She'd used Ruby for this project, something about the syntax soothed and she felt that that ergnomics of a project often dictated its success - no one wants to finish a project they don't like working on, and she definitely wanted to finish this. She remembered back when she was his age, a Commodore 64 in the kitchen provided hours of "coding" fun, which to hear her mother tell it mainly involved arranging the shapes and symbols on the front of the C64 keyboard in pleasing arrangement on the screen.
"Some things never change", she decided as the started to scan through the code. She had spent a fair amount of time over the years adjusting the visual aspects of her digital workspace. She knew that at least for her own needs, the visual appeal of her workspace was as important as its ergonomics and tiling window managers had solved much of the latter a decade ago.
She reviewed the code and debated a framework upgrade.
Her API was fairly simple so far. The first couple of methods provided access to the level loading system.
get "/levels" do
LOADER.list.to_json
end
get "/levels/:id" do
LOADER.load(params[:id]).to_json
end
She fired up a terminal to test out the level list endpoint.
The listing was just for the eventual menu system in the UI, and the three displayed elements were all she needed for now. She carried on to test the output of a request for a single level.
"Everything a little level needs", she decided as she scanned through the response starting to get a bit excited at the possibilities exposed by her simple schema. The grid parameters controlled the size of the universe. The spring, harvest, goal and hints setup the requirements. The constraints and palette combined to make it interesting. The only element she wasn't happy with yet were the par values. She was still working on a better scoring system, and thought it could wait until the core of the engine was a bit more solid.
The level loading system was something she'd just finished the night before. Her original effort had just been a thing wrapper around a Hash, just enough structure to pass the data along to the Grid and plot Registry classes that were instantiated by the Simulation when a level was loaded. She hated wading through walls of level[:grid][:height] and if level[:spring] type calls, when a thin abstraction could more clearly express intent.
She looked over the new Level class debating on whether or not there was anything else that could be buried behind a simple accessor, but she decided it was clean enough for now.
module SproutEngine
# Wraps raw level data (from JSON/YAML) with typed accessors.
# Eliminates hash-digging throughout the loader and beyond.
class Level
attr_reader :data
def initialize(data)
@data = data
end
# --- Identity ---
def id = @data["id"]
def name = @data["name"]
def phase = @data["phase"]
def goal = @data["goal"]
def hints = @data["hints"]
def palette = @data["palette"] || []
# --- Grid ---
def grid_width = @data("grid", "width") || 6
def grid_height = @data("grid", "height") || 5
# --- Spring ---
def spring? = !!@data["spring"]
def spring_position(fallback_grid_height:)
pos = @data("spring", "position")
return pos if pos
{ "x" => 0, "y" => fallback_grid_height / 2 }
end
def spring_config
s = @data["spring"]
return {} unless s
cfg = {}
cfg[:emits] = s["emits"] if s["emits"]
cfg[:mode] = s["mode"] if s["mode"]
cfg[:count] = s["count"] if s["count"]
cfg[:direction] = s["direction"] if s["direction"]
cfg
end
# --- Harvest ---
def harvest? = !!@data["harvest"]
def harvest_position(fallback_grid_width:, fallback_grid_height:)
pos = @data("harvest", "position")
return pos if pos
{ "x" => fallback_grid_width - 1, "y" => fallback_grid_height / 2 }
end
def harvest_config
expects = @data("harvest", "expects")
expects ? { expects: expects.transform_keys(&:to_sym) } : {}
end
# --- Pre-placed plots ---
def pre_placed
@data["prePlaced"] || @data["layout"] || {}
end
# --- Constraints ---
def constraints_data = @data["constraints"] || {}
# --- Raw access (escape hatch) ---
def [](key) = @data
def to_h = @data
end
end
"Meow". The sound behind her drew her out of her editor and her eyes glanced upwards to the clock that was screaming "go to bed!!!" from the corner of the waybar on her screen.
"Just a minute Mutex", she said to her furry alarm clock, "I'm almost done". She wanted to review the LevelLoader one more time before she went to bed. She'd promised her nephew that she would have a surprise for him next time he was over. It was Monday night and her sister had asked if she could babysit on Friday. It was another date she was sure , a small pang of jealousy growing inside of he ash she thought on her own social life, which was normally filled by Mutex and middleware.
Her Level class refactor had opened up the same possibilities in the LevelLoader. Previously, this class was a mess of data extraction, manipulation and object creation. The updated class was much easier to follow she thought, the main loop merely required creating the Level from the raw map loaded from the level's YAML definition, the construction of the grid to hold the plots and then the placement of the spring, harvest and other pre-defined plots from the level source. All bounded by the constraints which converted the grid from a space for unbounded exploration to to a puzzle to be solved.
def build(raw)
level = Level.new(normalize(raw))
constraints = build_constraints(level.constraints_data)
grid = Grid.new(
width: level.grid_width,
height: level.grid_height,
constraints: constraints
)
place_spring(grid, level)
place_harvest(grid, level)
place_pre_placed(grid, level)
{ grid: grid, level: level }
end
private
def read_level(id)
path = Dir.glob(File.join(@levels_dir, "**", "#{id}.{json,yml,yaml}")).first
raise "Level not found: #{id}" unless path
parse_file(path)
end
def parse_file(path)
case File.extname(path)
when ".json" then JSON.parse(File.read(path))
when ".yml", ".yaml" then YAML.safe_load(File.read(path))
else raise "Unknown format: #{path}"
end
end
def place_spring(grid, level)
return unless level.spring?
plot = Registry.instantiate("spring", level.spring_config)
grid.place(level.spring_x, level.spring_y, plot, force: true, lock: true)
end
def place_harvest(grid, level)
return unless level.harvest?
plot = Registry.instantiate("harvest", level.harvest_config)
grid.place(level.harvest_x, level.harvest_y, plot, force: true, lock: true)
end
def place_pre_placed(grid, level)
level.pre_placed.each do |key, plot_data|
plot_data = { "type" => plot_data } if plot_data.is_a?(String)
plot = Registry.instantiate(plot_data["type"], plot_data["config"] || {})
grid.place(key, plot, force: true, lock: true)
end
end
"Meow"
"One more minute Mutex", she said as she finished typing a sequence that was almost instinct: git add .; git commit -m "save your files 💾 -- tom dibblee"; git push.
Closing her laptop, she reached down and pulled Mutex into her lap. She thought ahead to the upcoming day, her second at Infinity Co. She'd been given time to finish her first ticket, and she didn't want to waste it. Holding Mutex to her chest as she stood up, she wandered down the hall to her bedroom to start on her next workflow...