Back to Squad Table
SOURCE / PINNED RELEASE

Made of little things.

Squad Table

Release
d36201f8ed4f…
Author-recorded commit
7b9bb681d6ba…
License
LICENSE
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-c043b0c0f1d

Archive hash verified: 8c0604b31f00cc5a…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/scenarios.ts
// Original maps and scenarios. Terrain is shown for reference; the players
// decide what it means under the rules they are playing.

import type { NationId } from './catalog.js';
import { hexId, parseHex, type Hex } from './hex.js';

export type Terrain = 'open' | 'woods' | 'brush' | 'grain' | 'orchard' | 'wood-bldg' | 'stone-bldg' | 'marsh' | 'water';

export const TERRAIN_NAMES: Record<Terrain, string> = {
  open: 'Open ground',
  woods: 'Woods',
  brush: 'Brush',
  grain: 'Grain',
  orchard: 'Orchard',
  'wood-bldg': 'Wooden building',
  'stone-bldg': 'Stone building',
  marsh: 'Marsh',
  water: 'Water',
};

const CODES: Record<string, Terrain> = {
  '.': 'open',
  w: 'woods',
  h: 'brush',
  g: 'grain',
  o: 'orchard',
  b: 'wood-bldg',
  s: 'stone-bldg',
  m: 'marsh',
  '~': 'water',
};

export type MapCell = { id: string; hex: Hex; terrain: Terrain; level: number; road: boolean };
export type Hexside = { a: string; b: string; kind: 'hedge' | 'wall' };

export type GameMap = {
  id: string;
  name: string;
  cols: number;
  rows: number;
  cells: Map<string, MapCell>;
  roads: string[][];
  hexsides: Hexside[];
};

type MapSource = {
  id: string;
  name: string;
  terrain: string[];
  elevation: string[];
  roads: string[][];
  hedges?: [string, string][];
  walls?: [string, string][];
};

function buildMap(src: MapSource): GameMap {
  const rows = src.terrain.length;
  const cols = src.terrain[0].length;
  const cells = new Map<string, MapCell>();
  const roadHexes = new Set(src.roads.flat());
  for (let r = 1; r <= rows; r += 1) {
    for (let c = 0; c < cols; c += 1) {
      const hex = { c, r };
      const id = hexId(hex);
      const road = roadHexes.has(id);
      const code = src.terrain[r - 1][c];
      cells.set(id, {
        id,
        hex,
        terrain: road ? 'open' : (CODES[code] ?? 'open'),
        level: Number(src.elevation[r - 1]?.[c] ?? 0) || 0,
        road,
      });
    }
  }
  const hexsides: Hexside[] = [
    ...(src.hedges ?? []).map(([a, b]) => ({ a, b, kind: 'hedge' as const })),
    ...(src.walls ?? []).map(([a, b]) => ({ a, b, kind: 'wall' as const })),
  ];
  return { id: src.id, name: src.name, cols, rows, cells, roads: src.roads, hexsides };
}

const row = (r: number, from: string, to: string): string[] => {
  const a = parseHex(`${from}${r}`)!;
  const b = parseHex(`${to}${r}`)!;
  const out: string[] = [];
  for (let c = a.c; c <= b.c; c += 1) out.push(hexId({ c, r }));
  return out;
};
const column = (col: string, from: number, to: number): string[] =>
  Array.from({ length: to - from + 1 }, (_, i) => `${col}${from + i}`);

export const MAPS: Record<string, GameMap> = {
  millbrook: buildMap({
    id: 'millbrook',
    name: 'Millbrook crossroads',
    terrain: [
      'www..ggg.....oo...ww..',
      'wwh..ggg..b..oo..www..',
      'wh.........b.....ww..h',
      '.....h...bs.sb........',
      '......................',
      '...gg....bs..sb....oo.',
      '...gg.....b..b....h...',
      'ww.......h......gg...w',
      'www..mm~~.......gg..ww',
      'wwww.mm~~~..h......www',
    ],
    elevation: [
      '0000000000000000122221',
      '0000000000000000122221',
      '0000000000000000011211',
      '0000000000000000001110',
      '0000000000000000000000',
      '0000000000000000000000',
      '0000000000000000000000',
      '0000000000000000000000',
      '0000000000000000000000',
      '0000000000000000000000',
    ],
    roads: [row(5, 'A', 'V'), column('L', 1, 10)],
    hedges: [
      ['C6', 'D6'],
      ['C7', 'D7'],
      ['D5', 'D6'],
      ['E6', 'F6'],
      ['P8', 'Q8'],
      ['P9', 'Q9'],
    ],
    walls: [
      ['J4', 'J5'],
      ['K4', 'K5'],
      ['M4', 'M5'],
      ['N4', 'N5'],
      ['S5', 'S6'],
      ['T5', 'T6'],
    ],
  }),
};

