Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
157280 stories
·
33 followers

Coding assistants - my evolving use

1 Share

I’ve been using coding assistants pretty much since they arrived on the scene and I’ve written about their impact on me personally previously. And while I find them incredibly useful I’ve noticed I’m starting to write more code directly myself again.

The reason is simple: if you’re doing something I would class as interesting they are never able to quite give you what you want (or sometimes give you it at all). I think there’s a couple of reasons for this.

Examples of interesting problems I’ve worked on recently are an AI for a 4X game (check out Annhexation) and procedural generation of scenarios for a space combat game (watch this space!). The correctness of both involves a good amount of what I can only call “feel” and so it can be difficult to set up a verification feedback loop. You can set up feedback loops over mechanical parts but not the whole and the fundamental challenge: “is this interesting to play”.

Another example is a neurosymbolic construction kit. The coding assistant was more useful here, and it was great at implementing a distributed processor for a directed acyclic graph, but only after I’d set up a very, very, clear architecture for it to fill in the blanks of. And even then its tendency seemed to remain as what I’d call steering towards the incorrect.

The other issue I’ve found is the tendency for AI to steer towards “quick fixes” and “papering over root problems” providing you with a solution to a bug or problem but only at a surface level.

What I find with these kinds of problems is that I spend a lot of time “bashing things with a hammer” while losing intimacy with the resulting code and I’m not convinced its saved any time. It feels like its faster. But I don’t think it is.

And yes, this was still the case with the much vaunted Fable, before it got turned off!

In any case the result is I’m getting a lot more selective about when I use it.

I still find them fantastically useful but the list of caveats I would place around that is growing all the time.

Building a believable computer opponent for a 4X strategy game is one of those problems that turns out to be bottomless. I’d use the cliche it looks simple from the outside… but I don’t think thats true, I thought this would be a tough nut from the outset. I’ve built a chess playing engine before and that was far simpler to get a strong opponent - though it helps that that is such a well understood and documented problem. The player wants an opponent that explores, expands, exploits and exterminates with apparent intent — one that musters an army over several turns, marches it across a continent, lands it on your shore and takes your city, all while you watched it coming and couldn’t quite stop it. They do not want an opponent that teleports units, reads your mind, or sits inert in its starting cities until you wander into range.

This post is a tour through the Annhexation AI — explaining how it makes decisions, what it remembers between turns, and how the same core machinery produces eight distinct civilizations and four difficulty levels. Annhexation isn’t open source, so rather than quote the implementation I’ll describe the design and illustrate the interesting bits with pseudocode.

I should note that the AI is still under development but after a lot of bashing with a hammer its feeling in a pretty decent place.

The core idea

The single most important design decision in the Annhexation AI is that strategy, planning and execution are decoupled. These are three layers that are seperated on purpose and an AI turn flows through three layers:

  1. Strategic layerWhat am I trying to achieve? Peace or war, expansion or consolidation, science race or turtle with wonders. This layer thinks in goals that last tens of turns.
  2. Operational layerHow do I achieve my strategic goals? Resource allocation, unit quotas, attack plans, city production, research direction. This is the planning.
  3. Tactical / execution layerWhat do I actually do this turn? Move this unit here, attack that stack, fortify this garrison, embark these troops. Turn to turn execution.

The payoff of this separation is, hopefully, coherence over time. A greedy turn-by-turn AI looks twitchy: it builds an army, gets distracted, disbands it, builds another. By contrast, an Annhexation AI that adopts a militaryPush goal will hold that goal for twenty-plus turns, funnelling production, research and unit movement toward a single objective until the city falls, the campaign demonstrably fails, or something seismic interrupts it. Strategy should be sticky while execution is flexible.

A complete turn runs as an ordered sequence of discrete phases — from threat assessment and diplomacy through combat, movement, production and fortification:

function runTurn(player, world, aiState):
    detectEvents(aiState, world)          # diff against last turn → fire interrupts
    aiState.goals = evaluateStrategy(player, world, aiState)
    plans = buildOperationalPlans(aiState.goals, player, world)
    executeTactics(plans, player, world)  # the phase sequence (see below)
    aiState.snapshot = snapshot(world)     # remember this turn for next time
    return aiState

Strategy

At the heart of the strategic layer is a prioritized goal stack. Each turn the AI either keeps its current goals or re-evaluates them, and the menu of things it can want is rich:

  • earlyExpand — plant N cities before consolidating
  • earlyRush — exploit the opening with an aggressive early attack
  • infrastructureConsolidation — buildings, population, growth
  • militaryPush — sustained warfare against a chosen player
  • defensiveWar / counterattack — react to aggression, retake what was lost
  • navalInvasion — assault a distant landmass
  • wonderRace, scienceVictoryPush, scoreOptimisation — the peaceful victory paths
  • raidWar, asymmetricWar — economic harassment instead of conquest
  • warPreparation, nuclearFirstStrike, recovery — the situational specials

Goals don’t fire on rigid rules rather they’re scored against each other and the highest-utility ones win. The scoring blends several signals:

  • Proximity. How far is the nearest enemy city? Distant neighbours (≥14 hexes) push the AI toward peaceful expansion; close ones (≤4 hexes) pull it toward military goals. Geography shapes temperament.
  • Force balance. Am I winning the simulated battles? Losing exchanges suppress military goals and inflate defensive ones.
  • Catch-up. Falling behind on city count inflates expansion scores so a boxed-in AI tries harder to grow.
  • Opportunity. Multipliers derived from how every met rival is currently behaving (more on that below).

Every score is then multiplied by a personality weight. Roughly:

function scoreGoals(player, world, personality):
    scores = {}
    for goal in CANDIDATE_GOALS:
        base = goal.baseValue(player, world)
        world_factors = proximity × forceBalance × catchUp × opportunity
        scores[goal] = base × world_factors × personality.weightFor(goal)
    return sortDescending(scores)

# e.g. early-expand ≈ base × siteRatio × proximityAdj × catchUp × personality.expansion

Two of those terms are about the world; one is about who this civ is. That’s how the same evaluation function produces a cautious turtle and a rampaging horde.

The top goal (priority 0) drives the turn. Secondary goals queue behind it, ready to take over the moment an interrupt fires.

Reading the opponents

A 4X AI that only looks at its own empire plays in a vacuum. Annhexation’s AI explicitly models every player it has met before deciding who to fight.

The AI profiles each known rival across roughly eleven dimensions, each normalised to [0, 1]:

  • militarisation, development, expansionism, techPace
  • exposure and coastalExposure (undefended or weakly-garrisoned cities)
  • borderTension and aggression (forces massed near our borders, active wars)
  • wonderFocus, scienceFocus, and the all-important isRunawayLeader flag

It also tracks trends — rising, flat or falling over the last five turns — so the AI reacts to a rival who is accelerating, not just one who is currently strong. Those snapshots are kept in persistent state so trend detection survives across turns.

A second pass turns those profiles into a war-target ranking. For each rival it weighs:

  • Aggression affinity — does attacking this player suit my personality?
  • Strength — can I actually win?
  • Accessibility — can I even reach them?
  • Stability — are they conveniently distracted by another war?
function scoreWarTargets(rivals, me, personality):
    for r in rivals:
        affinity      = personality.aggression × r.borderTension
        winnable      = clamp(myStrength / r.militarisation)
        reachable     = 1 / (1 + travelCost(me, r))
        distracted    = r.aggression_elsewhere
        r.score       = affinity × winnable × reachable × (1 + distracted)
    return sortDescending(rivals)

The winner of that scoring becomes the target of a militaryPush, and the magnitude feeds back as an opportunity multiplier into goal evaluation. An exposed, accessible, distracted neighbour is a temptation the AI is built to notice and exploit.

Personalities and doctrine

Personality in Annhexation isn’t a single “aggression” slider — it’s a vector of about twenty weights (military production, attack appetite, expansion, wonder-building, research, naval production, raid preference, plus early-game tuning like second-city urgency and first-build preference).

On top of that sits the doctrine system — eight civ-specific playbooks that override those weights and the AI’s unit-composition preferences:

Civ Doctrine Signature
Mongolia HORSE_RUSH +50% military production, +50% attack, double raid preference, cavalry-heavy armies
Aztecs WARRIOR_RUSH +40% military & attack, −20% expansion, melee-heavy early aggression
Russia EXPAND_WIDE +40% expansion, +30% garrison commitment
Rome INFRA_FIRST +40% infrastructure, +30% expansion
France WAR_FOR_SCIENCE +40% research, +30% science-victory focus
Greece STRATEGIST balanced militarisation across all domains
Egypt TURTLE_WONDERS +50% wonders & culture, −20% military
England COASTAL_ONLY +40% naval, +50% coastal-site preference, harbour priority

Because the doctrine only modulates shared machinery, Egypt and Mongolia run the identical goal-evaluation and combat code — they simply weight it toward completely different ends. Mongolia drowns you in cavalry; Egypt hides behind wonders and culture; England fights for the coastline.

Combined with unique per civ units this gives each civ a distinctive personality.

Operational planning: from intent to orders

Once a goal is chosen, the operational layer turns intent into concrete plans.

Unit quotas compute empire-wide demand for each unit class — settlers, workers, garrison, field army, reserve, naval, raiders — each scaled by goals, threat levels, personality and difficulty. During a militaryPush against a walled city, for instance, the garrison quota rises with threat level, melee demand jumps, and siege units become mandatory — you cannot crack walls without them, and the AI knows it.

Unit composition picks the melee/ranged/siege/mounted ratio for an army. Against an unwalled city it loads up on ranged units (free damage); against walls it must bring siege. Doctrine tilts the mix, and resource gating caps it — no horses means no cavalry, no iron means no siege, full stop:

function targetComposition(target, doctrine, resources):
    if target.walled: mix = {melee: 0.4, siege: 0.4, ranged: 0.2}
    else:             mix = {melee: 0.4, ranged: 0.5, mounted: 0.1}
    mix = applyDoctrineBias(mix, doctrine)   # HORSE_RUSH → more mounted, etc.
    if not resources.horses: mix.mounted = 0
    if not resources.iron:   mix.siege   = 0
    return normalise(mix)

Attack plans are first-class, multi-turn objects with an explicit lifecycle:

mustering → gathering → advancing → besieging → assaulting
                ↘ (naval) awaitingTransport → embarking → sailing → landing ↗

Target selection scores enemy cities by proximity (−5 per hex of distance), with bonuses for being unwalled (+15), being a capital (+10), and sitting near iron or horses the AI needs (a big multiplier gated on personality and urgency). It goes for the weakest reachable target first — and it commits.

City production is a distributed priority queue: high-output cities feed global military needs first, low-output cities backfill settlers and workers. The priority cascade runs upgrades → settlers → garrison → military → naval → workers/roads → buildings → wonders, gated by the active goal.

Research follows the goal: an expanding AI beelines the wheel and animal husbandry. A science-victory AI walks a hardcoded path toward rocketry while a warring AI weights military techs. It searches the prerequisite tree but abandons paths longer than three techs — no hundred-turn detours. In theory!

Worker management plans and caches road routes between cities and strategic resources, invalidating them when borders flip. Bottleneck detection explicitly diagnoses why military modernisation is stalled — waiting on a tech, lacking road access to iron, missing currency for trade — and escalates urgency the longer the bottleneck persists.

