Skip to content

Seedfolk Catch-Up: The Farm Is Starting to Feel Like a Place

A catch-up on the Seedfolk project: domain contracts, crop rendering, the TypeScript web-game stack, testing coverage, deployment plumbing, and the small details that make the farm feel alive.

I have been deep in Seedfolk lately, which is my cozy isometric farming game about tending a small patch of land, making useful choices, and slowly persuading a pile of TypeScript, tiles, sprites, and rules to feel like an actual place.

That is the cheerful version.

The more honest version is that I have been trying to make a tiny farm stop feeling like a tech demo with excellent posture.

Seedfolk is still early. There are plenty of rough edges, missing systems, half-built ideas, and future problems waiting politely in the hallway. But the project has crossed a useful little threshold: the pieces are starting to talk to each other in the same language.

The farm loop has shape.
The domain rules are less blurry.
The crops are starting to move with some character.
The web stack is becoming a real game stack, not just a browser wearing a straw hat.

Seedfolk gameplay screenshot showing a farmer among dense isometric crop beds. Public social preview image for the Seedfolk catch-up post.

The current feeling I am chasing: a small field that reads as authored, playable, and alive without the rules underneath becoming soup.

The project is becoming Seedfolk, not just an iso farm prototype

Section titled “The project is becoming Seedfolk, not just an iso farm prototype”

The earliest version of this project was mostly about proving that the basic farming loop could work.

Can I render an isometric map?
Can the player click a tile?
Can plots be placed, tilled, watered, planted, and harvested?
Can React and Phaser share enough state without turning the app into a custody dispute?

That phase was useful. It got the loop moving.

But prototypes can only carry a project so far. At some point, the question changes from “can this work?” to “what is this becoming?”

That is where Seedfolk has started to come into focus.

It is not just an isometric farm grid anymore. The direction is more specific now: a cozy farming game with readable rules, gentle feedback, practical craft, and little systems that help the world feel cared for.

That shift matters because it changes how I make decisions. The goal is not just to add features. The goal is to make the farm feel like a place where the player understands what happened, what changed, and what they might want to do next.

Concept art of a Seedfolk farm scene surrounded by crop sheets, sketches, tools, and a small outdoor workbench.

Concept art for the direction of the project: the farm as both a playable place and a little workshop of authored systems.

One of the biggest bits of progress has not been visual at all.

I have been thinking through the domain contract for Seedfolk: the shared rules, words, state shapes, commands, events, and invariants that the rest of the game needs to agree on.

That sounds very serious for a game where crops wiggle.

But the crops are exactly why it matters.

A farming game gets complicated quickly because soft-looking interactions are usually hiding hard rules. Seeds become crops. Crops become harvests. Soil can be rough, prepared, watered, fertilized, planted, ripe, spent, or waiting on a timer. The UI, game scene, save data, tests, and future server code all need to agree on what those words mean.

If they do not, the project slowly becomes a polite argument between layers.

So I have been pulling those meanings into clearer contracts:

  • What can a plot be?
  • What does it mean for soil to be prepared?
  • Which actions are commands?
  • Which changes become events?
  • What data needs to survive a save/load cycle?
  • Which layer owns the truth when the player plants, waters, waits, or harvests?

The aim is not architecture theatre. I do not need a marble temple for carrots.

The aim is a small, testable agreement that keeps the game honest as it grows.

Here is the kind of TypeScript shape I mean. This is a simplified version of the direction: commands come in, events come out, and the compiler gets to be the annoying friend who notices when I forgot that potatoes exist.

type PlotId = string & { readonly brand: "PlotId" };
type SeedId = "carrot" | "potato" | "strawberry" | "corn";
type PlotState =
| { kind: "empty"; id: PlotId }
| { kind: "rough"; id: PlotId }
| { kind: "prepared"; id: PlotId; watered: boolean }
| {
kind: "planted";
id: PlotId;
seedId: SeedId;
plantedAt: number;
watered: boolean;
}
| { kind: "ripe"; id: PlotId; seedId: SeedId; readyAt: number }
| { kind: "spent"; id: PlotId; seedId: SeedId };
type FarmCommand =
| { type: "TillPlot"; plotId: PlotId }
| { type: "WaterPlot"; plotId: PlotId }
| { type: "PlantCrop"; plotId: PlotId; seedId: SeedId; now: number }
| { type: "HarvestCrop"; plotId: PlotId; now: number };
type FarmEvent =
| { type: "PlotTilled"; plotId: PlotId }
| { type: "PlotWatered"; plotId: PlotId }
| { type: "CropPlanted"; plotId: PlotId; seedId: SeedId; plantedAt: number }
| { type: "CropHarvested"; plotId: PlotId; seedId: SeedId; harvestedAt: number };

The useful bit is not that these names are fancy. They are not. The useful bit is that the game cannot quietly pretend a rough plot is plantable, or that a harvested crop is still ready, without having to argue with the type system first.

The command handler can then stay deliberately small:

function decideFarmEvent(
plot: PlotState,
command: FarmCommand,
): FarmEvent | null {
switch (command.type) {
case "TillPlot":
return plot.kind === "rough"
? { type: "PlotTilled", plotId: plot.id }
: null;
case "WaterPlot":
return plot.kind === "prepared" || plot.kind === "planted"
? { type: "PlotWatered", plotId: plot.id }
: null;
case "PlantCrop":
return plot.kind === "prepared"
? {
type: "CropPlanted",
plotId: plot.id,
seedId: command.seedId,
plantedAt: command.now,
}
: null;
case "HarvestCrop":
return plot.kind === "ripe"
? {
type: "CropHarvested",
plotId: plot.id,
seedId: plot.seedId,
harvestedAt: command.now,
}
: null;
default:
return assertNever(command);
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled farm command: ${JSON.stringify(value)}`);
}

That assertNever is one of my favourite tiny TypeScript tricks. Add a new command and forget to handle it, and TypeScript complains before the player discovers a new category of haunted turnip.

I wrote more about that here: what the hell is a domain contract?

The most visible progress has been the crop rendering pass.

The first version was plain: one plot, one crop sprite, swap the sprite when the crop grows.

It worked. It also felt very much like the spreadsheet had successfully changed outfits.

So I started pushing the presentation layer further without making the gameplay model more complicated.

One gameplay crop can now render as a small visual cluster. The crop is still one crop as far as the rules are concerned, but visually it can show two, three, or more sprites. That immediately makes a field feel denser and less like a collection of isolated museum plants.

Those extra sprites use deterministic variation, so each planting can have a slightly different arrangement without jittering around every frame. Randomness is lovely, but only when it has agreed to stop moving the furniture while you are looking at it.

The placement also respects the isometric tile shape. Instead of spreading duplicates in a normal circle, the renderer projects them through an ellipse that fits the tile footprint. That keeps crops rooted in the soil rather than floating nearby with suspicious intentions.

Then came the motion:

  • growth anticipation before a stage change
  • squash-and-stretch pops from the plant base
  • subtle harvest-ready pulses
  • shared wind sway across the field
  • blended animation layers so effects do not fight each other

None of that changes the actual farming rules. A crop still grows on a timer. A plot still owns one crop. Harvesting still produces the same result.

But the feeling changes completely.

The field starts to read less like a grid of state machines and more like a little patch of land that is beginning to breathe.

I wrote the deeper crop-rendering breakdown here: From Static Crop Tiles to Little Living Fields

The stack is still web technology, and I like that

Section titled “The stack is still web technology, and I like that”

Seedfolk is being built with web technologies all the way down:

  • TypeScript for the game rules, state shapes, renderer glue, and contracts
  • strict TypeScript checks so vague assumptions get caught before they become farm lore
  • React for the surrounding app shell and interface surfaces
  • Phaser for the 2D game scene
  • Tiled maps for authored world layout
  • shared state boundaries between UI, scene, and domain logic
  • automated tests around the parts of the game that need to stay boringly correct
  • GitHub Actions for typecheck, tests, build validation, and deployment handoff
  • Cloudflare deployment plumbing so builds can get online quickly instead of living forever on my machine like a tiny digital houseplant
  • Slack webhook notifications so deploys and project signals show up where I will actually see them

Concept art of a browser-game development workshop with crop beds, tile maps, test panels, deployment crates, and tool boards arranged around a small farm diorama.

The stack in spirit: web tools, authored content, test coverage, and deployment all orbiting the same small playable world.

I like this stack because it makes the game feel approachable as software.

The browser gives fast iteration. Strict TypeScript gives shape to the rules and complains early, which is rude but helpful. React is useful for menus, overlays, debug tools, inventory surfaces, and all the bits around the game. Phaser handles the scene, input, sprites, cameras, and animation. Tiled keeps map authoring out of hard-coded coordinate soup.

The part that feels especially good right now is that the stack is starting to run smoothly as a whole. A change can move from local work to typed code, tested behavior, GitHub Actions validation, Cloudflare deployment, and Slack notification without the process feeling like a ceremonial rain dance around a build server.

The trick is keeping the boundaries honest.

React should not secretly become the crop simulation. Phaser should not invent private farming rules because it needed to show a hover state. The domain code should not know about every visual flourish. Tests should protect the rules without freezing the presentation layer in amber.

That is the ongoing discipline of the project.

Cozy games still need sharp edges in the code. The player should get softness. The codebase can keep the clipboard.

Testing is becoming part of the design work

Section titled “Testing is becoming part of the design work”

I have also been thinking more deliberately about test coverage.

Not everything in a game needs the same kind of test. I do not need to unit test whether a crop wiggle feels charming. That is a job for eyes, taste, and occasionally making a face at the screen.

But I do want tests around the rules that must not drift:

  • command preconditions
  • crop lifecycle transitions
  • save/load shapes
  • visual placement guardrails
  • deterministic variation
  • domain contracts between layers

The crop renderer is a good example. The exact animation can stay expressive, but the placement contract can still be tested. If a visual crop instance is supposed to stay inside the isometric tile footprint, that should not be a matter of hope and vibes.

Testing, in this project, is less about pretending every pixel can be proven correct and more about protecting the agreements that let the creative parts move safely.

For example, the tests do not need to know whether the crop sway has the perfect amount of charm. But they can know that visual yield is stable, deterministic, and contained inside the tile footprint:

it("keeps visual crop instances inside the isometric footprint", () => {
const placements = createCropPlacements({
cropInstanceId: "plot-12:corn",
cropTypeId: "corn",
visualYield: 5,
spread: 0.82,
});
expect(placements).toHaveLength(5);
for (const placement of placements) {
expect(isInsideIsoDiamond(placement.offsetX, placement.offsetY)).toBe(true);
}
});
it("keeps crop variation stable for the same planting", () => {
const first = createCropPlacements(seedfolkCornFixture);
const second = createCropPlacements(seedfolkCornFixture);
expect(second).toEqual(first);
});

That is the sweet spot I am aiming for: test the promises, leave room for taste.

That is also making deployment less dramatic. The stricter the types are, and the more the important rules are covered by tests, the less a deploy feels like pushing a mysterious box into the ocean and hoping it floats. GitHub Actions can run the boring checks, Cloudflare can take the build, Slack can tell me what happened, and I can spend more energy on the actual game.

Deployment matters earlier than it feels like it should

Section titled “Deployment matters earlier than it feels like it should”

Another quiet bit of progress is deployment.

I want Seedfolk to be easy to share early, even when it is not finished. That means the project needs normal web-app discipline: repeatable builds, static assets in the right places, deployable outputs, and enough confidence that a change does not only work because my local machine happens to be in a good mood.

The current shape is starting to feel like a proper rhythm:

  • write the change locally
  • let strict TypeScript catch the silly mistakes
  • run the tests that protect the domain rules
  • push through GitHub Actions
  • deploy through Cloudflare
  • get notified through Slack
  • open the build and actually look at the thing like a person with eyes

That loop is simple, but it changes the mood of the project. It means progress is not just “I made something locally.” Progress becomes a repeatable path from idea to checked build to shared build.

This matters because games need feedback.

It is very easy to keep a prototype private until it feels perfect. Unfortunately, perfect is a very comfortable hiding place.

Getting builds online earlier forces better habits. It reveals broken asset paths, weird video formats, missing metadata, layout issues, and all the little things that are invisible until someone else tries to load the work from a real URL.

It also keeps the project honest. If I can share a build note, a screenshot, or a playable slice, I can see what is actually there instead of only what exists in my head.

My head, for the record, ships features at an alarming rate. The repo is more measured.

The interesting part is the relationship between systems

Section titled “The interesting part is the relationship between systems”

The thing I keep coming back to is that Seedfolk is not one big system. It is a set of relationships.

The domain rules need to be clear enough that the UI and scene can trust them.

The renderer needs enough visual personality that the farm feels alive, but not so much that the player loses readability.

The art pipeline needs to let tiles carry presentation intent, so new crops can feel different without custom code every time.

The test suite needs to protect the parts that must stay stable, while leaving room for the parts that need taste, iteration, and play.

The deployment pipeline needs to make sharing normal instead of ceremonial.

That is where the project feels most alive to me right now. Not in one feature by itself, but in the way the pieces are starting to support each other.

A rhythm is being established: make the thing, check the thing, ship the thing, learn from the thing. Very advanced software methodology. Possibly too powerful. Please use responsibly.

The next stretch is about turning these foundations into more player-facing texture.

Concept art of a Seedfolk farm path leading from a finished crop patch toward future farm areas, with small markers, seed crates, and half-built plots.

The road ahead: more player-facing texture, more readable feedback, and a farm that keeps opening up without losing its centre.

I want to keep improving:

  • the authoring workflow for crop data and tile properties
  • the feedback around actions like watering, planting, harvesting, and clearing
  • the shape of save/load behavior
  • the contract between game state and UI state
  • the debug tools that make invisible rules inspectable
  • the balance between subtle animation and readable information
  • the deployment flow for sharing playable slices

There is still a lot to do.

But the farm is starting to feel like something.

That is a small sentence, but it is a big milestone. It means the project is no longer just a list of systems. It is beginning to have a mood, a set of rules, a technical spine, and a reason to keep walking around in it.

Which is good.

Because I have crops to persuade, tests to write, deployments to break responsibly, and a small field that is gradually becoming a place.