export type Placement = { k: string; at: string; count?: number };

export type ScenarioSide = {
  nat: NationId;
  name: string;
  setup: string; // free-text setup instructions
  entry?: 'west' | 'east' | 'north' | 'south'; // map edge for units entering from off-board
  units: Placement[]; // `at` may be a hex id or "pool" (off-board, ready to enter)
};

export type Scenario = {
  id: string;
  name: string;
  map: string;
  turns: number;
  brief: string;
  victory: string;
  first: 'a' | 'b'; // side that moves first
  sides: { a: ScenarioSide; b: ScenarioSide };
};

export const SCENARIOS: Scenario[] = [
  {
    id: 'millbrook-dawn',
    name: 'Dawn at Millbrook',
    map: 'millbrook',
    turns: 6,
    brief:
      'A reinforced American company pushes east along the valley road. A thin German screen holds the stone houses at the crossroads.',
    victory: 'Americans win at game end if they hold more stone-building hexes than the Germans. Otherwise the Germans win.',
    first: 'a',
    sides: {
      a: {
        nat: 'us',
        name: 'American',
        setup: 'Enter on turn 1 along the west edge (column A).',
        entry: 'west',
        units: [
          { k: 'us-666', at: 'pool', count: 6 },
          { k: 'us-747', at: 'pool', count: 2 },
          { k: 'us-ldr91', at: 'pool' },
          { k: 'us-ldr80', at: 'pool' },
          { k: 'us-ldr70', at: 'pool' },
          { k: 'us-mmg', at: 'pool' },
          { k: 'us-lmg', at: 'pool', count: 2 },
          { k: 'us-baz', at: 'pool' },
        ],
      },
      b: {
        nat: 'ge',
        name: 'German',
        setup: 'Set up first, east of column H. Units start where shown; adjust before turn 1 by agreement.',
        units: [
          { k: 'ge-467', at: 'K4' },
          { k: 'ge-467', at: 'N6' },
          { k: 'ge-467', at: 'K6' },
          { k: 'ge-447', at: 'M4' },
          { k: 'ge-447', at: 'L7' },
          { k: 'ge-ldr81', at: 'K6' },
          { k: 'ge-ldr80', at: 'N6' },
          { k: 'ge-mmg', at: 'K6' },
          { k: 'ge-lmg', at: 'K4' },
          { k: 'ge-pf', at: 'N6' },
          { k: 'mk-conceal', at: 'M4' },
          { k: 'mk-conceal', at: 'L7' },
        ],
      },
    },
  },
  {
    id: 'hill-stand',
    name: 'Stand on the High Ground',
    map: 'millbrook',
    turns: 5,
    brief: 'Russian guards storm the wooded hill east of Millbrook, held by German riflemen with machine-gun support.',
    victory: 'Russians win if they control every level-2 hex at game end.',
    first: 'a',
    sides: {
      a: {
        nat: 'ru',
        name: 'Russian',
        setup: 'Set up second, west of column P.',
        units: [
          { k: 'ru-628', at: 'O2', count: 2 },
          { k: 'ru-628', at: 'O4', count: 2 },
          { k: 'ru-447', at: 'N6', count: 2 },
          { k: 'ru-447', at: 'N3', count: 2 },
          { k: 'ru-ldr91', at: 'O2' },
          { k: 'ru-ldr80', at: 'N6' },
          { k: 'ru-lmg', at: 'O4' },
          { k: 'ru-mtr', at: 'N3' },
        ],
      },
      b: {
        nat: 'ge',
        name: 'German',
        setup: 'Set up first on or east of column R.',
        units: [
          { k: 'ge-467', at: 'S2' },
          { k: 'ge-467', at: 'T3' },
          { k: 'ge-447', at: 'U2' },
          { k: 'ge-ldr91', at: 'S2' },
          { k: 'ge-hmg', at: 'S2' },
          { k: 'ge-lmg', at: 'T3' },
          { k: 'mk-fox', at: 'S2' },
          { k: 'mk-fox', at: 'T3' },
        ],
      },
    },
  },
  {
    id: 'open-table',
    name: 'Open table',
    map: 'millbrook',
    turns: 8,
    brief: 'An empty map. Add counters from the tray to set up your own scenario or try a situation from your rulebook.',
    victory: 'Agree on victory conditions before you start.',
    first: 'a',
    sides: {
      a: { nat: 'us', name: 'American', setup: 'Free setup.', units: [] },
      b: { nat: 'ge', name: 'German', setup: 'Free setup.', units: [] },
    },
  },
];

export const SCENARIO_BY_ID = new Map(SCENARIOS.map((s) => [s.id, s]));

export const PHASES = [
  'Rally',
  'Prep Fire',
  'Movement',
  'Defensive Fire',
  'Advancing Fire',
  'Rout',
  'Advance',
  'Close Combat',
] as const;