Tactical execution: a turn, phase by phase

When the planning is done, the AI executes the turn as an ordered sequence of phases. Roughly:

Event detection & city-loss response      (compare against last turn's snapshot)
Emergency garrison fill                    (enemy standing on a city tile)
Unit upgrades & recalls
Retreats                                   (pull damaged units that aren't committed)
Combat                                     (city defence first, then general)
Naval invasion lifecycle                   (drive the beachhead state machines)
Settler escorts & transport convergence
Army movement                              (via the movement planner)
Build orders                               (worker tasks, roads)
Diplomacy                                  (trade, war declarations)
City Defence Commander                     (per-city garrison assignment)
Government & tech completion
Fortification & hidden-unit setup

A few pieces deserve a closer look.

  • Combat simulation estimates each attack before committing: attack strength (scaled by a difficulty-dependent effectiveness multiplier) versus defence strength (garrison, terrain and fortify bonuses), turned into a win probability and an expected HP loss.
  • On higher difficulties, combat phasing models ranged-fires-first, melee-counterattacks, melee-finishes — so the AI understands the value of softening a target with archers before the melee goes in. On Easy, that phasing is switched off, dumbing the AI down on purpose.
function shouldAttack(attacker, defender, difficulty):
    atk = attacker.strength × difficulty.combatEffectiveness
    def = defender.strength × terrainBonus × fortifyBonus × garrisonBonus
    winProb = clamp(0.5 + (atk - def) × 0.1, 0, 1)
    return winProb  attacker.riskTolerance
  • Movement shares a context across all units so two units never plan into the same tile (no accidental stacking). It uses strategic pathing with an A* fallback, plus anti-oscillation rules — it won’t step back onto a tile it occupied in the last couple of turns unless it’s hurt or there’s an enemy adjacent — which kills the classic “AI unit jitters back and forth forever” bug.

  • Retreat pulls units below an HP threshold (50% on Easy, down to 20% on Deity) or when outnumbered 2:1 nearby — but garrisons never retreat, assault-committed units only break below 15%, and loaded transports never run. Commitment is respected.

  • The City Defence Commander automates each threatened city’s garrison through its own little state machine — reinforcing → defending → critical → secure — tracking the local force balance and issuing movement orders to defenders. Cities defend themselves intelligently without the strategic layer micromanaging every hex.

Memory: what the AI carries between turns

None of this multi-turn coherence works without persistence. The AI’s state object is serialised between turns and carries, among other things:

  • the goal stack and all live attack plans with their lifecycle state
  • unit assignments — which unit is a garrison, a field-army member, a raider, a scout — and what it’s committed to
  • the border model, classifying cities as capital / frontier / critical / interior and tracking tension per neighbour
  • posture snapshots (five turns of history), grievances, pending attacks and city-defence commands
  • cached road routes, resource-access graphs, and settler journey state
  • the IDs of cities we’ve lost, so a counterattack knows what to retake
  • a full snapshot of last turn for event detection

That last point drives the AI’s reactivity. Each turn it diffs the current world against last turn’s snapshot to spot captured or lost cities, fresh war declarations, lost wonders, completed techs, detected nukes, and pillaged tiles. Any of these can fire an interrupt that pre-empts the current goal — lose a city and the AI drops what it was doing to respond; lose your capital and counterattack jumps the stack.

function detectEvents(aiState, world):
    prev = aiState.snapshot
    for change in diff(prev, world):
        if change is CITY_LOST:        raise Interrupt(counterattack, change.city)
        if change is WAR_DECLARED:     raise Interrupt(defensiveWar, change.by)
        if change is NUKE_DETECTED:    raise Interrupt(recovery, change.where)
        ...                            # wonders lost, tiles pillaged, techs done

Difficulty: honest tuning plus a few sanctioned cheats

Difficulty in Annhexation is partly competence and partly bonus — and the line between them is deliberate.

Easy Normal Hard Deity
Production / Research / Gold 0.8× 1.0× 1.15× / 1.1× / 1.1× 1.3× / 1.25× / 1.2×
Combat phasing & focus fire off on on on
Will retreat no yes yes yes
Combat effectiveness 0.95× 1.0× 1.08× 1.15×
Decision accuracy ~60% 100% 100% 100%
Strategy re-evaluation every 20 turns 12 10 8

So an Easy AI isn’t just weaker — it genuinely plays worse: it makes suboptimal choices more often, doesn’t phase its combat, doesn’t retreat damaged units, and reconsiders its strategy only sluggishly. A Deity AI plays the engine to its full ability and gets economic bonuses on top.

The higher difficulties also unlock a small, clearly-scoped set of adaptive cheats: a fog-of-war peek at rival posture, conditional production boosts while pursuing a goal, completion boosts on the home stretch of a wonder or spaceship, and an increased chance of coordinating a joint attack with another AI. These are bonuses with a purpose rather than omniscience.

What it’s optimised for

The Annhexation AI deliberately trades short-term tactical perfection for long-term strategic coherence. Its unit movement is somewhat greedy; it will occasionally make a locally-suboptimal step. But it musters real armies, plans amphibious invasions across several turns, reads which neighbour is weak and accessible, holds a campaign together through a dozen turns of grinding siege, and reacts when you take one of its cities.

The architecture is what makes that possible: a sticky goal stack on top, multi-turn plans in the middle, flexible greedy execution at the bottom, and a persistent memory threading it all together — with personality and difficulty as multipliers reaching into every layer. The result is eight civilizations that feel different, four difficulty levels that genuinely play differently, and an opponent whose intentions you can usually see coming. Stopping them is the game.

Testing and tools

It doesn’t take long before you realise that working on the AI will need you to analyse a lot of games and a lot of data. You need to see why it did something - as the AI grows in complexity you’ll find, or I found, that I would end up with units sat idle, units osciallating between two positions, hopeless attacks, settlers refusing to found cities. And all this can be impacted by all the possibilities that can emerge from the complex set of rules the AI follows and the situations that develop on the map.

And so you need instrumentation, a way to interrogate it, and a way to play more games than you humanly can. At least as a solo developer!

And so a big chunk of work turned out not to be the AI itself but building tools to let me use it and interrogate it.

A headless CLI for batch simulation

Playing the game by hand to test the AI is hopeless — turns are slow, and you need hundreds of them across many games to spot patterns. So there’s a command-line testbed that runs all-AI games with no rendering and no human in the loop:

testbed new   --map continent --difficulty deity --players 6   # create an all-AI game
testbed run   <gameId> --turns 250 --snapshot-every 10         # advance it, headless
testbed inspect <gameId>                                        # one-shot state summary
testbed list                                                    # all games + winners

run advances a game by N turns as fast as the machine will go, printing per-turn progress and bailing early if someone wins. inspect dumps a per-player table — civ, city count, unit count, gold, current research, alive or dead — and list shows every game in the diagnostics directory with its current turn and winner. This is what turns “I think the Mongolian AI rushes too hard” into “I ran forty games and Mongolia wins by turn 90 in thirty of them” — the difference between a hunch and a regression test. Everything is stored in a per-game directory (state.json, ai-states.json, a run.log of notable events like cities founded and wars declared) ready for inspection.

An in-browser testbed and AI inspector

The CLI is great for volume but blind to space — it can’t show you that the army is stuck because a single enemy scout is sitting on the only bridge. For that I run all-AI games inside the actual client. When a game has no human player the normal “End Turn” button is replaced by a testbed panel: buttons to advance 1, 5, 10, 20, 50 or 250 turns, and a “view as” dropdown that swaps the map’s fog-of-war filter so you can watch the game unfold from any AI’s perspective.

Layered on top of that is an AI inspector that lets you select any AI unit or city and it surfaces the internal state that the JSON logs hold, but anchored to what you’re looking at on the map:

  • the player’s goal stack — each goal’s type, priority, whether it’s active or blocked, the turn it was created, and goal-specific detail (militaryPush vs player_2 → city_42, scienceVictory: 4/4 parts, 5 techs left)
  • live attack plans — target city, lifecycle state (gathering → besieging → assault), unit fill (5/8 units, siege needed) and rally point
  • the selected unit’s assignment — role, commitment, target, the turn it was assigned, and the plan it belongs to
  • the selected city’s classification (interior / border / coastal), the goals that involve it, and its garrison strength
  • the border model — per-rival tension, culture pressure with turns-to-flip, chokepoint counts
  • the personality weights that are notably high or low

Turn-by-turn decision logs

Underneath both of those is the thing I lean on most: every AI writes a complete, structured record of its reasoning every single turn. Point an environment variable at a directory and each turn produces a pretty-printed JSON file per AI player — turn-014-mongolia.json and a companion full-state ai-state-014-mongolia.json.

These aren’t log lines; they’re a forensic snapshot of the entire decision. A single turn file captures the goal stack with its scores, the posture and opportunity score it assigned every rival, every city’s production and classification, every unit’s assignment (role, target, commitment, position, HP), the active attack plans — and, crucially, a command trace: an ordered list of every command the AI issued that turn, tagged with the phase that issued it, and success: true or a blocked reason straight from the engine. So when a move silently does nothing, the log tells you the engine rejected it and why.

There are dedicated traces for the gnarly subsystems too: a combat trace of every simulated fight, a naval lifecycle narrative for debugging amphibious invasions (the single most fiddly thing in the whole AI), and a citySiteDecisions list recording every settle attempt and its outcome — accepted, too-close-to-foreign-city, food-tiles-short, on-foreign-landmass-blocked. That last one is the cure for the maddening “why won’t this settler settle?” bug: the answer is right there in the file. Here’s a heavily, heavily, trimmed example JSON from a turn:

{
  "turn": 18, "playerId": "player_4", "civilisation": "greece",
  "doctrine": "STRATEGIST", "difficulty": "hard",

  "goals": [
    { "type": "earlyExpand", "priority": 0, "status": "active", "createdOnTurn": 11,
      "targetCityCount": 4, "settlerCount": 0,
      "bestSites": [
        { "q": 23, "r": 20, "totalScore": 111.4, "penalties": 0 },
        { "q": 25, "r": 19, "totalScore": 109.6, "penalties": 0 }
        /*  277 more, descending  */
      ] },
    { "type": "infrastructureConsolidation", "priority": 1, "status": "active" },
    { "type": "warPreparation", "priority": 2, "status": "active",
      "targetPlayerId": "player_1", "targetForceSize": 4, "currentForceSize": 3 }
  ],

  "postures": {
    "player_2": { "militarisation": 0.69, "isRunawayLeader": true, "borderTension": 0.27 }
  },

  "cities": [
    { "name": "Athens", "population": 2, "production": "library", "classification": "border" }
  ],

  "commandTrace": [
    { "step": "10", "command": "moveUnit", "unitId": "unit_14", "role": "worker",
      "from": "25,23", "to": "26,23", "success": true },
    { "step": "10", "command": "buildImprovement", "unitId": "unit_14", "success": true },
    { "step": "16", "command": "endTurn", "success": true }
  ]
}

The workflow ties together neatly. Run a few hundred turns headless with the CLI; spot a game that went wrong in the list output; either replay it in the browser with the F3 inspector or crack open the turn-N JSON and read, in order, exactly what the AI was thinking and what the engine let it do. Most of the “the AI is being dumb” moments turn out to be one specific, fixable thing — and these tools are how you find it instead of guessing.

Conclusions

Creating an AI for a 4X is definitely quite an undertaking. Its pretty easy to get units moving around but getting the AI to act in ways that are both interesting and credible takes a lot of effort. Its not that the code is complicated but that their is so much interacting that small changes can result in difficult to predict second and third order effects.

I spent countless hours on things that on the one hand seem simple “stop a unit from oscillating between A and B” but turn out to be really rather complex. While yes you can put in guards “don’t do this” the guards themselves can have unforeseen effects and don’t fix root problems.

You also can’t automate all this away. Yes you can create test cases, yes you can have the AI play countless games against the AI, but an AI isn’t a human and its the human the AI has to respond interestingly to.

I’ve released Annhexation into early access now and the primary reason for that is the AI. I need more people to play it and then resolve the things that inevitably will emerge.

If you’d like to give it a go you can play it online, for free, now.

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Podcast: Formal Methods for Every Engineer in an AI-Powered Future

1 Share

In this podcast Shane Hastie, Lead Editor for Culture & Methods spoke to Gabriela Moreira about making formal methods accessible through the Quint specification language, how AI is dramatically lowering the barrier to entry for formal specification and model-based testing, and why defining correct system behaviour remains essential human work in an AI-driven world.

By Gabriela Moreira
Read the whole story
alvinashcraft
21 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

What Is Dystopian Fiction & How Do I Write It?

1 Share

Explore dystopian fiction and learn why readers love this genre. Discover its elements and get tips for writing dynamic dystopian stories.

What Is Dystopian Fiction?

Basically, dystopian fiction talks about an author’s idea of hell on Earth. As a genre, it’s as old as humanity. Dystopian fiction is often considered the ugly twin of utopian fiction. So, let’s take a brief look at this.

As a genre, utopian fiction existed first. It takes its name from Thomas More’s novel Utopia (1516). ‘Utopia’ sounds very much like the Greek ‘eutopia’, meaning ‘good place’. In his book, More describes an ideal society on a secluded island, where humanity can live happily ever after. As a literary genre, utopian fiction has its own characteristics. You can read up on this in this blog: What Is Utopian Fiction & How Do I Write It?

Now, back to the dystopian fiction. The Greek prefix ‘dys-‘ means ‘bad, diseased, abnormal, difficult.’ That already tells you that dystopia is the ‘bad place’. Dystopian fiction describes a world much worse than our own. Many dystopian stories fall under the mundane science fiction banner. In these stories, people live in a dangerous, oppressive, and unfair place. Let’s look at some more characteristics.

Characteristics Of Dystopian Fiction

1. The Mission

This is the most important element. Dystopian stories are all geared at showing us what could happen in the future if the current society continues on its path. It’s a call to action to stop in our tracks and to change our course. If not, we’ll go to that fictional hell described by the author. The mission can be about our society as such (like ‘The Hunger Games’, depicting totalitarianism and media manipulation), or about a smaller aspect (climate change, for example, or the outbreak of a virus).

2. Distance To The Reader’s Present

Dystopian stories need to be set apart from our present reality. This distance lets us accept the message of the story. Without it, readers would take the criticism personally and ultimately dismiss it. The author can create this distance by time (science fiction or historical fiction), by place (inventing a new city or even another planet), or by manner. Distance by manner can involve fantasy, but full-blown fantasy creates too much distance. The readers would no longer transfer the dystopian warning to their own reality. This would ruin the story’s mission.

A smart way to create distance by manner is to create an alternative history starting at a specific event in the past. The TV show ‘The Man In The High Castle’ works like this. The alternative historical timeline starts with a different outcome of the Second World War. The story explores how the world would have been under Nazi rule. This parallel universe seems uncanny because it is so similar to our own world and yet so strikingly different.

3. Setting

The author must be meticulous about worldbuilding. Even if the dystopian story is mostly set in our own day and age, the rules and laws of that dystopian society need to differ from our own, and yet still form a consistent system. Inconsistency makes the story lose its magic.

4. The Guide Character

Both utopian and dystopian stories require a character who guides the readers into the world of the story. It is through these characters that we explore the set of rules of this new world.

  1. Utopian novels have a strong connection to our own world. Usually, there’s a character visiting the Utopian world first, and then telling the story when back home. The guide character here is usually a witness.
  2. A dystopian story may have that (but doesn’t have to). Usually, it’s the main character (MC) who serves as the reader’s guide. Readers need to be able to identify with the MC, as this character either experiences or witnesses the rebellion against the oppressive system.

Central Themes Of Dystopian Fiction

Most dystopian stories have certain themes in common. Here are a few:

  1. Control/oppression: There’s always a dictator or an elitist group in charge. They control society through government, bureaucracy, and/or technology. The spectrum of control can range from mere surveillance to psychological and even physical control (birth control, or mind control through implants, for example).
  2. Loss of individualism to a collective ideology: This ties in with the general totalitarian feel of most dystopias. It’s the logical consequence of the complex set of rules of oppression.
  3. Hostile environment: If the environment threatens the existence of the dystopian society, then people have nowhere to escape. This is another form of control, serving as an excuse to herd people together in confined spaces.
  4. A scapegoat: A scapegoat, a universal threat, or a common enemy all serve to unite the people. It camouflages the motives of the oppressive government. It makes it easier to control the people.
  5. Survival: The need for survival is how the oppressive government justifies its control. However, this is also the reason for the MC’s rebellion, as the MC needs to fight for the survival of the individual.

Let’s Write Dystopian Fiction

If you’d like to try writing in this genre, here’s a word of warning. There’s no dabbling. There’s no pantsing. As a writer, you need to plot meticulously.

Here’s your action plan:

  1. Decide what aspect of modern society aggravates you the most. This will serve as the mission of your story. Amplify and exaggerate as much as you can.
  2. Decide on how you can employ the element of distance in your dystopia (distance by time? By place? By manner?).
  3. Build your world. Make sure it is consistent.
  4. Devise a protagonist. How can your MC’s experiences show the rules of this new world to the reader? Beware of too much telling.
  5. How does your MC get into conflict with your dystopian world? Is there an area where your MC doesn’t feel at home? This is where the MC’s rebellion will start.
  6. What’s the MC’s inner motivation? What are the MC’s strengths that might help win the conflict?
  7. Will the MC win? Or lose? What does the MC’s rebellion do to change the dystopian world?

The Last Word

Dystopian fiction is a genre popular with authors and readers alike. It doesn’t have to include outright violence, and it doesn’t have to end up in chaos. But it’s a great way to show the troubles and conflicts of our present day. What do you think could be done about it all?  Have a go and write your own dystopian fiction! I have also written this post to inspire you: 7 Gripping Dystopian Plot Ideas For Writers

Further Reading

Here are some classics of this genre:

  1. Jonathan Swift, Gulliver’s Travels (1726)
  2. H.G. Wells, The Time Machine (1895)
  3. Aldous Huxley, Brave New World (1932)
  4. George Orwell, Nineteen Eighty-Four (1949)
  5. William Golding, Lord Of The Flies (1954)
  6. Lois Lowry, The Giver (1993)
  7. Suzanne Collins, The Hunger Games (series, starting in 2008)
  8. Ernest Cline, Ready Player One (2011)

Please check out Wikipedia’s list of dystopian fiction.

Susanne Bennett
By Susanne Bennett. Susanne is a German-American writer who is a journalist by trade and a writer by heart. After years of working at German public radio and an online news portal, she has decided to accept challenges by Deadlines for Writers. Currently she is writing her first novel with them. She is known for overweight purses and carrying a novel everywhere. Follow her on Facebook.

More Posts From Susanne

  1. What Is Utopian Fiction & How Do I Write It?
  2. How To Write Alternate History: A Complete Guide For Writers
  3. How To Create Tension In Storytelling
  4. 7 Benefits Of Keeping A Diary – For Writers
  5. 7 Gripping Dystopian Plot Ideas For Writers
  6. Poets On Writing Poetry: Insights & Inspiration
  7. Poetry Made Easy: How To Read & Interpret Poems
  8. What Writers Can Learn From Trashy Novels
  9. 10 Weird Things Writers Do
  10. Weird Habits Of Famous Writers

Top Tip: Sign up for our free daily writing links.

The post What Is Dystopian Fiction & How Do I Write It? appeared first on Writers Write.

Read the whole story
alvinashcraft
40 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

v1.0.7-preview.1

1 Share

Feature: in-process (FFI) transport for the Node.js SDK

The Node.js SDK now supports an experimental in-process transport that hosts the Copilot runtime as a native library via FFI (using [koffi]((koffi.dev/redacted), instead of spawning a child process. This reduces process overhead and eliminates the need for a separate CLI installation when using a bundled native runtime. (#1953)

const connection = RuntimeConnection.forInProcess("/path/to/libcopilot-runtime.so");
const client = new CopilotClient({ runtimeConnection: connection });

Feature: canvasProvider field on session config

All SDKs (Node.js, .NET, Go, Python, Rust) now support an optional canvasProvider field on session create and resume config. This lets host connections supply a stable canvas-provider identity so host-provided canvases restore correctly across cold resume. (#1847)

  • TypeScript: canvasProvider: { id: "app:builtin:my-host", name: "My Host" }
  • C#: CanvasProvider = new CanvasProviderIdentity { Id = "app:builtin:my-host" }
  • Python: canvas_provider=CanvasProviderIdentity(id="app:builtin:my-host")
  • Go: CanvasProvider: &CanvasProviderIdentity{Id: "app:builtin:my-host"}

Feature: agent metadata in request handler contexts

Request handler callbacks across all SDKs (Node.js, Python, Go, .NET, Rust, Java) now expose agentId, parentAgentId, and interactionType from LLM inference request-start frames. This lets BYOK/CAPI request handlers identify which agent is making the inference request, and whether it is a subagent call. (#1949)

Other changes

  • bugfix: [.NET] fix request-handler forwarding so GET/HEAD requests do not receive an empty content body (#1949)

Generated by Release Changelog Generator · sonnet46 498.2K

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

python-1.11.0

1 Share

[1.11.0] - 2026-07-09

Added

  • agent-framework-core: Add message injection middleware so tools or host code can enqueue messages into an active run and drain them into the next model call within the same AgentSession (#6998)
  • agent-framework-core: Integrate message injection into create_harness_agent and the harness console sample so a running harness agent can be nudged mid-turn (#7027)
  • agent-framework-core: Add progressive MCP disclosure so agents can discover, load, and unload MCP tool schemas on demand while keeping the allowed_tools boundary intact (#6850)
  • agent-framework-core: Add refresh_interval (TTL) to CachingSkillsSource so cached skill lists expire and re-fetch on a configured interval (#6977)
  • agent-framework-core, agent-framework-foundry-hosting: Add SkillsSourceContext (invoking agent plus optional session) threaded through the skills source pipeline, enabling context-aware filtering and per-key cache isolation (#6895)
  • agent-framework-core: Allow disabling approval for SkillsProvider tools (#6867)
  • agent-framework-core: Allow opting out of FileAccessProvider tool approval (#6879)
  • agent-framework-core: Allow custom argument parsing for inline skill scripts so non-conforming tool-call argument shapes (for example, vLLM) can be handled (#6817)
  • agent-framework-hosting, agent-framework-hosting-responses: Add a hosting protocol helper surface (AgentState, WorkflowState, SessionStore, AgentRunArgs, WorkflowRunArgs) and Responses helpers (create_response_id, responses_session_id, responses_to_run, responses_from_run, responses_from_streaming_run) (#6891)
  • agent-framework-ag-ui: Add FastAPI SSE keepalive support for long, output-silent streams (#6980)
  • agent-framework-github-copilot: Forward skill_directories and disabled_skills to the Copilot session (#6937)
  • agent-framework-openai: Allow tool_choice: required when allowed_tools is set (#7024)
  • agent-framework-anthropic, agent-framework-core, agent-framework-foundry-hosting, agent-framework-gemini, agent-framework-openai: Mark hosted/provider-executed tool calls as informational-only via Content.informational_only so they remain visible in transcripts without local re-invocation (#6997)
  • samples: Add a deterministic action-boundary validation middleware sample (#6528)
  • samples: Add Agent Harness blog post accompanying samples, part 3 (#6741)
  • samples: Add a declarative Foundry Hosted Agent workflow sample (#6897)

Changed

  • agent-framework-core: [BREAKING — experimental] Extract caching from SkillsProvider into a CachingSkillsSource decorator (#6847)
  • agent-framework-core: [BREAKING — experimental] Treat nested SKILL.md content as part of the parent skill instead of discovering it as a separate skill root (#6849)
  • agent-framework-core: [BREAKING — experimental] FileAccess/FileMemory replace_lines now performs literal replacement (including line deletion) instead of always re-adding a line terminator (#6859)
  • agent-framework-core: Remove the experimental marker from the Skills API now that its surface is stable (#6974)
  • agent-framework-core: Lazy-load root agent_framework exports to reduce import cost for narrow-surface scenarios (#6962)
  • agent-framework-a2a, agent-framework-claude, agent-framework-copilotstudio, agent-framework-core, agent-framework-durabletask, agent-framework-github-copilot, agent-framework-purview: Implement ADR-0029 service_session_id lifecycle mapping, separating durable continuation state, per-run identity forwarding, and telemetry conversation-id extraction (#6724)
  • agent-framework-azurefunctions, agent-framework-core, agent-framework-durabletask: [BREAKING] Support multi-workflow hosting and sub-workflows on the Durable Task host, including per-workflow durable naming and nested human-in-the-loop request routing (#6696)
  • agent-framework-ag-ui: [BREAKING] Canonicalize AG-UI interrupt and resume handling around RUN_FINISHED.outcome.interrupts and canonical ResumeEntry payloads (#6925)
  • agent-framework-mem0: Support the mem0ai 2.x OSS search call shape (#7004)
  • agent-framework-mistral: Widen the uv_build backend requirement to allow newer uv releases (#7033)
  • agent-framework-lab: Raise the agentlightning dependency ceiling for the lightning extra (#6984)
  • agent-framework-claude, agent-framework-durabletask, agent-framework-gemini, agent-framework-monty, agent-framework-openai: Raise dependency floors to the first versions that provide the SDK APIs and typing consumed by these packages
  • docs: Clarify AgentSession.service_session_id scoping to document backing API key/project boundaries and hosted multi-tenant guidance (#6993)
  • docs: Add security guidance for external skill sources and script execution to harness feature docstrings (#6936)
  • samples: Bump vite and @vitejs/plugin-react-swc in the ChatKit integration sample frontend (#6613)
  • samples: Add a multi-tenant hosting security consideration note to the A2A sample (#6983)
  • samples: Update Foundry Hosted Agent samples for the v2 protocol changes (#6841)
  • samples: Use a writable runtime directory for the Foundry Skills sample (#6606)
  • tests: Add Agent typing smoke tests across chat clients (#6950)
  • tests: Skip NumPy stubs during mypy typing to unblock scheduled dependency-maintenance typing runs (#6969)
  • tests: Consolidate Dependabot dependency updates for dev tooling (uv, ruff, pytest, mypy, pyright, mcp, opentelemetry-sdk, poethepoet) across the workspace and package dev-dependency groups (#6984, #7033)
  • tests: Bump the transitive js-yaml dependency in the DevUI frontend lockfile (#6813)

Fixed

  • agent-framework-core: Parse the structured response value from the final message instead of concatenated text, avoiding spurious ValidationError/JSONDecodeError (#6383)
  • agent-framework-core: Fix read_skill_resource instruction dropping the .md extension (#7031)
  • agent-framework-core: Bind policy-enforcement approvals to a single tool invocation (call id, function, arguments, security label, and session) and consume them on first use (#6966)
  • agent-framework-core: Process messages to an executor serially within a superstep to prevent concurrent handler invocations for the same target executor (#6776)
  • agent-framework-core: Auto-inject local conversation history on stateless clients even when non-history context providers (for example, SkillsProvider and FileAccessProvider) are registered (#6810)
  • agent-framework-core: Improve the error message when a TypeVar is used in handler/executor registration (#4553)
  • agent-framework-anthropic, agent-framework-core: Fix Anthropic requests that mix tool calls and tool results in one assistant message, and return a deterministic result when the function-loop limit is reached with a blank final response (#6794)
  • agent-framework-anthropic, agent-framework-core, agent-framework-foundry-hosting, agent-framework-openai: Fix Foundry reasoning/MCP compaction so reasoning output keeps its provider id and reasoning plus MCP call pairs stay atomic (#6907)
  • agent-framework-anthropic: Normalize a single Anthropic tool value the same as a one-item sequence during request preparation (#6903)
  • agent-framework-anthropic: Migrate structured outputs to the stable output_config.format shape to avoid malformed/concatenated JSON when tools are also present (#5884)
  • agent-framework-azure-ai-search: Pass include_reference_source_data in agentic search requests so source_data is populated on returned references (#5100)
  • agent-framework-bedrock: Fix non-ASCII escaping in JSON content blocks returned by the Converse API (#6628)
  • agent-framework-foundry: Strip tools from the Foundry agent request on the preview path (allow_preview=True) to avoid invalid_payload errors (#6644)
  • agent-framework-gemini: Fix GeminiChatClient dropping image/file content on multimodal messages (#6751)
  • agent-framework-claude, agent-framework-core, agent-framework-github-copilot, agent-framework-ollama: Fix response metadata construction so usage, finish reason, raw response, continuation token, and structured value are propagated consistently across providers (#6955)
  • agent-framework-a2a: Accept A2A data URIs whose media type includes parameters before the ;base64 marker (#6818)
  • agent-framework-ag-ui: Prefer explicit AG-UI resume payloads over message-derived responses (#6360)
  • agent-framework-ag-ui: Clear queued approvals on cancel so cancelled flows do not leave stale prompts for later turns (#6947)
  • agent-framework-ag-ui: Preserve the streamed text message id in mixed snapshots with pending tool calls and streamed trailing text (#6269)
  • agent-framework-devui: Fix list[Message] input handling for declarative ToolAgent entries (#6534)
  • agent-framework-devui: Fix DevUI deployment Dockerfile auth args (#6150)
  • agent-framework-hyperlight: Harden workspace staging against symlinks and reparse points that could escape the sandbox workspace/mount root (#6856)
  • agent-framework-openai: Fix web_search_options sent to the Azure OpenAI Chat Completions API (#6225)
  • docs: Fix stale ChatAgent references in _clients.py docstrings and make tool-support examples copy/paste-safe (#6924)
  • samples: Fix an invalid options kwarg in the workflow shared-session sample (#6294)
  • docs: Add prerequisite command documentation for Python hosting samples (#5935)

Removed

  • agent-framework-hosting-telegram: [BREAKING] Remove the unreleased hosting-telegram package and the earlier host/channel surface from the workspace, superseded by the new hosting protocol helper surface (#6891)

Full Changelog: python-1.10.0...python-1.11.0

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

1.0.70

1 Share

2026-07-09

  • Add GPT-5.6 model support
  • Show a single Error prefix for mcp and skill command failures
  • Show the real parse error when --agent selects a malformed custom agent
  • web_fetch works through mandatory HTTPS proxies
  • Hide / search on the Gists tab
  • Treat superseded subagent runs as cancellations instead of failures
  • Add paginated session.mcp.resources read/list/listTemplates RPCs for MCP server resources
  • preToolUse hooks that exit with code 2 deny tool calls
  • Create draft skills when Forge finds a clear workflow pattern
  • Hide the GitHub App install nudge in remote terminals
  • Pin plugins to an exact commit SHA using the sha field in plugin source configuration
  • Add --sandbox and --no-sandbox flags to turn the OS-level shell sandbox on or off for the current session only, without changing your saved sandbox setting (useful with -p)
  • Add /refine to rewrite a rough, stream-of-consciousness prompt into a clear one
  • Add --repo and --local flags to /settings and /model
  • Add a setting to show or hide timeline timestamps
  • Let a trusted repository pin the model, effort level, and context tier and extend the URL/MCP/skill deny lists via .github/copilot/settings.json
  • Expose SDK APIs to manage live MCP servers in running sessions
  • Show the active user's models after /user switch
  • Declining an extension's permission prompt no longer disables tool approvals for the rest of the session
  • Avoid redundant background agent notifications after a blocking read_agent returns its result
  • Startup auth errors recommend the real copilot login command
  • Keep merge-semantics settings editable in /settings
  • Re-sync managed plugins when their cache is missing or empty
  • Copy the last assistant response even after command echoes
  • Persist the last-logged-in user on every login so a restarted runtime client stays authenticated
  • Hide /agent picker navigation hints when there is nothing to select
  • Open the plan file or research report with Ctrl+Y in any mode
  • Keep terminal color scheme changes in sync over SSH and remote shells
  • Prefill /chronicle search so it can accept a query
  • Show a distinct scrollbar thumb glyph in the /model picker on the no-color path (--no-color, non-color terminals) so the scroll position stays visible
  • Skip launching a browser in remote terminals
  • Arrow keys in /search and reverse search stay in search instead of switching tabs
  • Restore tool event ordering so permission prompts appear after tool start
  • Show only one cancellation message when streaming is aborted
  • Keep /pr tables aligned in compact timeline view
  • Show clear validation errors for empty or non-ASCII skill and command names
  • Keep footer selection highlights aligned when the session bar is open
  • Fail fast when marketplace plugin git auth needs a terminal prompt
  • Dismiss other pending read and fetch sandbox-bypass prompts after you disable the sandbox
  • Fix a crash on Windows triggered by desktop toast notifications
  • Improve GPT-5.6 commentary guidance for tool-driven progress updates
  • Highlight the sidebar toggle hint in the input footer
  • Make markdown links and bare URLs in the timeline and tool output clickable
  • Reclaim the blank line under the home tab bar: the timeline (and Sessions+Current split) sits flush under the tabs when a prompt is pinned, keeping one breathing line only while nothing is pinned
  • Press Tab to switch the context window in /model
  • Long-running sessions refresh enterprise managed settings hourly
  • Mark locally-spawned MCP servers that run inside the sandbox in /mcp list (e.g. connected (sandboxed))
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories