Skip to content

32. Colony

Sands (chapter 31) had an economy of one good: crystal became credits, and credits paid for everything. Colony is a game about the economy itself. A green valley, a storehouse and a few dozen settlers: the player lays out houses and links them with roads, flag to flag, and every piece of road gets a carrier who takes the goods from one flag to the next. Woodcutters fell trees and foresters plant new ones, the quarry breaks stone, the sawmill turns logs into planks. Farms grow grain, the windmill grinds it, the bakery bakes bread; the sheep farm turns grain and water into wool, the weaver makes cloth. Bread or fish and cloth, brought to a residence, make a new settler, and tools from the toolmaker turn settlers into workers. Lookout towers move the border out. Nobody fights, but the year turns: winter stops the fields, houses catch fire in the summer heat, and wolves come for the sheep.

A settlement after eleven minutes A road being laid

Mission 1 after eleven minutes (left): 57 of 60 settlers, the storehouse with its flag in the middle, the windmill, the sheep farm at the lake, a road from every door. Laying a road (right): from the flag at the white ring, the dotted line shows the shortest way to the tile under the mouse; one click lays it.

Settlement games are built from a few ideas that look simple and are not: goods that travel on a network of roads in short hops, each hop with a person of its own; buildings that ask for what they need instead of being sent it; people who are made from goods and become workers through tools; and a border that grows. Each of them is a small piece of engine, and Colony needs nearly all of the engine there is besides. It is the first game in this handbook whose difficulty lies not in quick hands or in an opponent, but in the shape of what the player builds.

What you will learn

  • A road network: flags and the roads between them, a road split in two by a new flag, the next road on the shortest way, searched once per goal and kept until the network changes
  • Carriers: one person per road, a ware carried one hop at a time, and what happens when the next flag is full
  • Demand instead of orders: buildings ask, the storehouses answer twice a second, every ware knows where it is going
  • Goods and recipes: a stock with limits, a recipe that takes bread or fish, workers made by tools
  • The land: a border made of claims, a year of seasons, sheep that graze on their own
  • Trouble without combat: fire and the well, wolves, the dog and the watchman's horn
  • A computer that builds: a plan in a text file, places scored by what the worker needs, a tower when there is no room
  • Roads with the mouse: a building laid out, the road following the mouse from its flag, click by click

The complete game is in amade-colony-game.

The rules

  1. A map is up to 64 × 64 tiles of 16 units: grass, forest, rock, water and mountains. The view shows 32 × 24 tiles; the panel on the right takes 128 of the 640 units, the goods bar at the top 20.
  2. The settlement starts with a storehouse, some goods and 48 settlers living in it. The land within a radius of the storehouse is the settlement's; everything is built inside the border.
  3. A building is chosen in the panel and laid out on free land, with room for its flag below the door. A builder walks there from the storehouse, the goods it costs (planks, stone) are carried to it, and the house rises as far as the goods brought allow.
  4. Roads run from flag to flag, one tile at a time. Each road has one carrier, who walks it back and forth and takes one ware at a time to the other flag. A flag holds eight wares; a long road gets faster when a flag in its middle makes two roads of it.
  5. Houses ask for what goes in and put what they make out at their flag. What nobody needs goes to the nearest storehouse. Woodcutters, quarry workers, fishers and farmers need tools before they take the job.
  6. A residence turns bread or fish, and cloth, into a new settler. A lookout tower claims the land around it, and its watchman's horn drives wolves away.
  7. A year has four seasons. In winter the fields do not grow. On missions with fire, a house catches fire now and then, more often in summer; a well nearby puts it out, else the house burns down. On missions with wolves, they come for the sheep in autumn and winter; the dog drives off one, then needs a rest.
  8. Each mission has a goal: a number of settlers, sometimes with goods in the storehouses, sometimes within a number of years. The fastest win of each mission is kept, and winning opens the next.

Step 1: Roads from flag to flag

In Sands, a harvester drove the whole way from the crystal to the refinery. That is fine for a few vehicles, but a settlement moves hundreds of goods, and the convention of the genre is a different one: a road joins two flags and no more, a ware travels in hops from flag to flag, and each hop is one carrier's work. A network of roads is a graph: the flags are its nodes, the roads its edges, and a road's length is its number of steps. The engine's RoadNetwork keeps it:

amade-game-engine · road/RoadNetwork.java
public final class RoadNetwork {
    /** A road from flag {@code from} to flag {@code to} over {@code cells}, the first at {@code from}, the last at {@code to}. */
    public record Road(int id, int from, int to, List<Cell> cells) {
        public Road {
            cells = List.copyOf(cells);
            if (cells.size() < 2) throw new IllegalArgumentException("a road needs two cells at least");
        }

        /** Steps from one end to the other. */
        public int length() {
            return cells.size() - 1;
        }

        /** The flag at the other end from {@code flag}. */
        public int other(int flag) {
            if (flag == from) return to;
            if (flag == to) return from;
            throw new IllegalArgumentException("flag " + flag + " is not an end of road " + id);
        }

        /** The cells walked from {@code flag} to the other end. */
        public List<Cell> from(int flag) {
            if (flag == from) return cells;
            if (flag != to) throw new IllegalArgumentException("flag " + flag + " is not an end of road " + id);
            List<Cell> back = new ArrayList<>(cells);
            Collections.reverse(back);
            return back;
        }
    }

    /** The shortest distance of every flag to one goal, and the road each takes on its way there. */
    private record Search(Map<Integer, Integer> distance, Map<Integer, Road> next) {
    }

    private static final int SEARCHES = 64;

    private final Map<Integer, Cell> flags = new TreeMap<>();
    private final Map<Cell, Integer> flagAt = new HashMap<>();
    private final Map<Integer, Road> roads = new TreeMap<>();
    /** Every cell a road runs over, but not its two flags: where a road may be split by a new flag. */
    private final Map<Cell, Road> roadAt = new HashMap<>();
    private final Map<Integer, List<Road>> touching = new TreeMap<>();
    private final Map<Integer, Search> searches = new LinkedHashMap<>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Search> eldest) {
            return size() > SEARCHES;
        }
    };
    private int nextFlag;
    private int nextRoad;
    private int version;
    private int searched;

A road is a list of cells, the first one its first flag's, the last one its second flag's. Two maps answer the questions the mouse asks: flagAt, which flag stands on a tile, and roadAt, which road runs over it. The cells of the flags are not in roadAt, so "a road runs here" always means "a flag can be put here". connect checks everything a road must be: steps from cell to cell, no crossing of another road, from its first flag to its second.

amade-game-engine · road/RoadNetwork.java
    /**
     * A road from {@code from} to {@code to} over {@code cells}: the first cell is {@code from}'s, the last
     * {@code to}'s, each one step from the one before, and none taken by another road or flag.
     */
    public Road connect(int from, int to, List<Cell> cells) {
        return connect(nextRoad, from, to, cells);
    }

    public Road connect(int id, int from, int to, List<Cell> cells) {
        if (roads.containsKey(id)) throw new IllegalArgumentException("road " + id + " exists");
        if (from == to) throw new IllegalArgumentException("a road joins two different flags");
        if (!flags.containsKey(from) || !flags.containsKey(to)) throw new IllegalArgumentException("no such flag");
        if (!cells.getFirst().equals(flags.get(from)) || !cells.getLast().equals(flags.get(to))) {
            throw new IllegalArgumentException("a road runs from its first flag to its second");
        }
        for (int i = 1; i < cells.size(); i++) {
            Cell a = cells.get(i - 1);
            Cell b = cells.get(i);
            if (Math.abs(a.col() - b.col()) + Math.abs(a.row() - b.row()) != 1) {
                throw new IllegalArgumentException("the cells of a road are steps: " + a + " to " + b);
            }
            if (i < cells.size() - 1 && (roadAt.containsKey(b) || flagAt.containsKey(b))) {
                throw new IllegalArgumentException("the road would cross another at " + b);
            }
        }
        Road road = new Road(id, from, to, cells);
        roads.put(id, road);
        for (Cell cell : cells.subList(1, cells.size() - 1)) roadAt.put(cell, road);
        touching.get(from).add(road);
        touching.get(to).add(road);
        nextRoad = Math.max(nextRoad, id + 1);
        changed();
        return road;
    }

The player's most useful move with a road is to put a flag in its middle: a road of twelve steps carries one ware every 24 steps of walking, two roads of six carry twice as many. split makes two roads of one, and the cells of the old road are cut at the new flag:

amade-game-engine · road/RoadNetwork.java
    /**
     * Puts a new flag on {@code cell}, in the middle of a road, and makes two roads of the one: a long road
     * becomes two short ones, each with a carrier of its own. Returns the new flag.
     */
    public int split(Cell cell) {
        Road road = roadAt.get(cell);
        if (road == null) throw new IllegalArgumentException("no road runs over " + cell);
        int at = road.cells().indexOf(cell);
        remove(road.id());
        int flag = addFlag(cell);
        connect(road.from(), flag, road.cells().subList(0, at + 1));
        connect(flag, road.to(), road.cells().subList(at, road.cells().size()));
        return flag;
    }
amade-game-engine · test/road/RoadNetworkTest.java
    @Test
    void aFlagInTheMiddleMakesTwoRoadsOfOne() {
        RoadNetwork roads = new RoadNetwork();
        int a = roads.addFlag(new Cell(0, 0));
        int b = roads.addFlag(new Cell(6, 0));
        RoadNetwork.Road road = connect(roads, a, b);
        assertEquals(road, roads.roadAt(new Cell(3, 0)));
        assertNull(roads.roadAt(new Cell(0, 0)), "the flags are not part of the road");

        int middle = roads.split(new Cell(3, 0));
        assertEquals(2, roads.roads().size());
        assertEquals(3, roads.next(a, b).length());
        assertEquals(middle, roads.next(a, b).other(a));
        assertEquals(6, roads.distance(a, b), "the way is as long as before");
        assertEquals(middle, roads.flagAt(new Cell(3, 0)));
    }

The next road

A ware at a flag wants to know one thing: which road do I take next? The way to its goal is a shortest path on the graph of flags, and since the roads have lengths, the search is Dijkstra's, not the breadth-first search of the flow fields (chapter 30). There are many wares, but few goals: the houses and the storehouses. So the search runs from the goal outwards, once, and gives every flag at once its distance to the goal and the road it takes. next is then a lookup in a map. The searches are kept for 64 goals, the least used dropped first, and all of them are forgotten when the network changes:

amade-game-engine · road/RoadNetwork.java
    /**
     * The road a ware at flag {@code from} takes next on a shortest way to flag {@code to}; {@code null}
     * when it is there already or no way leads there.
     */
    public Road next(int from, int to) {
        if (from == to) return null;
        return search(to).next().get(from);
    }

    /** Steps on the shortest way from one flag to another; {@link Integer#MAX_VALUE} without a way. */
    public int distance(int from, int to) {
        if (!flags.containsKey(from) || !flags.containsKey(to)) return Integer.MAX_VALUE;
        return search(to).distance().getOrDefault(from, Integer.MAX_VALUE);
    }

    public boolean connected(int from, int to) {
        return distance(from, to) != Integer.MAX_VALUE;
    }

    /** Dijkstra's search from the goal outwards, kept until the network changes. */
    private Search search(int goal) {
        Search search = searches.get(goal);
        if (search != null) return search;
        Map<Integer, Integer> distance = new HashMap<>();
        Map<Integer, Road> next = new HashMap<>();
        // Ties go to the lower flag number, so the answer never depends on the order of a hash map.
        PriorityQueue<int[]> open = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : Integer.compare(a[1], b[1]));
        if (flags.containsKey(goal)) {
            distance.put(goal, 0);
            open.add(new int[]{0, goal});
        }
        while (!open.isEmpty()) {
            int[] head = open.poll();
            int flag = head[1];
            if (head[0] > distance.get(flag)) continue;     // an older, longer entry of a flag settled since
            for (Road road : touching.get(flag)) {
                int other = road.other(flag);
                int d = head[0] + road.length();
                Integer known = distance.get(other);
                if (known == null || d < known || d == known && road.id() < next.get(other).id()) {
                    distance.put(other, d);
                    next.put(other, road);
                    open.add(new int[]{d, other});
                }
            }
        }
        search = new Search(distance, next);
        searches.put(goal, search);
        searched++;
        return search;
    }

Ties are broken by the flag number and the road number, never by the order of a hash map. This is the rule of chapter 31 again: a game that is to be saved and loaded and go on exactly as before must not depend on anything Java does not promise.

amade-game-engine · test/road/RoadNetworkTest.java
    @Test
    void aWareTakesTheShortestWayFromFlagToFlag() {
        RoadNetwork roads = new RoadNetwork();
        int home = roads.addFlag(new Cell(0, 0));
        int east = roads.addFlag(new Cell(10, 0));
        int south = roads.addFlag(new Cell(0, 4));
        int mill = roads.addFlag(new Cell(10, 4));
        RoadNetwork.Road top = connect(roads, home, east);      // 10 steps
        connect(roads, east, mill);                              // 4
        RoadNetwork.Road left = connect(roads, home, south);    // 4
        connect(roads, south, mill);                             // 10
        assertEquals(14, roads.distance(home, mill));
        // Both ways are 14 steps long: the tie goes to the road made first.
        assertEquals(top, roads.next(home, mill));

        roads.remove(top.id());
        assertEquals(left, roads.next(home, mill), "the other way now");
        assertEquals(14, roads.distance(home, mill));
        int middle = roads.split(new Cell(5, 4));                // a flag on the southern road
        assertEquals(3, roads.way(home, mill).size(), "three roads now");
        assertEquals(9, roads.distance(home, middle));
        assertNull(roads.next(mill, mill), "there already");
        assertEquals(left.id(), roads.way(south, home).getFirst().id());
    }

Step 2: Carriers

A carrier belongs to one road. They walk there from the storehouse, wait in the middle, and look at the two flags: is a ware waiting at one of them whose next road is this one? Then they walk to it, pick it up, carry it to the other flag and put it down. If that flag is the door of the house the ware is meant for, it goes into the house. The four tasks are a small state machine:

amade-colony-game · domain/Game.java
    /**
     * A carrier and the road: wait in the middle, walk to a flag where a ware wants this road, carry it to
     * the other flag, and put it down there; into the house when the house's door is that flag. When the
     * other flag is full, a ware that waits there for this road is taken back in exchange.
     */
    private void carrier(Person p, double seconds) {
        RoadNetwork.Road road = roads.road(p.job);
        if (road == null) {
            dismiss(p);
            return;
        }
        double steps = Field.WALK * seconds;
        switch (p.task) {
            case GOING -> {
                if (p.walk(steps)) {
                    int end = roads.flagAt(p.path.getLast());
                    p.path = road.cells();
                    p.along = end == road.to() ? road.length() : 0;
                    p.to = p.along;
                    p.task = Person.Task.IDLE;
                }
            }
            case IDLE -> {
                Ware ware = waiting(road);
                if (ware != null) {
                    p.ware = ware.id();
                    p.to = ware.flag() == road.from() ? 0 : road.length();
                    p.task = Person.Task.FETCH;
                    reserved.add(ware.id());
                } else {
                    p.to = road.length() / 2.0;
                    p.walk(steps);
                }
            }
            case FETCH -> {
                if (!p.walk(steps)) return;
                int flag = p.to == 0 ? road.from() : road.to();
                Ware ware = wares.get(p.ware);
                reserved.remove(p.ware);
                if (ware != null && ware.flag == flag && wants(ware, road)) {
                    ware.flag = -1;
                    p.load = ware.good();
                    p.to = p.to == 0 ? road.length() : 0;
                    p.task = Person.Task.CARRY;
                } else {
                    p.ware = -1;
                    p.task = Person.Task.IDLE;
                }
            }
            case CARRY -> {
                if (!p.walk(steps)) return;
                int flag = p.to == 0 ? road.from() : road.to();
                Ware ware = wares.get(p.ware);
                Building to = ware == null ? null : buildings.get(ware.to);
                if (ware == null) {
                    drop(p);
                } else if (to != null && to.flag() == flag) {
                    deliver(ware, to);
                    drop(p);
                } else if (waitingAt(flag).size() < Field.FLAG_ROOM) {
                    ware.flag = flag;
                    drop(p);
                } else {
                    Ware back = waitingFor(flag, road);
                    if (back != null) {
                        ware.flag = flag;
                        back.flag = -1;
                        p.ware = back.id();
                        p.load = back.good();
                        p.to = p.to == 0 ? road.length() : 0;
                    }
                }
            }
            default -> {
            }
        }
    }

Three details make it work:

  • A ware is reserved while a carrier walks to it, so that the carrier of the other road at the same flag does not walk there too. When the carrier arrives, they ask again whether the ware still wants this road: a new road may have made another way shorter in the meantime.
  • A full flag holds eight wares. A carrier who finds it full does not wait for room, because the carrier of the next road may be waiting for room at this carrier's flag, and the two would wait for each other for ever. Instead they swap: their ware is put down, and a ware that waits there for the way back is taken along. Without a ware to swap, they wait.
  • The question "is this my road?" is one line: roads.next(ware.flag, to.flag()), the lookup of Step 1.

Placing a windmill The sheep farm, chosen

A windmill being laid out (left): the green mark under the mouse says it may stand there, and the panel shows what it costs and what it does. The sheep farm, chosen (right): the shepherd and the dog in the fenced pasture, the panel with what goes in (grain, water), what comes out (wool), how far the work is, the sheep, the dog on watch.

Step 3: Demand: who needs what

Who decides where a ware goes? The obvious design, a house that makes planks and sends them to whoever might need them, puts the decision in the wrong place. Colony turns it round: every building says what it wants, and the ware is sent to the nearest building that wants it. A site wants what it costs, less what it has and what is on the way; a house wants what goes into its recipe, up to how many it keeps at the door:

amade-colony-game · domain/Game.java
    /** How many more of {@code good} a building wants brought: a site its cost, a house its inputs. */
    public int want(Building b, Good good) {
        if (b.burning()) return 0;
        if (!b.done) return b.type().cost().getOrDefault(good, 0) - b.stock.count(good) - b.coming(good);
        if (b.type().recipe() == null || !b.type().recipe().needs().contains(good)) return 0;
        return b.type().keep() - b.stock.count(good) - b.coming(good);
    }

The words "and what is on the way" matter more than any other in the economy. Without them, a sawmill that wants two logs asks again in every round until the first two arrive, and twenty logs set out for it. come counts each ware that sets out for a building, and arriving counts it back.

A ware made in a house is given a goal at once: the nearest building on the roads that wants it, or, if none does, the nearest storehouse.

amade-colony-game · domain/Game.java
    /**
     * Where a ware goes: to the nearest building on the roads that wants it, else to the nearest storehouse;
     * nowhere while no road leads to one.
     */
    private void route(Ware ware, Building from) {
        Building best = null;
        int bestD = Integer.MAX_VALUE;
        for (Building b : buildings.values()) {
            if (b == from || b.type().work() == Work.STORE || want(b, ware.good()) <= 0) continue;
            int d = roads.distance(ware.flag, b.flag());
            if (d < bestD) {
                bestD = d;
                best = b;
            }
        }
        if (best == null) best = nearestStore(ware.flag, null);
        ware.to = best == null ? -1 : best.id();
        if (best != null) best.come(ware.good(), 1);
    }

Twice a second, the storehouses look at what is missing. Wares whose goal can no longer be reached (the road was torn down, the house burned) get a new one. Buildings that want goods get them from the nearest storehouse that has them. And people are sent out: a carrier to every road without one, a builder to every site, a worker to every house that is ready. The buildings take turns being first, so that with few planks in the store no site is always served first and the others never:

amade-colony-game · domain/Game.java
    /**
     * Twice a second the storehouses look at what is missing: wares that lost their way get a new one, the
     * buildings that want goods get them from the nearest store that has them, and roads, sites and houses
     * without a person get a settler. The buildings take turns, so none is always served first.
     */
    void dispatch() {
        for (Ware ware : List.copyOf(wares.values())) {
            if (ware.flag < 0) continue;
            Building to = buildings.get(ware.to);
            if (to != null && to.flag() == ware.flag) {
                deliver(ware, to);
            } else if (to == null || !roads.connected(ware.flag, to.flag())) {
                if (to != null) to.come(ware.good(), -1);
                route(ware, null);
            }
        }
        List<Building> all = new ArrayList<>(buildings.values());
        int start = all.isEmpty() ? 0 : turn++ % all.size();
        for (int k = 0; k < all.size(); k++) {
            Building b = all.get((start + k) % all.size());
            if (b.type().work() == Work.STORE) continue;
            for (Good good : Good.values()) {
                if (want(b, good) <= 0) continue;
                Building store = nearestStore(b.flag(), good);
                if (store == null || waitingAt(store.flag()).size() >= Field.FLAG_ROOM) continue;
                store.stock.take(good, 1);
                Ware ware = new Ware(nextId++, good, b.id(), store.flag());
                wares.put(ware.id(), ware);
                b.come(good, 1);
            }
        }
        staff();
    }

Why twice a second and not in every step? The game runs 120 steps a second, and nothing in a settlement needs an answer within a hundred-and-twentieth of a second: a carrier takes seconds to walk a road. Asking less often costs nothing the player can see and saves most of the work. It is also the moment the computer thinks (Step 7).

The step of the whole game is short, because each part has its own method:

amade-colony-game · domain/Game.java
    /** Lets {@code seconds} pass: the seasons, the land, the people, the houses, the storehouses' orders. */
    public void update(double seconds) {
        events.clear();
        if (phase != Phase.PLAYING) return;
        time += seconds;
        if (seasons.update(seconds) > 0) events.add(new Event.Season(seasons.season(), seasons.year()));
        boolean winter = winter();
        grow(seconds, winter);
        for (Person person : List.copyOf(people.values())) step(person, seconds);
        for (Building building : List.copyOf(buildings.values())) {
            if (buildings.containsKey(building.id())) step(building, seconds, winter);
        }
        dispatch -= seconds;
        if (dispatch <= 0) {
            dispatch += Field.DISPATCH;
            dispatch();
            if (overseer != null) overseer.think();
        }
        if (mission.fire()) fire(seconds);
        if (mission.wolves()) wolves(seconds);
        judge();
    }

Step 4: Goods and recipes

What a house holds is a Stock: a count for each kind of good, kept in an EnumMap, with a limit if the house sets one. It is the smallest class of this chapter, and it is used everywhere: the storehouses, the sites, every house's inputs.

amade-game-engine · economy/Stock.java
public final class Stock<G extends Enum<G>> {
    private final Class<G> kinds;
    private final EnumMap<G, Integer> counts;
    private final EnumMap<G, Integer> limits;

    public Stock(Class<G> kinds) {
        this.kinds = kinds;
        this.counts = new EnumMap<>(kinds);
        this.limits = new EnumMap<>(kinds);
    }

    public int count(G good) {
        return counts.getOrDefault(good, 0);
    }

    /** How many {@code good} may be kept; {@link Integer#MAX_VALUE} unless a limit was set. */
    public int limit(G good) {
        return limits.getOrDefault(good, Integer.MAX_VALUE);
    }

    public void limit(G good, int limit) {
        if (limit < 0) throw new IllegalArgumentException("negative limit " + limit);
        limits.put(good, limit);
    }

    /** How many more {@code good} fit. */
    public int room(G good) {
        return Math.max(0, limit(good) - count(good));
    }

    /** Adds {@code count} of {@code good}, as far as they fit, and returns how many did not. */
    public int add(G good, int count) {
        if (count < 0) throw new IllegalArgumentException("negative count " + count);
        int taken = Math.min(count, room(good));
        if (taken > 0) counts.merge(good, taken, Integer::sum);
        return count - taken;
    }

    /** Takes {@code count} of {@code good} if there are that many; true if they were taken. */
    public boolean take(G good, int count) {
        if (count < 0) throw new IllegalArgumentException("negative count " + count);
        int have = count(good);
        if (have < count) return false;
        if (have == count) counts.remove(good);
        else counts.put(good, have - count);
        return true;
    }

What a house makes of it is a Recipe: a list of inputs, the good it makes, and how long that takes. Each input is a set of goods, any one of which will do. The residence needs "bread or fish" and "cloth": [{BREAD, FISH}, {CLOTH}]. In the data file that is written needs bread|fish cloth. take takes one good for each input, or nothing at all; a recipe never takes half of what it needs:

amade-game-engine · economy/Recipe.java
public record Recipe<G extends Enum<G>>(List<Set<G>> inputs, G makes, double seconds) {
    public Recipe {
        inputs = List.copyOf(inputs);
        for (Set<G> input : inputs) {
            if (input.isEmpty()) throw new IllegalArgumentException("an input needs one good at least");
        }
        if (seconds <= 0) throw new IllegalArgumentException("seconds " + seconds);
    }

    /** True if {@code stock} has a good for every input. */
    public boolean ready(Stock<G> stock) {
        return choose(stock) != null;
    }

    /** Takes one good for every input from {@code stock}; the goods taken, or {@code null} and nothing taken. */
    public List<G> take(Stock<G> stock) {
        List<G> chosen = choose(stock);
        if (chosen == null) return null;
        for (G good : chosen) stock.take(good, 1);
        return chosen;
    }

    private List<G> choose(Stock<G> stock) {
        List<G> chosen = new ArrayList<>(inputs.size());
        for (Set<G> input : inputs) {
            G found = null;
            for (G good : input.stream().sorted().toList()) {
                long already = chosen.stream().filter(good::equals).count();
                if (stock.count(good) > already) {
                    found = good;
                    break;
                }
            }
            if (found == null) return null;
            chosen.add(found);
        }
        return chosen;
    }
amade-game-engine · test/economy/StockTest.java
    @Test
    void aRecipeTakesOneOfEachInputAndPrefersTheFirstAlternative() {
        Recipe<Good> settler = new Recipe<>(List.of(Set.of(Good.FISH, Good.BREAD), Set.of(Good.CLOTH)), null, 30);
        Stock<Good> home = new Stock<>(Good.class);
        home.add(Good.FISH, 1);
        assertFalse(settler.ready(home), "no cloth");
        home.add(Good.CLOTH, 1);
        home.add(Good.BREAD, 1);
        assertEquals(List.of(Good.BREAD, Good.CLOTH), settler.take(home), "bread comes first in the enum");
        assertEquals(1, home.count(Good.FISH));
        assertNull(settler.take(home));
        assertEquals(Set.of(Good.BREAD, Good.FISH, Good.CLOTH), settler.needs());
    }

Stock and Recipe know nothing of the goods of Colony. They take any enum, as Production of chapter 31 did, and a game about steel and coal uses them as they are. A house then does the same thing in every step: if it is not making anything, has room at its door and has a worker, it takes what its recipe needs and starts; when the time is up, a good is made and put out at the flag. The residence's "good" is a settler. The sheep farm works faster with more sheep, and stops without any:

amade-colony-game · domain/Game.java
    /** A house works its recipe, keeps its sheep, and puts what it made out at its flag. */
    private void step(Building b, double seconds, boolean winter) {
        if (!b.done) return;
        Work work = b.type().work();
        if (work == Work.GRAZE) graze(b, seconds, winter);
        boolean timed = work == Work.MAKE || work == Work.GRAZE || work == Work.SETTLE;
        if (timed && !b.burning() && (b.staffed || work == Work.SETTLE)) {
            if (b.work > 0) {
                double pace = work == Work.GRAZE ? (double) b.sheep / Field.SHEEP : 1;
                b.work -= seconds * pace;
                if (b.work <= 0) {
                    b.work = 0;
                    if (work == Work.SETTLE) {
                        settlers++;
                        events.add(new Event.Settled(b));
                    } else {
                        b.out++;
                        events.add(new Event.Made(b, b.type().recipe().makes()));
                    }
                }
            }
            boolean room = work == Work.SETTLE || b.out < Field.OUT_ROOM && !enough(b.type().recipe().makes());
            if (b.work == 0 && room && (work != Work.GRAZE || b.sheep > 0) && b.type().recipe().take(b.stock) != null) {
                b.work = b.type().recipe().seconds();
            }
        }
        if (b.out > 0 && waitingAt(b.flag()).size() < Field.FLAG_ROOM) {
            b.out--;
            Ware ware = new Ware(nextId++, b.type().recipe().makes(), -1, b.flag());
            wares.put(ware.id(), ware);
            route(ware, b);
        }
    }

The goods and the buildings are data. buildings.txt gives each building its tab in the panel, its size, its cost, its work, its recipe and its radius, and the README explains every word. A Good is an enum with the colour it has on the minimap and in the goods bar:

amade-colony-game · domain/Good.java
public enum Good {
    LOG, PLANK, STONE, GRAIN, FLOUR, WATER, BREAD, FISH, WOOL, CLOTH, TOOLS;

    public String id() {
        return name().toLowerCase(Locale.ROOT);
    }

    /** The good a word of a data file names, or {@code null}. */
    public static Good ofId(String id) {
        for (Good good : values()) {
            if (good.id().equals(id)) return good;
        }
        return null;
    }
}
amade-colony-game · domain/Rules.java
    private static Map<String, BuildingType> buildings(List<TextData.Line> lines) {
        Map<String, BuildingType> buildings = new LinkedHashMap<>();
        for (Catalog.Entry building : Catalog.read(lines, "building")) {
            TextData.Line size = building.line("size");
            if (size.size() != 3) throw size.error("size takes the width and the height in tiles");
            int width = size.integer(1);
            int height = size.integer(2);
            if (width < 1 || height < 1 || width > 4 || height > 4) throw size.error("a building is 1 to 4 tiles a side");
            Work work = choice(building, "work", Work.class);
            Map<Good, Integer> cost = building.has("cost") ? goods(building.line("cost")) : Map.of();
            double seconds = cost.isEmpty() ? 1 : positive(building, "seconds");
            Recipe<Good> recipe = recipe(building, work);
            int radius = building.integer("radius", 0);
            boolean reaches = work == Work.STORE || work == Work.WATCH || work == Work.FELL || work == Work.PLANT
                    || work == Work.QUARRY || work == Work.FISH || work == Work.FARM;
            if (reaches && radius < 1) throw building.error("a building that works " + work.name().toLowerCase(Locale.ROOT)
                    + " needs a radius");
            BuildingType type = new BuildingType(building.name(), choice(building, "tab", BuildingType.Tab.class),
                    width, height, cost, seconds, work, recipe, radius, yes(building, "tool"),
                    building.integer("keep", 2));
            building.finish();
            buildings.put(type.id(), type);
        }
        if (buildings.values().stream().noneMatch(type -> type.work() == Work.STORE)) {
            throw new DataException("buildings.txt: no building is the storehouse (work store)");
        }
        return buildings;
    }

    /** What goes in and comes out: {@code needs} with a|b for either, {@code makes}, {@code takes}. */
    private static Recipe<Good> recipe(Catalog.Entry building, Work work) {
        List<Set<Good>> inputs = new ArrayList<>();
        if (building.has("needs")) {
            TextData.Line needs = building.line("needs");
            for (int i = 1; i < needs.size(); i++) {
                Set<Good> either = EnumSet.noneOf(Good.class);
                for (String word : needs.word(i).split("\\|")) either.add(good(needs, word));
                inputs.add(either);
            }
        }
        Good makes = building.has("makes") ? good(building.line("makes"), building.text("makes")) : null;
        boolean timed = work == Work.MAKE || work == Work.GRAZE || work == Work.SETTLE;
        if (timed) return new Recipe<>(inputs, makes, positive(building, "takes"));
        if (!inputs.isEmpty()) throw building.line("needs").error("only make, graze and settle take goods in");
        return makes == null ? null : new Recipe<>(List.of(), makes, 1);
    }

Step 5: Builders and workers

A site is a building that is not done. A builder walks there and works, but only as far as the goods brought allow: with half the planks there, the house stands half. This one rule makes the game readable: a site that stops growing is waiting for goods, and the player sees it without opening a panel.

amade-colony-game · domain/Game.java
    /** A builder works as far as the goods brought allow: half the goods, half the house. */
    private void builder(Person p, double seconds) {
        Building site = buildings.get(p.job);
        if (site == null || site.done) {
            dismiss(p);
            return;
        }
        if (p.task == Person.Task.GOING) {
            if (p.walk(Field.WALK * seconds)) p.task = Person.Task.BUILD;
            return;
        }
        double brought = (double) site.stock.total() / site.type().costTotal();
        if (site.built < brought) {
            site.built = Math.min(brought, site.built + seconds / site.type().seconds());
            p.timer = 1;
        } else {
            p.timer = 0;
        }
        if (site.built >= 1 - 1e-9) {
            complete(site);
            dismiss(p);
            events.add(new Event.Built(site));
        }
    }

When the house is done, the storehouse sends a settler to work there. A woodcutter, a quarry worker, a fisher or a farmer needs tools first: the settler takes a set of tools from the store and becomes a worker. Without tools, the house waits, and the panel says so. That is the second way people matter in Colony. Settlers are not only a score; idle ones are the workforce, and tools turn them into workers.

The workers who go out (woodcutter, forester, quarry worker, fisher, farmer, shepherd) look for the nearest work within the radius of their house that nobody else has taken: a grown tree, a free tile for a sapling, a rock with stone left, water with fish, a field to sow or reap. They walk there with A* (chapter 25), work, and walk back with what they got:

amade-colony-game · domain/Game.java
    /**
     * A worker looks for the nearest work within the radius of the house that nobody else is going to, and
     * walks there: a grown tree, a free tile for a sapling, a rock with stone, water with fish, a ripe field
     * or one to sow. False when there is none.
     */
    private boolean setOut(Person p, Building b) {
        Set<Cell> taken = new HashSet<>();
        for (Person other : people.values()) if (other.spot != null) taken.add(other.spot);
        Cell door = roads.cell(b.flag());
        int r = b.type().radius();
        List<Cell> near = new ArrayList<>();
        for (int dr = -r; dr <= r; dr++) {
            for (int dc = -r; dc <= r; dc++) {
                if (dc * dc + dr * dr > r * r + r) continue;
                int col = door.col() + dc;
                int row = door.row() + dr;
                if (level.contains(col, row) && target(b, col, row) && !taken.contains(new Cell(col, row))) {
                    near.add(new Cell(col, row));
                }
            }
        }
        near.sort((a, c) -> {
            int da = dist2(a, door);
            int dc = dist2(c, door);
            return da != dc ? Integer.compare(da, dc) : a.row() != c.row() ? Integer.compare(a.row(), c.row())
                    : Integer.compare(a.col(), c.col());
        });
        for (Cell target : near.subList(0, Math.min(4, near.size()))) {
            Cell stand = standFor(b.type().work(), target);
            if (stand == null) continue;
            AStar.Path path = AStar.find(ground, door, stand, this::walkCost);
            if (!path.found()) continue;
            p.walkAlong(path.cells());
            p.spot = target;
            p.task = Person.Task.OUT;
            return true;
        }
        return false;
    }

The place the worker chose is kept in spot and counted as taken. Two woodcutters next to each other would otherwise walk to the same tree, and one of them would come back with nothing.

Step 6: The land and the year

The border

The settlement owns the land within a radius of its storehouse and of each lookout tower. Where two circles overlap, the land is owned twice; a tower that burns down must give back its land, but not the land the storehouse still holds. The engine's Territory counts claims per tile, not owners: a tile is inside while its count is above zero.

amade-game-engine · tile/Territory.java
public final class Territory {
    private record Claim(int col, int row, int radius) {
    }

    private final int cols;
    private final int rows;
    private final int[] claims;
    private final Map<Integer, Claim> byId = new TreeMap<>();
    private int version;

    public Territory(int cols, int rows) {
        this.cols = cols;
        this.rows = rows;
        this.claims = new int[cols * rows];
    }

    /** Claims every tile within {@code radius} of {@code col}, {@code row}, under the number {@code id}. */
    public void claim(int id, int col, int row, int radius) {
        if (byId.containsKey(id)) throw new IllegalArgumentException("claim " + id + " exists");
        Claim claim = new Claim(col, row, radius);
        byId.put(id, claim);
        mark(claim, 1);
    }

    /** Gives back the land of claim {@code id}, where no other claim holds it. */
    public void release(int id) {
        Claim claim = byId.remove(id);
        if (claim == null) throw new IllegalArgumentException("no claim " + id);
        mark(claim, -1);
    }

    private void mark(Claim claim, int by) {
        int r = claim.radius();
        for (int row = Math.max(0, claim.row() - r); row <= Math.min(rows - 1, claim.row() + r); row++) {
            for (int col = Math.max(0, claim.col() - r); col <= Math.min(cols - 1, claim.col() + r); col++) {
                int dc = col - claim.col();
                int dr = row - claim.row();
                if (dc * dc + dr * dr <= r * r + r) claims[row * cols + col] += by;
            }
        }
        version++;
    }

    public boolean inside(int col, int row) {
        return col >= 0 && row >= 0 && col < cols && row < rows && claims[row * cols + col] > 0;
    }
amade-game-engine · test/tile/TerritoryTest.java
    @Test
    void aTowerPushesTheBorderOutAndTakesItsLandBack() {
        Territory land = new Territory(40, 30);
        land.claim(1, 10, 10, 5);
        assertTrue(land.inside(10, 15));
        assertFalse(land.inside(10, 17));
        assertTrue(land.border(15, 10));
        assertFalse(land.border(10, 10));

        land.claim(2, 16, 10, 5);                      // a lookout tower near the edge
        assertTrue(land.inside(20, 10));
        assertFalse(land.border(15, 10), "inside now, not at the border");
        land.release(2);                               // it burned
        assertFalse(land.inside(20, 10));
        assertTrue(land.inside(15, 10), "the first claim still holds its own land");
    }

The circle is dc² + dr² ≤ r² + r, not r². With r² alone, a circle of tiles has single tiles sticking out at the top, bottom, left and right; the + r rounds it. A building may stand only where every tile and its flag are inside:

amade-colony-game · domain/Game.java
    /** Whether {@code type} may stand with its top left tile at {@code col}, {@code row}. */
    public Placement placement(BuildingType type, int col, int row) {
        for (int r = row; r < row + type.height(); r++) {
            for (int c = col; c < col + type.width(); c++) {
                if (!level.contains(c, r) || !land.inside(c, r)) return Placement.OUTSIDE;
                if (!free(c, r)) return Placement.BLOCKED;
            }
        }
        Cell door = new Cell(col + type.flagColumn(), row + type.height());
        if (!level.contains(door.col(), door.row()) || !land.inside(door.col(), door.row())) return Placement.OUTSIDE;
        int flag = roads.flagAt(door);
        if (flag >= 0) {
            for (Building b : buildings.values()) if (b.flag() == flag) return Placement.FLAG;
            return Placement.OK;
        }
        if (roads.roadAt(door) != null) return flagSpace(door) ? Placement.OK : Placement.FLAG;
        return free(door.col(), door.row()) && flagSpace(door) ? Placement.OK : Placement.FLAG;
    }

Seasons

A year is four seasons of given lengths, from missions.txt: mission 3 has a winter of 170 seconds against 90 in mission 1. Seasons counts the time and answers which season and which year it is. update returns how many seasons began during the step, so that the game can say "Winter" exactly once:

amade-game-engine · time/Seasons.java
public final class Seasons {
    private final double[] lengths;
    private final double yearLength;
    private double time;

    public Seasons(double... lengths) {
        if (lengths.length == 0) throw new IllegalArgumentException("a year needs one season at least");
        double sum = 0;
        for (double length : lengths) {
            if (length <= 0) throw new IllegalArgumentException("season of " + length + " s");
            sum += length;
        }
        this.lengths = lengths.clone();
        this.yearLength = sum;
    }

    /** Lets {@code seconds} pass; the number of seasons that began meanwhile. */
    public int update(double seconds) {
        if (seconds < 0) throw new IllegalArgumentException("negative time " + seconds);
        long before = count(time);
        time += seconds;
        return (int) (count(time) - before);
    }

    /** The seasons begun since the start, the first not counted. */
    private long count(double at) {
        long years = (long) Math.floor(at / yearLength);
        double rest = at - years * yearLength;
        int season = 0;
        while (season < lengths.length - 1 && rest >= lengths[season]) {
            rest -= lengths[season];
            season++;
        }
        return years * lengths.length + season;
    }

    /** The current season, 0 to the number of seasons less one. */
    public int season() {
        return (int) (count(time) % lengths.length);
    }

    /** The current year, from 0. */
    public int year() {
        return (int) (count(time) / lengths.length);
    }
amade-game-engine · test/time/SeasonsTest.java
    @Test
    void theYearGoesRoundInSeasonsOfTheirOwnLength() {
        Seasons year = new Seasons(10, 20, 10, 5);
        assertEquals(0, year.update(9.5));
        assertEquals(0, year.season());
        assertEquals(1, year.update(1));
        assertEquals(1, year.season());
        assertEquals(0.025, year.progress(), 1e-9);
        assertEquals(2, year.update(30), "autumn and winter began");
        assertEquals(3, year.season());
        assertEquals(0, year.year());
        assertEquals(0, year.update(4));
        assertEquals(1, year.update(1));
        assertEquals(0, year.season());
        assertEquals(1, year.year(), "spring of the second year");
    }

Seasons know nothing of snow or harvest; the game decides what they mean. In winter, fields and trees do not grow, and a farmer has nothing to reap. Autumn is the warning, and the message line says so: Autumn: the harvest ends soon. A settlement that has no bread and fish in the store at the start of winter makes no settlers until spring.

Winter in mission 3 Wolves at the pasture

Mission 3 in winter (left): snow on the ground, the trees and the rocks, the roads trodden into slush. Wolves (right): the message at the bottom left, the sheep farm at the edge of the forest, two towers nearby whose watchmen blow the horn.

Grazing

Sheep are not workers and have no task; they graze. Each one walks to a spot in the pasture, stands there for one to four seconds, and walks to the next. When a wolf comes, they run away from it, faster. The engine's Wander is that behaviour, for anything that idles around a home: sheep, chickens, villagers on a square.

amade-game-engine · motion/Wander.java
public final class Wander {
    private double homeX;
    private double homeY;
    private final double radius;
    private final double speed;
    private double x;
    private double y;
    private double targetX;
    private double targetY;
    private double pause;
    private double pace = 1;
    private boolean left;

    public Wander(double homeX, double homeY, double radius, double speed) {
        if (radius < 0 || speed <= 0) throw new IllegalArgumentException("radius " + radius + ", speed " + speed);
        this.homeX = homeX;
        this.homeY = homeY;
        this.radius = radius;
        this.speed = speed;
        this.x = homeX;
        this.y = homeY;
        this.targetX = homeX;
        this.targetY = homeY;
    }

    /** One step: stands or walks; true while it walks. */
    public boolean update(double seconds, SeededRandom random) {
        if (pause > 0) {
            pause -= seconds;
            if (pause > 0) return false;
            pickSpot(random);
        }
        double dx = targetX - x;
        double dy = targetY - y;
        double distance = Math.hypot(dx, dy);
        double step = speed * pace * seconds;
        if (Math.abs(dx) > 1e-9) left = dx < 0;
        if (distance <= step) {
            x = targetX;
            y = targetY;
            pace = 1;
            pause = 1 + random.nextDouble() * 3;     // one to four seconds of grazing
            return false;
        }
        x += dx / distance * step;
        y += dy / distance * step;
        return true;
    }

    /** A spot drawn evenly from the circle: the square root keeps the middle from crowding. */
    private void pickSpot(SeededRandom random) {
        double angle = random.nextDouble() * 2 * Math.PI;
        double r = radius * Math.sqrt(random.nextDouble());
        targetX = homeX + Math.cos(angle) * r;
        targetY = homeY + Math.sin(angle) * r;
    }

The spot is drawn evenly from the circle, which needs the square root: an angle and a radius drawn without it put half of the spots in the inner quarter of the circle, and the sheep would crowd in the middle.

amade-game-engine · test/motion/WanderTest.java
    @Test
    void aSheepGrazesWithinItsPastureAndWalksAndStands() {
        Wander sheep = new Wander(100, 50, 20, 8);
        SeededRandom random = new SeededRandom(3);
        int walking = 0;
        for (int i = 0; i < 120 * 60; i++) {
            if (sheep.update(1.0 / 120, random)) walking++;
            assertTrue(Math.hypot(sheep.x() - 100, sheep.y() - 50) <= 20 + 1e-9, "never beyond the fence");
        }
        assertTrue(walking > 120 * 5, "it walks some of the time");
        assertTrue(walking < 120 * 55, "and stands some of the time");
    }

Step 7: Fire and wolves

Colony has no enemy who builds, but it has trouble that comes on its own. Both are made so that the player can prepare for them, which is the difference between pressure and bad luck.

Fire. Now and then a house catches fire, every 150 seconds or so in summer and every 450 in the other seasons, the first never before five minutes. A well with a worker within eight tiles puts it out in five seconds. Without one, the house burns for 25 seconds and is gone, with what was in it, and leaves ashes where nothing can be built for a minute. The computer builds what burned again; the player must do it themselves.

amade-colony-game · domain/Game.java
    /**
     * Now and then a house catches fire, more often in summer. A manned well within reach puts it out; else
     * the house burns down after a while, and what was in it with it.
     */
    private void fire(double seconds) {
        fireTimer -= seconds;
        if (fireTimer <= 0) {
            double every = seasons.season() == 1 ? Field.FIRE_SUMMER : Field.FIRE_ELSE;
            fireTimer = every * (0.5 + random.nextDouble());
            List<Building> houses = new ArrayList<>();
            for (Building b : buildings.values()) {
                if (b.done && !b.burning() && b.type().work() != Work.STORE && b.type().work() != Work.WATCH) houses.add(b);
            }
            if (!houses.isEmpty()) {
                Building b = houses.get(random.nextInt(houses.size()));
                b.fire = 1e-6;
                events.add(new Event.Burning(b));
            }
        }
        for (Building b : List.copyOf(buildings.values())) {
            if (!b.burning()) continue;
            b.fire += seconds;
            if (b.douse > 0) {
                b.douse -= seconds;
                if (b.douse <= 0) {
                    b.douse = 0;
                    b.fire = 0;
                    events.add(new Event.Doused(b));
                }
            } else if (well(b) != null) {
                b.douse = Field.DOUSE;
            } else if (b.fire >= Field.BURNS) {
                remove(b);
                for (int r = b.row(); r < b.row() + b.type().height(); r++) {
                    for (int c = b.col(); c < b.col() + b.type().width(); c++) ashes.put(index(c, r), 60.0);
                }
                landVersion++;
                events.add(new Event.Burned(b));
                if (overseer != null) overseer.burned(b.type());
            }
        }
    }

Wolves. On missions with wolves, they leave their dens in autumn and winter for the nearest pasture, two at a time in winter. The dog at the sheep farm drives one away, then needs twelve seconds of rest. A tower near the pasture blows its horn at the next one. A wolf that gets through takes a sheep. New lambs come every 45 seconds while the herd is smaller than five, so a lost sheep is a slower farm, not a lost one.

amade-colony-game · domain/Game.java
    /**
     * In autumn and winter wolves leave their dens for the nearest pasture; in winter two at a time. The dog
     * drives one off, then needs a rest; the watchman of a tower nearby blows the horn at the next. A wolf
     * that gets through takes a sheep.
     */
    private void wolves(double seconds) {
        int season = seasons.season();
        if (season >= 2) {
            wolfTimer -= seconds;
            if (wolfTimer <= 0) {
                wolfTimer = Field.WOLF_EVERY * (0.7 + 0.6 * random.nextDouble());
                hunt(season == 3 ? 2 : 1);
            }
        }
        for (Wolf wolf : List.copyOf(wolves)) {
            Building farm = buildings.get(wolf.farm);
            double speed = Field.WOLF_SPEED * seconds * (wolf.fleeing ? 1.4 : 1);
            if (wolf.fleeing || farm == null) {
                wolf.fleeing = true;
                wolf.along -= speed;
                if (wolf.along <= 0) wolves.remove(wolf);
                continue;
            }
            wolf.along = Math.min(wolf.path.size() - 1, wolf.along + speed);
            double[] pen = farm.pasture();
            double dx = wolf.x() - (pen[0] + pen[2] / 2);
            double dy = wolf.y() - (pen[1] + pen[3] / 2);
            if (Math.hypot(dx, dy) > Field.DOG_REACH * Field.TILE && wolf.along < wolf.path.size() - 1) continue;
            wolf.fleeing = true;
            if (farm.staffed && farm.dog <= 0) {
                farm.dog = Field.DOG_REST;
                farm.dogX = wolf.x();
                farm.dogY = wolf.y();
                for (Wander sheep : farm.herd) sheep.flee(wolf.x(), wolf.y());
                events.add(new Event.Barked(farm, false));
            } else if (watched(farm)) {
                events.add(new Event.Barked(farm, true));
            } else if (!farm.herd.isEmpty()) {
                farm.herd.removeLast();
                farm.sheep = farm.herd.size();
                for (Wander sheep : farm.herd) sheep.flee(wolf.x(), wolf.y());
                events.add(new Event.SheepTaken(farm));
            }
        }
    }

A house on fire

The sheep farm on fire in mission 2, after five minutes: flames on the roof and in the pasture, and a red mark on the minimap. The only well is too far away; unless one is built and manned within reach, the farm is gone in 25 seconds.

Step 8: The computer builds

In Sands the computer played against the player. Here it plays alone, on the title screen and in the tests, and that second use is the more important one. GameTest lets the computer play mission 1 from the start and asks that it is won within fifteen minutes. If someone changes a number in buildings.txt and the economy stalls (too little stone for the toolmaker, a residence that waits for cloth for ever), the build fails:

amade-colony-game · test/domain/GameTest.java
    @Test
    void theComputerBuildsASettlementThatGrowsToTheGoal() {
        Game game = game(1, true);
        run(game, 15 * 60);
        assertEquals(Game.Phase.WON, game.phase(), "mission 1 won within fifteen minutes; settlers: " + game.settlers());
        assertTrue(game.settlers() >= game.mission().goal());
    }

The computer is simple on purpose. It builds in the order of plan.txt, waits until the storehouses have what the next building costs, and never has more than two sites at a time. When it finds no place three times in a row, it builds a lookout tower first: no room means too little land.

amade-colony-game · domain/Overseer.java
    void think() {
        if (++thought % EVERY != 0 || game.phase != Game.Phase.PLAYING) return;
        long sites = game.buildings.values().stream().filter(b -> !b.done).count();
        if (sites >= SITES) return;
        List<BuildingType> plan = game.rules.plan().build();
        if (next >= plan.size()) next = plan.size() - 4;        // at the end, it builds the last ones again
        boolean first = !urgent.isEmpty();
        BuildingType type = first ? urgent.getFirst() : plan.get(next);
        Building store = game.home();
        if (store == null) return;
        for (var cost : type.cost().entrySet()) {
            if (game.stores(cost.getKey()) < cost.getValue()) return;   // it waits for the goods
        }
        if (place(type, store)) {
            if (first) urgent.removeFirst();
            else {
                next++;
                towers = 0;
            }
            failures = 0;
        } else if (++failures >= 3) {
            failures = 0;
            BuildingType tower = tower();
            if (!first && type != tower && tower != null && towers < 2) {
                urgent.addFirst(tower);                       // no room: more land first
                towers++;
            } else if (first) {
                urgent.removeFirst();
            } else {
                next++;
                towers = 0;
            }
        }
    }

    /** What burned down is built again. */
    void burned(BuildingType type) {
        if (!urgent.contains(type)) urgent.add(type);
    }

    private BuildingType tower() {
        for (BuildingType type : game.rules.buildings()) if (type.work() == Work.WATCH) return type;
        return null;
    }

Where a building goes is a score for every free place inside the border: near the storehouse, and near what its worker needs. A woodcutter wants trees within its radius, a quarry rock, a fisher water, a farm free land; a forester wants to be near a woodcutter; a tower wants to be far from the storehouse, where it gains the most land.

amade-colony-game · domain/Overseer.java
    /** Lower is better: near the storehouse, and near what the worker needs. NaN where it cannot work. */
    private double score(BuildingType type, int col, int row, Building store) {
        Cell door = new Cell(col + type.flagColumn(), row + type.height());
        double home = Math.hypot(door.col() - store.col() - 1, door.row() - store.row() - 2);
        int r = type.radius();
        return switch (type.work()) {
            case FELL -> need(count(door, r, (c, w) -> game.tree(c, w) >= 1), 4, home - 0.8 * count(door, r,
                    (c, w) -> game.tree(c, w) >= 1));
            case PLANT -> need(count(door, r, game::free), 8, 1.5 * nearest(door, Work.FELL) + 0.3 * home);
            case QUARRY -> need(count(door, r, (c, w) -> game.stone(c, w) > 0), 3, home - count(door, r,
                    (c, w) -> game.stone(c, w) > 0));
            case FISH -> need(count(door, r, (c, w) -> game.fish(c, w) > 0), 4, home - 0.5 * count(door, r,
                    (c, w) -> game.fish(c, w) > 0));
            case FARM -> need(count(door, r, game::free), 8, home - 0.3 * count(door, r, game::free)
                    + 0.5 * count(door, r, (c, w) -> game.tree(c, w) >= 0));
            case WATCH -> -home;
            default -> home;
        };
    }

The road from the new flag goes to the nearest flag that already leads to the storehouse, and is then cut in the middle, where a flag fits, until no piece is longer than six steps; each piece gets a carrier of its own:

amade-colony-game · domain/Overseer.java
    /** A road from the new flag to the nearest flag that leads to the storehouse, cut into short pieces. */
    private boolean connect(int flag, Building store) {
        RoadNetwork roads = game.roads;
        if (roads.connected(flag, store.flag())) return true;
        Cell from = roads.cell(flag);
        List<Integer> others = new ArrayList<>();
        for (int other : roads.flags()) {
            if (other != flag && roads.connected(other, store.flag())) others.add(other);
        }
        others.sort(Comparator.comparingInt((Integer other) -> manhattan(from, roads.cell(other))).thenComparing(o -> o));
        List<Cell> best = List.of();
        for (int other : others.subList(0, Math.min(8, others.size()))) {
            List<Cell> way = game.plan(flag, roads.cell(other));
            if (!way.isEmpty() && (best.isEmpty() || way.size() < best.size())) best = way;
        }
        if (best.isEmpty() || game.pave(flag, best) != Game.Paving.OK) return false;
        cut(best);
        return true;
    }

The computer wins mission 1 in about eleven minutes. A player who plans well is faster.

Step 9: Laying out and roads with the mouse

The panel has three tabs of buildings, each cell with its picture and its cost. A click on a cell picks a building, and the next click on the map lays it out where the marks were green. The interesting part is the road. A new building with a flag that leads nowhere is useless, so the game goes straight on to the road: from the new flag, the shortest way to the tile under the mouse is shown as a dotted line, green where it may be laid and red where it may not.

amade-colony-game · ui/game/ColonyGame.java
        /** The road the mouse shows: the shortest way from the flag to the tile under it. */
        private void planning(Cell hover) {
            if (!roads || roadFrom < 0 || hover == null) {
                plan = List.of();
                return;
            }
            if (!plan.isEmpty() && plan.getLast().equals(hover)) return;
            plan = game.plan(roadFrom, hover);
            planOk = !plan.isEmpty() && game.canPave(roadFrom, plan) == Game.Paving.OK;
        }

The way is found by A* over the free tiles inside the border, so the line bends around houses and trees by itself:

amade-colony-game · domain/Game.java
    /** The shortest road the player could lay from a flag to a tile; empty when there is none. */
    public List<Cell> plan(int from, Cell to) {
        if (!roads.hasFlag(from) || !level.contains(to.col(), to.row()) || !end(from, to)) return List.of();
        Cell start = roads.cell(from);
        AStar.Path path = AStar.find(ground, start, to, (c, r) -> {
            if (c == to.col() && r == to.row()) return 1;
            return land.inside(c, r) && free(c, r) ? 1 : Double.POSITIVE_INFINITY;
        });
        return path.found() && path.cells().size() >= 3 ? path.cells() : List.of();
    }

A click lays the piece of road the line shows and goes on from its end, until the road meets a flag or a road that is already there. Then laying stops, because the building is connected. Each piece is checked by canPave and laid by pave. A road that ends on another road splits it there, and a road that ends on free land puts a new flag there:

amade-colony-game · domain/Game.java
    /**
     * Whether a road may run from flag {@code from} over {@code cells}: one step at a time over free land
     * inside the border, to a flag, to a road that gets a flag there, or to free land where a flag can stand.
     */
    public Paving canPave(int from, List<Cell> cells) {
        if (!roads.hasFlag(from) || cells.isEmpty() || !cells.getFirst().equals(roads.cell(from))) return Paving.BLOCKED;
        if (cells.size() < 3) return Paving.SHORT;
        Set<Cell> seen = new HashSet<>();
        for (int i = 0; i < cells.size(); i++) {
            Cell cell = cells.get(i);
            if (!seen.add(cell)) return Paving.BLOCKED;
            if (i > 0) {
                Cell before = cells.get(i - 1);
                if (Math.abs(before.col() - cell.col()) + Math.abs(before.row() - cell.row()) != 1) return Paving.BLOCKED;
            }
            if (!level.contains(cell.col(), cell.row()) || !land.inside(cell.col(), cell.row())) return Paving.OUTSIDE;
            if (i > 0 && i < cells.size() - 1 && !free(cell.col(), cell.row())) return Paving.BLOCKED;
        }
        return end(from, cells.getLast()) ? Paving.OK : Paving.FLAG;
    }

    public Paving pave(int from, List<Cell> cells) {
        Paving paving = canPave(from, cells);
        if (paving != Paving.OK) return paving;
        Cell last = cells.getLast();
        int to = roads.flagAt(last);
        if (to < 0) to = roads.roadAt(last) != null ? roads.split(last) : roads.addFlag(last);
        roads.connect(from, to, cells);
        events.add(new Event.Paved(cells.size() - 1));
        return Paving.OK;
    }
amade-colony-game · ui/game/ColonyGame.java
        /**
         * The left button on the map. Placing: the site is laid out, and the road from its flag begins.
         * Laying roads: the first click picks the flag (or puts one down), each next one lays the road shown
         * and goes on from its end, until it meets the roads there are. Else it chooses what is there.
         */
        private void leftClick(Cell cell) {
            if (placing != null) {
                if (place == null) return;
                Game.Placement result = game.found(placing, place.col(), place.row());
                if (result != Game.Placement.OK) {
                    refuse(result);
                    return;
                }
                Building site = game.buildingAt(place.col(), place.row());
                placing = null;
                if (site != null && game.roads().roads(site.flag()).isEmpty()) {
                    roads = true;
                    roadFrom = site.flag();
                    choose(null, -1, null);
                    say(msg.get("message.road"), Hud.CHALK);
                }
                return;
            }
            if (roads) {
                road(cell);
                return;
            }
            Building there = game.buildingAt(cell.col(), cell.row());
            int at = game.roads().flagAt(cell);
            RoadNetwork.Road on = game.roads().roadAt(cell);
            if (there != null) choose(there, -1, null);
            else if (at >= 0) choose(null, at, null);
            else if (on != null) choose(null, -1, on);
            else choose(null, -1, null);
            if (there != null || at >= 0 || on != null) effects.tick();
        }

        private void road(Cell cell) {
            if (roadFrom < 0) {
                int at = game.roads().flagAt(cell);
                Building there = game.buildingAt(cell.col(), cell.row());
                if (at < 0 && there != null) at = there.flag();
                if (at < 0 && game.placeFlag(cell)) at = game.roads().flagAt(cell);
                if (at >= 0) {
                    roadFrom = at;
                    effects.tick();
                } else {
                    effects.no();
                }
                return;
            }
            if (plan.isEmpty()) {
                effects.no();
                return;
            }
            Cell end = plan.getLast();
            boolean joins = game.roads().flagAt(end) >= 0 || game.roads().roadAt(end) != null;
            Game.Paving paving = game.pave(roadFrom, plan);
            if (paving != Game.Paving.OK) {
                refuse(paving);
                return;
            }
            plan = List.of();
            roadFrom = joins ? -1 : game.roads().flagAt(end);
        }

The test plays exactly that with a mouse: the woodcutter from the first cell of the panel, a click on the map, a click on the storehouse's flag, and a carrier on the new road a second later:

amade-colony-game · test/ui/game/ColonyGameTest.java
    @Test
    void thePanelLaysOutABuildingAndTheRoadFromItsFlagFollows() {
        ColonyGame.PlayScene play = game.play(1);
        BuildingType woodcutter = game.rules().building("woodcutter");
        assertSame(woodcutter, Hud.buildings(play.game(), BuildingType.Tab.BASIC).getFirst());
        clickOn(Hud.cell(0));
        assertSame(woodcutter, play.placing());

        Cell at = spot(play.game(), woodcutter);
        over(play, new Cell(at.col() + (woodcutter.width() - 1) / 2, at.row() + (woodcutter.height() - 1) / 2));
        click(MouseEvent.BUTTON1);
        assertNull(play.placing());
        Building site = play.game().buildingAt(at.col(), at.row());
        assertNotNull(site, "the site is laid out");
        assertTrue(play.roads(), "and the road from its flag is next");
        assertEquals(site.flag(), play.roadFrom());

        over(play, play.game().roads().cell(play.game().home().flag()));
        assertFalse(play.plan().isEmpty(), "the mouse shows the way to the storehouse");
        click(MouseEvent.BUTTON1);
        assertEquals(1, play.game().roads().roads(site.flag()).size(), "the road is laid");
        assertEquals(-1, play.roadFrom(), "it met the roads there are, so laying stops");
        seconds(1);
        assertNotNull(play.game().carrier(play.game().roads().roads(site.flag()).getFirst()), "and a carrier is coming");
    }

A click on a house, a flag or a road shows it in the panel: the recipe with what is there and what is missing, how far the work is, whether a worker or tools are missing, the sheep and the dog, the wares waiting at a flag, the carrier of a road. The panel answers the question every settlement game asks all the time: why is nothing happening here?

amade-colony-game · ui/game/Hud.java
    /** A building: how far its site is, or what goes in and comes out, whether it works and what holds it up. */
    private void building(Graphics2D g, Game game, Building b) {
        BuildingType type = b.type();
        String name = msg.get("building." + type.id());
        if (!b.done()) {
            double y = title(g, name, msg.get("info.site"), AMBER);
            List<Good> goods = type.cost().keySet().stream().sorted().toList();
            for (int i = 0; i < goods.size(); i++) {
                Good good = goods.get(i);
                int needed = type.cost().get(good);
                slot(g, INFO.x + 5 + i * 34, y - 4, good, Math.min(b.count(good), needed) + "/" + needed,
                        b.count(good) >= needed ? GREEN : CHALK);
            }
            row(g, y + 20, msg.get("info.built"), (int) Math.floor(b.built() * 100) + "%", CHALK);
            bar(g, y + 25, b.built(), new Color(0xC89040), new Color(0xF8D080));
            boolean builder = game.people().stream().anyMatch(p -> p.role() == Person.Role.BUILDER && p.job() == b.id());
            row(g, y + 45, msg.get("info.builder"), msg.get(builder ? "info.coming" : "info.nobody"), builder ? CHALK : AMBER);
            return;
        }
        String status;
        Color colour;
        boolean worker = !type.manned() || game.people().stream()
                .anyMatch(p -> p.role() == Person.Role.WORKER && p.job() == b.id());
        if (b.burning()) {
            status = msg.get("info.burning");
            colour = RED;
        } else if (!worker) {
            status = msg.get(type.tool() ? "info.notools" : "info.noworker");
            colour = AMBER;
        } else if (b.working() || type.work() == Work.STORE) {
            status = msg.get(type.work() == Work.STORE ? "info.open" : "info.working");
            colour = GREEN;
        } else {
            status = msg.get("info.waiting");
            colour = GREY;
        }
        double y = title(g, name, status, colour);
        switch (type.work()) {
            case STORE -> {
                row(g, y, msg.get("info.settlers"), Integer.toString(game.settlers()), CHALK);
                row(g, y + 11, msg.get("info.idle"), Integer.toString(game.idle()), CHALK);
                row(g, y + 22, msg.get("info.goods"), Integer.toString(b.stocked()), CHALK);
                text(g, msg.get("info.store.text"), y + 40);
                return;
            }
            case WATCH -> {
                row(g, y, msg.get("info.watch"), msg.get(worker ? "info.yes" : "info.no"), worker ? GREEN : AMBER);
                text(g, msg.get("info.watch.text"), y + 18);
                return;
            }
            default -> {
            }
        }
        Recipe<Good> recipe = type.recipe();
        if (recipe != null) {
            double x = INFO.x + 5;
            for (Set<Good> input : recipe.inputs()) {
                Good good = input.stream().filter(gd -> b.count(gd) > 0).findFirst().orElse(input.iterator().next());
                int count = 0;
                for (Good each : input) count += b.count(each);
                slot(g, x, y - 4, good, Integer.toString(count), count > 0 ? CHALK : RED);
                x += 25;
            }
            if (!recipe.inputs().isEmpty()) {
                arrow(g, x, y + 1.5);
                x += 13;
            }
            slot(g, x, y - 4, recipe.makes(), b.out() > 0 ? Integer.toString(b.out()) : null, CHALK);
            bar(g, y + 12, b.progress(), new Color(0xC89040), new Color(0xF8D080));
            y += 28;
        }
        if (type.work() == Work.GRAZE) {
            row(g, y, msg.get("info.sheep"), b.sheep() + "/" + Field.SHEEP, b.sheep() > 0 ? CHALK : RED);
            row(g, y + 11, msg.get("info.dog"), msg.get(b.dog() > 0 ? "info.dog.resting" : "info.dog.watching"), CHALK);
            y += 22;
        }
        if (type.work() == Work.SETTLE) {
            text(g, msg.get("info.residence.text"), y + 2);
            return;
        }
        if (type.manned()) row(g, y, msg.get("info.worker." + type.work().name().toLowerCase()),
                msg.get(worker ? "info.yes" : "info.no"), worker ? CHALK : AMBER);
        Building well = game.well(b);
        row(g, y + 11, msg.get("info.well"), msg.get(well != null ? "info.yes" : "info.no"), well != null ? CHALK : GREY);
    }

Step 10: Saving the settlement

Chapter 31 saved a battle with Snapshot, exact to the bit. A settlement has more lists but the same kinds of things: buildings with their stock, flags, roads, wares with their goals, people with their task and the path they walk, trees, fields, fish, sheep with their spot, wolves, the seasons and the random numbers. Archive writes them all and reads them back:

amade-colony-game · domain/Archive.java
    public static Map<String, String> write(Game game) {
        Snapshot.Writer w = Snapshot.writer();
        w.put("mission", game.level.number()).put("computer", game.computer());
        w.put("time", game.time).put("dispatch", game.dispatch).put("turn", game.turn).put("next", game.nextId);
        w.put("fire", game.fireTimer).put("wolf", game.wolfTimer).put("fish", game.fishTimer);
        w.put("phase", game.phase).put("settlers", game.settlers).put("land", game.landVersion);
        w.put("seed", game.random.seed()).put("random", game.random.state());
        w.put("herdSeed", game.herd.seed()).put("herd", game.herd.state());
        w.put("season", game.seasons.time());
        StringBuilder ground = new StringBuilder();
        for (int row = 0; row < game.level.height(); row++) {
            for (int col = 0; col < game.level.width(); col++) ground.append(game.ground.get(col, row).name().charAt(0));
        }
        w.put("ground", ground.toString());
        w.put("trees", doubles(game.tree)).put("fields", doubles(game.field)).put("farms", ints(game.fieldFarm));
        w.put("stone", ints(game.stone)).put("fishes", ints(game.fish));
        w.list("ash");
        for (Map.Entry<Integer, Double> ash : game.ashes.entrySet()) {
            w.add("ash").put("tile", ash.getKey()).put("left", ash.getValue());
        }
        RoadNetwork roads = game.roads;
        w.put("nextFlag", roads.nextFlag()).put("nextRoad", roads.nextRoad());
        w.list("flag");
        for (int flag : roads.flags()) {
            Cell cell = roads.cell(flag);
            w.add("flag").put("id", flag).put("col", cell.col()).put("row", cell.row());
        }
        w.list("road");
        for (RoadNetwork.Road road : roads.roads()) {
            w.add("road").put("id", road.id()).put("from", road.from()).put("to", road.to()).put("cells", cells(road.cells()));
        }
        w.list("building");
        for (Building b : game.buildings.values()) {
            Snapshot.Writer o = w.add("building").put("id", b.id()).put("kind", b.type().id()).put("col", b.col())
                    .put("row", b.row()).put("flag", b.flag()).put("done", b.done).put("built", b.built)
                    .put("out", b.out).put("builder", b.builder).put("worker", b.worker).put("staffed", b.staffed)
                    .put("work", b.work).put("burning", b.fire).put("douse", b.douse).put("lamb", b.lamb)
                    .put("dog", b.dog).put("dogX", b.dogX).put("dogY", b.dogY);
            for (Good good : Good.values()) {
                o.put("stock." + good.id(), b.stock.count(good)).put("coming." + good.id(), b.coming(good));
            }
            o.list("sheep");
            for (Wander sheep : b.herd) o.add("sheep").put("state", doubles(sheep.state()));
        }
        w.list("person");
        for (Person p : game.people.values()) {
            w.add("person").put("id", p.id()).put("role", p.role()).put("job", p.job).put("task", p.task)
                    .put("path", cells(p.path)).put("along", p.along).put("to", p.to)
                    .put("load", p.load == null ? "none" : p.load.id()).put("ware", p.ware)
                    .put("spot", p.spot == null ? "none" : cells(List.of(p.spot))).put("timer", p.timer);
        }
        w.list("ware");
        for (Ware ware : game.wares.values()) {
            w.add("ware").put("id", ware.id()).put("good", ware.good().id()).put("to", ware.to).put("flag", ware.flag);
        }
        w.list("wolf");
        for (Wolf wolf : game.wolves) {
            w.add("wolf").put("farm", wolf.farm).put("path", cells(wolf.path)).put("along", wolf.along)
                    .put("fleeing", wolf.fleeing);
        }
        if (game.overseer != null) {
            Overseer o = game.overseer;
            Snapshot.Writer over = w.child("overseer").put("next", o.next).put("thought", o.thought)
                    .put("failures", o.failures).put("towers", o.towers);
            over.list("urgent");
            for (BuildingType type : o.urgent) over.add("urgent").put("kind", type.id());
        }
        return w.values();
    }

The flags and roads keep their numbers, because wares and carriers refer to them. RoadNetwork therefore has connect and addFlag with a given number, and restore sets its counters, so that the next new flag after loading gets the number it would have got without saving. The searches of Step 1 are not saved: they are worked out again the first time they are needed.

amade-colony-game · domain/Archive.java
    /** The saved settlement; {@code levels} gives the map of a mission number. */
    public static Game read(Map<String, String> values, Rules rules, IntFunction<Level> levels) {
        Snapshot.Reader r = Snapshot.reader(values);
        Level level = levels.apply(r.integer("mission"));
        Game game = new Game(level, rules, r.whole("seed"), r.whole("random"), r.bool("computer"));
        game.herd = SeededRandom.resume(r.whole("herdSeed"), r.whole("herd"));
        game.time = r.number("time");
        game.dispatch = r.number("dispatch");
        game.turn = r.integer("turn");
        game.nextId = r.integer("next");
        game.fireTimer = r.number("fire");
        game.wolfTimer = r.number("wolf");
        game.fishTimer = r.number("fish");
        game.phase = r.choice("phase", Game.Phase.class);
        game.settlers = r.integer("settlers");
        game.landVersion = r.integer("land");
        game.seasons.restore(r.number("season"));
        String ground = r.text("ground");
        if (ground.length() != level.width() * level.height()) throw new Snapshot.SnapshotException("ground: wrong size");
        for (int i = 0; i < ground.length(); i++) {
            Ground g = switch (ground.charAt(i)) {
                case 'G' -> Ground.GRASS;
                case 'F' -> Ground.FOREST;
                case 'R' -> Ground.ROCK;
                case 'W' -> Ground.WATER;
                case 'M' -> Ground.MOUNTAIN;
                default -> throw new Snapshot.SnapshotException("ground: '" + ground.charAt(i) + "'");
            };
            game.ground.set(i % level.width(), i / level.width(), g);
        }
        copy(doubles(r.text("trees")), game.tree);
        copy(doubles(r.text("fields")), game.field);
        copy(ints(r.text("farms")), game.fieldFarm);
        copy(ints(r.text("stone")), game.stone);
        copy(ints(r.text("fishes")), game.fish);
        for (Snapshot.Reader ash : r.list("ash")) game.ashes.put(ash.integer("tile"), ash.number("left"));
        RoadNetwork roads = game.roads;
        for (Snapshot.Reader flag : r.list("flag")) {
            roads.addFlag(flag.integer("id"), new Cell(flag.integer("col"), flag.integer("row")));
        }
        for (Snapshot.Reader road : r.list("road")) {
            roads.connect(road.integer("id"), road.integer("from"), road.integer("to"), cells(road.text("cells")));
        }
        roads.restore(r.integer("nextFlag"), r.integer("nextRoad"));
        for (Snapshot.Reader o : r.list("building")) {
            BuildingType type = rules.building(o.text("kind"));
            Building b = new Building(o.integer("id"), type, o.integer("col"), o.integer("row"), o.integer("flag"));
            b.done = o.bool("done");
            b.built = o.number("built");
            b.out = o.integer("out");
            b.builder = o.integer("builder");
            b.worker = o.integer("worker");
            b.staffed = o.bool("staffed");
            b.work = o.number("work");
            b.fire = o.number("burning");
            b.douse = o.number("douse");
            b.lamb = o.number("lamb");
            b.dog = o.number("dog");
            b.dogX = o.number("dogX");
            b.dogY = o.number("dogY");
            for (Good good : Good.values()) {
                b.stock.set(good, o.integer("stock." + good.id()));
                b.come(good, o.integer("coming." + good.id()));
            }
            double[] pen = b.pasture();
            for (Snapshot.Reader sheep : o.list("sheep")) {
                Wander wander = new Wander(pen[0] + pen[2] / 2, pen[1] + pen[3] / 2, Math.min(pen[2], pen[3]) / 2 - 5, 5);
                wander.restore(doubles(sheep.text("state")));
                b.herd.add(wander);
            }
            b.sheep = b.herd.size();
            game.buildings.put(b.id(), b);
            for (int row = b.row(); row < b.row() + type.height(); row++) {
                for (int col = b.col(); col < b.col() + type.width(); col++) game.built[game.index(col, row)] = b.id();
            }
            if (b.done && type.work() == Work.STORE) game.land.claim(b.id(), b.col() + 1, b.row() + 1, type.radius());
            if (b.done && b.staffed && type.work() == Work.WATCH) game.land.claim(b.id(), b.col(), b.row(), type.radius());
        }
        for (Snapshot.Reader o : r.list("person")) {
            Person p = new Person(o.integer("id"), o.choice("role", Person.Role.class), o.integer("job"));
            p.task = o.choice("task", Person.Task.class);
            p.path = cells(o.text("path"));
            p.along = o.number("along");
            p.to = o.number("to");
            p.load = good(o.text("load"));
            p.ware = o.integer("ware");
            String spot = o.text("spot");
            p.spot = spot.equals("none") ? null : cells(spot).getFirst();
            p.timer = o.number("timer");
            game.people.put(p.id(), p);
            if (p.task == Person.Task.FETCH && p.ware >= 0) game.reserved.add(p.ware);
        }
        for (Snapshot.Reader o : r.list("ware")) {
            Good good = good(o.text("good"));
            if (good == null) throw new Snapshot.SnapshotException("ware: no good");
            Ware ware = new Ware(o.integer("id"), good, o.integer("to"), o.integer("flag"));
            game.wares.put(ware.id(), ware);
        }
        for (Snapshot.Reader o : r.list("wolf")) {
            Wolf wolf = new Wolf(o.integer("farm"), cells(o.text("path")), o.number("along"));
            wolf.fleeing = o.bool("fleeing");
            game.wolves.add(wolf);
        }
        if (game.overseer != null && r.has("overseer.next")) {
            Snapshot.Reader over = r.child("overseer");
            game.overseer.next = over.integer("next");
            game.overseer.thought = over.integer("thought");
            game.overseer.failures = over.integer("failures");
            game.overseer.towers = over.integer("towers");
            for (Snapshot.Reader type : over.list("urgent")) game.overseer.urgent.add(rules.building(type.text("kind")));
        }
        return game;
    }

The test is the one of chapter 31: the computer plays mission 3 for eight minutes, through fire and wolves; the game is saved and loaded; both run on for three minutes, and their save files must be the same.

amade-colony-game · test/domain/GameTest.java
    @Test
    void aSavedSettlementGoesOnExactlyAsBefore() {
        Game game = game(3, true);
        run(game, 8 * 60);
        Map<String, String> saved = Archive.write(game);
        Game loaded = Archive.read(saved, RULES, number -> Level.load(number, RULES));
        assertEquals(saved, Archive.write(loaded), "written again, the same");
        run(game, 3 * 60);
        run(loaded, 3 * 60);
        assertEquals(Archive.write(game), Archive.write(loaded), "and three minutes on, still the same");
    }

Step 11: Drawing the valley

The ground, summer and winter

The ground is drawn as in Sands: every pixel worked out from noise and the tiles of the map, in squares of 8 × 8 tiles, on other threads. A square keeps its ground without roads, and the roads and the border are mixed in again when they change: roads as trodden dirt, the land outside the border dimmed.

amade-colony-game · ui/game/Terrain.java
    /**
     * After every step: a square whose ground has changed is drawn anew, one whose roads or border have
     * changed is mixed anew. The same terrain serves every game on its map, so a new game is a change, too.
     */
    void update(Game game) {
        boolean fresh = game != this.game;
        this.game = game;
        boolean groundChanged = fresh || game.landVersion() != landVersion;
        boolean maskChanged = fresh || game.roads().version() != roadsVersion || game.land().version() != borderVersion;
        if (groundChanged) {
            landVersion = game.landVersion();
            byte[] now = grounds(game);
            groundChanged = !Arrays.equals(now, ground);
            ground = now;
        }
        if (maskChanged) {
            roadsVersion = game.roads().version();
            borderVersion = game.land().version();
            mask = mask(game);
        }
        for (Chunk chunk : chunks) {
            if (groundChanged) chunk.wantedGround = around(ground, chunk, AROUND);
            if (maskChanged) chunk.wantedMask = around(mask, chunk, 1);
            boolean idle = chunk.work == null || chunk.work.isDone();
            if (idle && (!Arrays.equals(chunk.drawn, chunk.wantedGround) || !Arrays.equals(chunk.mixed, chunk.wantedMask))) {
                schedule(chunk);
            }
        }
    }

New is winter. The slow drawing of the ground also works out how much snow each pixel takes: much on open grass, less under the trees and on steep rock, none on water. Mixing then makes two pictures of each square, summer and winter. At the end of autumn the winter picture fades in over the summer one, and in spring it fades out; a frame draws at most two pictures per square, and no pixel is worked out again.

amade-colony-game · ui/game/Terrain.java
    /**
     * The square's two pictures, summer and winter, with the roads and the border in {@code bits}: the
     * roads as trodden dirt, slush in winter, and the land outside the border dimmed.
     */
    private BufferedImage[] mix(Chunk chunk, byte[] bits) {
        int[] base = chunk.base;
        byte[] light = chunk.light;
        byte[] cover = chunk.cover;
        int width = Math.min(SIDE, (level.width() - chunk.col) * Field.TILE * PIXELS);
        int height = Math.min(SIDE, (level.height() - chunk.row) * Field.TILE * PIXELS);
        BufferedImage summer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        BufferedImage winter = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        int[] outSummer = ((DataBufferInt) summer.getRaster().getDataBuffer()).getData();
        int[] outWinter = ((DataBufferInt) winter.getRaster().getDataBuffer()).getData();
        float[] inside = new float[bits.length];
        for (int i = 0; i < bits.length; i++) inside[i] = (bits[i] & INSIDE) != 0 ? 1 : 0;
        double ox = chunk.col * Field.TILE;
        double oy = chunk.row * Field.TILE;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int i = y * SIDE + x;
                double lx = x / (double) PIXELS;
                double ly = y / (double) PIXELS;
                double l = (light[i] & 255) / 127.0;
                int c = base[i];
                int snow = Art.shade(Art.mix(0xF4F8FC, 0xC8D8EC, 0.25), Math.min(1.08, l * 0.95 + 0.1));
                int w = Art.mix(c, snow, (cover[i] & 255) / 255.0);
                double road = road(bits, lx, ly);
                if (road > 0.15) {
                    double wx = ox + lx;
                    double wy = oy + ly;
                    int dirt = Art.mix(0x8C6A42, 0xB08B58, Art.fbm(wx / 10.0, wy / 10.0, 13));
                    double grit = Art.hash(chunk.col * SIDE + x, chunk.row * SIDE + y, 27);
                    dirt = Art.mix(dirt, 0x5E4428, grit * 0.25);
                    if (grit > 0.97 && road > 0.6) dirt = 0x9C9282;
                    else if (grit < 0.03 && road > 0.6) dirt = 0x4E4234;
                    double part = (road - 0.15) / 0.4;
                    c = Art.mix(Art.mix(c, 0x5A4A2A, 0.4), Art.shade(dirt, l), part);
                    w = Art.mix(Art.mix(w, 0x6A6458, 0.3), Art.shade(Art.mix(dirt, 0xD8D8D4, 0.45), l), part);
                }
                double dim = Art.smooth(0.6, 0.35, field(inside, 1, lx + warpX(ox + lx, oy + ly) * 0.3,
                        ly + warpY(ox + lx, oy + ly) * 0.3)) * 0.5;
                if (dim > 0) {
                    c = Art.mix(c, OUTSIDE_COLOUR, dim);
                    w = Art.mix(w, OUTSIDE_COLOUR, dim);
                }
                outSummer[y * width + x] = c;
                outWinter[y * width + x] = w;
            }
        }
        return new BufferedImage[]{summer, winter};
    }
amade-colony-game · ui/game/Terrain.java
    /**
     * The squares the camera sees: as pictures when they are ready, else in the plain colours of their tiles;
     * the winter picture over the summer one as far as the snow has come.
     */
    void render(Graphics2D g, Camera camera, double alpha) {
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        double snow = game == null ? 0 : snow(game);
        Composite before = g.getComposite();
        for (Chunk chunk : chunks) {
            show(chunk);
            double x = chunk.col * Field.TILE;
            double y = chunk.row * Field.TILE;
            if (!camera.sees(x, y, SIZE, SIZE, alpha)) continue;
            if (chunk.summer == null) {
                plain(g, chunk, snow > 0.5);
                continue;
            }
            if (snow < 1) chunk.summer.drawSeamless(g, x, y);
            if (snow > 0) {
                if (snow < 1) g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) snow));
                chunk.winter.drawSeamless(g, x, y);
                g.setComposite(before);
            }
        }
    }

Houses and people

Fifteen buildings, each with its site, all drawn from vector shapes when the window opens: half-timbered walls, roofs of tiles, slate or thatch, the windmill whose sails turn while it grinds, the sheep farm with its fence. One method draws every house from its walls, roof and windows; the special ones add their parts:

amade-colony-game · ui/game/Houses.java
    /** The building of kind {@code id}, {@code width} × {@code height} tiles, without its shadow. */
    static void building(Graphics2D g, String id, int width, int height) {
        double door = door(width);
        switch (id) {
            case "storehouse" -> {
                house(g, 7, -10, 40, 40, 12, 0xC8C0AE, 0xA8482E, TILES, false, new double[]{0.18, 0.72, 0.9}, door);
                chimney(g, 7 + 40 * 0.74, -6);
                tower(g, 0, -12, 15, 11, 31, 0xA8482E);
                banner(g, 13, -12);
                crates(g, 32, 31);
            }
            case "woodcutter" -> {
                house(g, 4, -4, 24, 28, 9, 0xD8C8A6, 0x7A6A58, SHINGLES, true, new double[]{0.3}, door);
                logPile(g, -2, 17, 0.75);
            }
            case "forester" -> {
                house(g, 8, -4, 22, 28, 9, 0xE6D8BA, 0xC8A050, THATCH, true, new double[]{0.3}, door);
                for (int i = 0; i < 3; i++) {
                    double x = 1.5 + i * 2.5;
                    double y = 14 + i * 6;
                    g.setColor(new Color(0x5A4026));
                    g.fill(new Ellipse2D.Double(x - 2, y - 1, 4, 2));
                    Art.tree(g, x, y - 1, false, 0.3, false, false, 40 + i);
                }
            }
            case "quarry" -> {
                house(g, 8, -4, 22, 28, 9, 0xB8B2A2, 0x7A6A58, SHINGLES, false, new double[]{0.3}, door);
                blocks(g, -1, 17, 0.7);
            }
            case "sawmill" -> {
                house(g, 1, -6, 30, 34, 11, 0xE6D8BA, 0x8A7A66, SHINGLES, true, new double[]{0.15}, door);
                saw(g, 1 + 30 * 0.45, 28 - 11);
                planks(g, -3, 30, 0.8);
            }
            case "tower" -> {
                tower(g, 1, -22, 14, 26, 37, 0x7A5A3A);
                banner(g, 8, -24);
            }
            case "well" -> {
                AffineTransform at = g.getTransform();
                g.translate(8, 13);
                g.scale(0.8, 0.8);
                well(g, 0, 0);
                g.setTransform(at);
            }
            case "fisher" -> {
                house(g, 4, -2, 24, 26, 9, 0xD8C8A6, 0xB89048, THATCH, true, new double[]{0.3}, door);
                rack(g, -2, 26);
            }
            case "farm" -> {
                house(g, 4, -4, 40, 30, 10, 0xE6D8BA, 0xD0A858, THATCH, true, new double[]{0.18, 0.75}, door);
                hay(g, 44, 26);
            }
            case "windmill" -> windmill(g, 16, 24);
            case "bakery" -> {
                house(g, 10, -4, 22, 30, 10, 0xEADCC0, 0xB0583A, TILES, true, new double[]{0.25}, door);
                chimney(g, 10 + 22 * 0.2, -1);
                oven(g, 0, 27);
            }
            case "sheepfarm" -> {
                house(g, 35, 6, 27, 38, 10, 0xEADCC0, 0xB89048, THATCH, true, new double[]{0.7}, door);
                fence(g, 0, 0, 32, height * Field.TILE, true);
            }
            case "weaver" -> {
                house(g, 3, -4, 26, 30, 10, 0xE8DDC8, 0x6A5E7A, SHINGLES, true, new double[]{0.28}, door);
                clothLine(g, -3, 14);
            }
            case "toolmaker" -> {
                house(g, 3, -4, 26, 30, 10, 0xA8A090, 0x5A4A42, SHINGLES, false, new double[]{0.28}, door);
                chimney(g, 3 + 26 * 0.15, -2);
                anvil(g, 1, 30);
            }
            case "residence" -> {
                house(g, 2, -6, 28, 32, 12, 0xF0E4C8, 0xB8482E, TILES, true, new double[]{0.2, 0.5}, door);
                chimney(g, 2 + 28 * 0.3, -4);
                garden(g, -3, 22);
            }
            default -> house(g, 2, -4, width * Field.TILE - 4, height * Field.TILE + 2, 10, 0xD8C8A6, 0x8A7A66,
                    SHINGLES, true, new double[]{0.3}, door);
        }
    }

A settlement game is watched more than played, and people are what the eye follows. Folk gives every person a look: shirt, legs or skirt, skin, hair, a beard, an apron, and headwear by job: the carrier's cap, the woodcutter's knitted cap, the farmer's straw hat, the baker's white toque, the shepherd's felt hat. Every worker who goes out carries the tool of their trade: an axe, a spade, a pick, a rod, a scythe, a crook. Carriers get one of twelve looks by their number, so the crowd on the roads is not twelve copies of one person.

amade-colony-game · ui/game/Folk.java
    /** How a person looks: shirt, legs or skirt, skin, hair, headwear and its colour, an apron, a beard. */
    record Look(int shirt, int legs, int skin, int hair, Hat hat, int hatColour, boolean skirt, int apron, boolean beard) {
    }

    static final int[] SKIN = {0xF0C8A0, 0xE0A880, 0xC08860, 0x8A5A3A};
    static final int[] HAIR = {0x3A2414, 0x6A4424, 0xC89850, 0x1A1410, 0x9A9890, 0xA0482A};
    static final int CARRIER_LOOKS = 12;

    /** The carriers: everyday clothes in many colours; the blue cap or scarf shows they work for the colony. */
    static Look carrier(int i) {
        int[] shirts = {0xB8A27A, 0x8A5A3A, 0xA8B4A0, 0xC88A4A, 0x7A6A9A, 0x9AA27A, 0xD8C8A0, 0x6A7A8A, 0xB86A5A};
        int[] legs = {0x5A4A3A, 0x4A4E5A, 0x6A5638, 0x3A3A3A};
        boolean skirt = i % 3 == 1;
        int blue = Art.BLUE[1].getRGB() & 0xFFFFFF;
        return new Look(shirts[(i * 5) % shirts.length], legs[i % legs.length], SKIN[(i * 3) % SKIN.length],
                HAIR[(i * 7) % HAIR.length], skirt ? Hat.SCARF : i % 4 == 3 ? Hat.NONE : Hat.CAP, blue, skirt,
                skirt && i % 2 == 0 ? 0xE8E0CC : 0, !skirt && i % 5 == 2);
    }

    /** A builder: a leather apron, sleeves rolled up. */
    static Look builder(int i) {
        return new Look(0xC86A3A, 0x4A4E5A, SKIN[i % SKIN.length], HAIR[(i * 5) % HAIR.length], Hat.NONE, 0, false,
                0x9A7A50, i % 2 == 0);
    }

    /** The worker of a building, by what the building does and makes. */
    static Look worker(Work work, Good makes, int i) {
        int skin = SKIN[i % SKIN.length];
        int hair = HAIR[(i * 5 + 1) % HAIR.length];
        return switch (work) {
            case FELL -> new Look(0xA83A2A, 0x4A3A2A, skin, hair, Hat.KNIT, 0x6A2A1A, false, 0, true);
            case PLANT -> new Look(0x4E7A3A, 0x5A4A3A, skin, hair, Hat.HOOD, 0x3A6A2E, false, 0, false);
            case QUARRY -> new Look(0x8A8A82, 0x4A4038, skin, hair, Hat.CAP, 0x6A6A6A, false, 0x7A5A3A, false);
            case FISH -> new Look(0x4A6A8A, 0x3A3A3A, skin, hair, Hat.FELT, 0xD8B040, false, 0, true);
            case FARM -> new Look(0xF2ECDC, 0x8A4A3A, skin, hair, Hat.STRAW, 0xE8C878, true, 0xD8C8A0, false);
            case GRAZE -> new Look(0x7A5A3A, 0x4A4038, skin, hair, Hat.FELT, 0x5A4632, false, 0, true);
            case WATCH -> new Look(0x3E5A8A, 0x3A3A3A, skin, hair, Hat.HOOD, 0x2A3E64, false, 0, false);
            default -> switch (makes == null ? Good.WATER : makes) {
                case PLANK -> new Look(0xC89A5A, 0x4A4038, skin, hair, Hat.CAP, 0x7A5A36, false, 0x8A6A40, false);
                case FLOUR -> new Look(0xF0ECE0, 0x8A8070, skin, hair, Hat.CAP, 0xF0ECE0, false, 0, false);
                case BREAD -> new Look(0xF2EEE4, 0x6A5A4A, skin, hair, Hat.TOQUE, 0xFFFFFF, false, 0xF8F4EA, false);
                case CLOTH -> new Look(0x8A5A9A, 0x5A4A6A, skin, hair, Hat.SCARF, 0xD8A040, true, 0, false);
                case TOOLS -> new Look(0x6A6A6A, 0x3A3A3A, skin, hair, Hat.NONE, 0, false, 0x5A3A22, true);
                default -> new Look(0x6A8AA8, 0x5A4A3A, skin, hair, Hat.SCARF, 0x4A7AA8, true, 0xE8E0CC, false);
            };
        };
    }
amade-colony-game · ui/game/Pictures.java
final class Pictures {
    /** A tree is drawn at four sizes as it grows, in three shapes of each, for summer and winter, inside and outside. */
    static final double[] TREE_SIZES = {0.38, 0.55, 0.75, 1.0};
    static final int VARIANTS = 3;
    /** A field at its four stages: just sown, sprouting, green, ripe. */
    static final int STAGES = 4;
    /** The windmill's sails in this many turns of a quarter circle, and the flames' flicker. */
    static final int SAILS = 32;
    static final int FLAMES = 6;
    static final double[] PUFFS = {2, 3, 4, 5.5, 7};
    /** A person's picture, wider with a tool; their feet in the middle, {@link #FEET_Y} from the top. */
    static final double PERSON_WIDTH = 16;
    static final double WORKER_WIDTH = 46;
    static final double PERSON_HEIGHT = 30;
    static final double FEET_Y = 24;

    /** One pose of a person: their look, the load held up, from behind, the step, mirrored, the tool and its swing. */
    record Pose(Folk.Look look, boolean armsUp, boolean back, int step, boolean left, Folk.Tool tool, int frame) {
    }

    private final Map<String, Sprite> buildings = new HashMap<>();
    private final Map<String, Sprite> yards = new HashMap<>();
    private final Map<String, Sprite> shadows = new HashMap<>();
    private final Map<String, Sprite> sites = new HashMap<>();
    private final Map<String, Sprite> icons = new HashMap<>();
    private final Map<Good, Sprite> goods = new EnumMap<>(Good.class);
    private final Map<Good, Sprite> bigGoods = new EnumMap<>(Good.class);
    private final Map<Pose, Sprite> people = new HashMap<>();
    /** By conifer, size, winter, outside and shape. */
    private final Sprite[][][][][] trees = new Sprite[2][TREE_SIZES.length][2][2][VARIANTS];
    private final Sprite[] treeShadows = new Sprite[TREE_SIZES.length];
    /** By stage, winter, outside and shape. */
    private final Sprite[][][][] fields = new Sprite[STAGES][2][2][2];
    /** By stone left, 1 to {@link Field#STONE}, winter and outside. */
    private final Sprite[][][] boulders = new Sprite[Field.STONE + 1][2][2];
    final Rotations sails = Rotations.of(SAILS, 44, (g, w, h) -> {
        g.translate(w / 2, h / 2);
        Houses.sails(g);
    });
    final Rotations sailsShadow = Rotations.of(SAILS, 44, (g, w, h) -> {
        g.translate(w / 2, h / 2);
        Houses.sailsShadow(g);
    });
    final Sprite flag = Sprite.drawn(12, 14, (g, w, h) -> {
        Art.smooth(g);
        Art.flag(g, 3, 12);
    });
    final Sprite post = Sprite.drawn(6, 8, (g, w, h) -> {
        Art.smooth(g);
        Art.borderPost(g, 2, 7);
    });
    final Sprite stump = Sprite.drawn(8, 8, (g, w, h) -> {
        Art.smooth(g);
        Art.stump(g, 4, 4);
    });
    final Sprite[] ashes = new Sprite[3];
    /** The sheep facing right and left, in summer and winter; the dog; the wolf at two strides. */
    final Sprite[][] sheep = new Sprite[2][2];
    final Sprite[] dog = new Sprite[2];
    final Sprite[][] wolf = new Sprite[2][2];
    final Sprite[] flames = new Sprite[FLAMES];
    final Sprite[] puffs = new Sprite[PUFFS.length];
    final Sprite[] darkPuffs = new Sprite[PUFFS.length];
    final Sprite[] seasons = new Sprite[4];
    final Sprite[] markers = new Sprite[2];

The settlement is drawn in layers: the ground; then everything that stands, sorted by its foot, so that a person behind a house is hidden by it and one in front of it is not; then smoke and sparks; then the marks of what the player is doing.

amade-colony-game · ui/game/ColonyPainter.java
    /** The settlement as the camera sees it, and the marks of what the player is doing. */
    void paint(Graphics2D g, Game game, Terrain terrain, Camera camera, Effects effects, Marks marks, double alpha) {
        Shape clip = g.getClip();
        AffineTransform screen = g.getTransform();
        g.clip(new Rectangle2D.Double(camera.screenLeft(), camera.screenTop(), camera.viewWidth(), camera.viewHeight()));
        g.translate(camera.screenLeft() - camera.x(alpha), camera.screenTop() - camera.y(alpha));
        int fromCol = Math.max(0, (int) Math.floor(camera.x(alpha) / Field.TILE) - 2);
        int fromRow = Math.max(0, (int) Math.floor(camera.y(alpha) / Field.TILE) - 1);
        int toCol = Math.min(game.level().width() - 1, (int) ((camera.x(alpha) + camera.viewWidth()) / Field.TILE) + 2);
        int toRow = Math.min(game.level().height() - 1, (int) ((camera.y(alpha) + camera.viewHeight()) / Field.TILE) + 4);
        boolean winter = Terrain.snow(game) > 0.5;
        double time = effects.time();

        terrain.render(g, camera, alpha);
        ground(g, game, camera, alpha, fromCol, fromRow, toCol, toRow, winter);
        effects.renderGround(g, camera, alpha);
        shadows(g, game, camera, alpha, fromCol, fromRow, toCol, toRow, time);
        things.clear();
        collect(game, camera, alpha, fromCol, fromRow, toCol, toRow);
        things.sort(Comparator.comparingDouble(Thing::y).thenComparingDouble(Thing::x));
        for (Thing thing : things) draw(g, game, thing, winter, time);
        effects.render(g, alpha);
        marks(g, game, marks, camera, alpha, fromCol, fromRow, toCol, toRow);
        g.setTransform(screen);
        g.setClip(clip);
    }

Step 12: Everything else

  • Events. The rules say what happened as records, as since chapter 26: built, made, settled, a season began, a house burning, doused, burned, wolves, barked, a sheep taken, a lamb born, a road paved. Effects turns them into smoke, sparks, snowflakes and sounds; the message line at the bottom of the map shows the ones the player must know about.
  • The minimap. The engine's Minimap (chapter 31): one pixel per tile, the roads, the houses and the border on it, white in winter. It is painted again every half second, and only when something changed.
  • Wide screens. As in Sands, the canvas widens up to 1024 units, the valley gets the extra width, and the panel is drawn moved to the right edge, with the mouse moved back for its clicks (chapter 31). The goods bar along the top grows with the screen; its background is a cached face, kept once per width.
  • Music. An easy tune in MML in three-four time over a walking bass, and short ones for winning and losing.
  • Scenes. Title (over a settlement the computer builds), the choice of a mission (each card with a preview of its map), the mission, the pause (continue, save, load, leave), the result with the settlers, the houses built, the goods made, the road laid, and what fire and wolves took.

The title over the computer's settlement The choice of a mission

The title (left), over a settlement the computer builds, and the choice of a mission (right): each with its goal, its years, and whether there are fires and wolves.
amade-colony-game · ui/game/Effects.java
    /** Shows what happened in the last step, and plays it if {@code audible}, as far as {@code camera} sees it. */
    void react(Game game, Camera camera, boolean audible) {
        for (Event event : game.events()) {
            switch (event) {
                case Event.Founded(Building b) -> {
                    dust(b, 6);
                    if (audible) play(sounds.founded);
                }
                case Event.Built(Building b) -> {
                    dust(b, 14);
                    if (heard(camera, audible, b.x(), b.y())) play(sounds.built);
                }
                case Event.Paved paved -> {
                    if (audible && !game.computer()) play(sounds.paved);
                }
                case Event.Felled(int col, int row) -> {
                    double x = (col + 0.5) * Field.TILE;
                    double y = (row + 0.5) * Field.TILE;
                    stumps.add(new Stump(x, y + 3, time));
                    sparkle(x, y, 8, CHIPS, 30, 0);
                    if (heard(camera, audible, x, y)) play(sounds.felled);
                }
                case Event.Settled(Building b) -> {
                    say(b.x(), b.y() - 20, "+1", Hud.GREEN);
                    if (heard(camera, audible, b.x(), b.y())) play(sounds.settled);
                }
                case Event.Season season -> {
                    if (audible) play(season.season() == 3 ? sounds.winter : sounds.season);
                }
                case Event.Burning(Building b) -> {
                    if (audible) play(sounds.alarm);
                }
                case Event.Doused(Building b) -> {
                    sparkle(b.x(), b.y() - 6, 20, WATER, 30, 0);
                    puff(b.x(), b.y() - 10, 3, false);
                    if (audible) play(sounds.doused);
                }
                case Event.Burned(Building b) -> {
                    dust(b, 24);
                    for (int i = 0; i < 4; i++) puff(b.x() + (random.nextDouble() - 0.5) * 20, b.y(), 4, true);
                    if (audible) play(sounds.collapse);
                }
                case Event.Demolished(Building b) -> {
                    dust(b, 20);
                    if (audible) play(sounds.collapse);
                }
                case Event.Wolf wolf -> {
                    if (audible) play(sounds.howl);
                }
                case Event.Barked(Building farm, boolean horn) -> {
                    if (audible) play(horn ? sounds.horn : sounds.bark);
                }
                case Event.SheepTaken(Building farm) -> {
                    double[] pen = farm.pasture();
                    sparkle(pen[0] + pen[2] / 2, pen[1] + pen[3] / 2, 16, WOOL, 26, 0);
                    say(pen[0] + pen[2] / 2, pen[1], "-1", Hud.RED);
                    if (audible) play(sounds.bleat);
                }
                case Event.Lamb(Building farm) -> {
                    if (heard(camera, audible, farm.x(), farm.y())) play(sounds.lamb);
                }
                case Event.Over(boolean won) -> {
                    if (audible && !game.computer()) mixer.playMusic(won ? music.won : music.lost, false);
                }
                case Event.Made made -> {
                }
                case Event.NoTools noTools -> {
                }
            }
        }
    }

What moved into the engine

Package What For
road RoadNetwork flags and the roads between them, split by a new flag, the next road on the shortest way
economy Stock, Recipe goods counted by kind with limits, what a house makes of them, with alternatives
time Seasons a year of seasons of any length, which one began
tile Territory land claimed in circles, overlapping, given back
motion Wander something that grazes around a home and flees

None of them knows of sheep or planks. The city builder that came next, Town (chapter 33), needed other parts after all: its services reach in circles that overlap and add up, which Territory does not count, so it got a Coverage of its own.

Exercises

  1. Donkeys. On a busy road, the carrier is the bottleneck. Give a road a second carrier, a donkey bred on a farm, when more than four wares wait at its flags. What does the carrier's state machine need to know about the other one?
  2. Priorities. Every site is served in turn. Let the player give one building priority: its demand is answered first. Where in dispatch does that go?
  3. Roads that wear in. A road that carries much becomes a paved road, and carriers walk it faster. Count the wares per road. What changes in RoadNetwork, and what only in the game?
  4. A second storehouse. The game already sends goods to the nearest storehouse. Add it to the panel, and watch the wares stop crossing the whole map. Which goods should go to which store?
  5. Trade. A market at the edge of the map buys cloth and sells tools. Is it a building with a recipe, or something else?
  6. Seasons in Stock. Bread keeps, fish does not. Let fish spoil in summer: one fish less in every store each minute. Is that a job for Stock, for Seasons, or for the game?