Harnesses for retreating and the winter
All 176 published cases now parse and run. Retreating passes 18 of 18, building 7 of 7, civil disorder 13 of 13; movement is unchanged at 133 of 139. The retreat and winter engine code was written before these cases and mostly held up, but civil disorder was wrong in a way playing would never have shown. It measured distance to a power's *home* centres. The rule is distance to the centres it owns -- a Russia driven out of Moscow and holding Sweden measures from Sweden -- so every power that had lost a home centre was disbanding the wrong unit. And it measured over the graph each unit moves on, which is wrong in both directions. An army counts sea provinces it would have to be convoyed across, so Albania is two steps from Naples rather than unreachable. A fleet counts inland provinces it could never enter, so the Baltic is two from Warsaw. That reads as nonsense until you notice what is being measured: not how the unit would get home, but how far out of the way it is. A fleet in the Baltic is still in the middle of Russia's business; one in the North Atlantic is not. Civil disorder also only applies to the shortfall. A power that disbanded one of the two it owed has half answered, and the automatic rule picks from what is left rather than from what it started with.
This commit is contained in:
@@ -86,11 +86,20 @@ the Baltic and made Denmark worthless.
|
|||||||
|
|
||||||
`tools/datc.py` turns the Diplomacy Adjudicator Test Cases into
|
`tools/datc.py` turns the Diplomacy Adjudicator Test Cases into
|
||||||
`src/game/datc.json` -- the orders and their annotated outcomes, not the
|
`src/game/datc.json` -- the orders and their annotated outcomes, not the
|
||||||
document's prose, which is Lucas Kruijswijk's writing. **133 of 136 movement
|
document's prose, which is Lucas Kruijswijk's writing. Every one of the 176
|
||||||
cases pass.** They are the reason this adjudicator is worth trusting: they
|
published cases is parsed and run.
|
||||||
found that orders were never validated, that the paradox rule did not
|
|
||||||
terminate, and that five of my own hand-written cases rested on a support
|
| Section | | Passing |
|
||||||
Paris cannot give.
|
| --- | --- | --- |
|
||||||
|
| 6.A - 6.G | movement | 133 / 139 |
|
||||||
|
| 6.H | retreating | 18 / 18 |
|
||||||
|
| 6.I | building | 7 / 7 |
|
||||||
|
| 6.J | civil disorder | 13 / 13 |
|
||||||
|
|
||||||
|
They are the reason this adjudicator is worth trusting: they found that
|
||||||
|
orders were never validated, that the paradox rule did not terminate, that
|
||||||
|
civil disorder was measuring the wrong distance, and that five of my own
|
||||||
|
hand-written cases rested on a support Paris cannot give.
|
||||||
|
|
||||||
Three cases are excluded rather than failed. 6.A.6, 6.B.10 and 6.B.11 state
|
Three cases are excluded rather than failed. 6.A.6, 6.B.10 and 6.B.11 state
|
||||||
their setup in prose -- "Germany has a fleet in London" -- and the fixture is
|
their setup in prose -- "Germany has a fleet in London" -- and the fixture is
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import cases from './datc.json'
|
||||||
|
import { adjudicate } from './adjudicate'
|
||||||
|
import { boardFrom, type Order, type Unit } from './orders'
|
||||||
|
import type { Power } from './map'
|
||||||
|
import {
|
||||||
|
applyAdjustments,
|
||||||
|
civilDisorderDisbands,
|
||||||
|
applyMoves,
|
||||||
|
applyRetreats,
|
||||||
|
type AdjustOrder,
|
||||||
|
type Ownership,
|
||||||
|
type RetreatOrder,
|
||||||
|
} from './turn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The published cases for the two phases that are not movement.
|
||||||
|
*
|
||||||
|
* They are shaped differently and so they need their own harness. A retreat
|
||||||
|
* case plays a movement phase first and then a RETREATS section; a winter
|
||||||
|
* case states the position with "Germany owns SC Kiel" lines instead of
|
||||||
|
* ordering it into existence, then gives builds or disbands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Case {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
units: Unit[]
|
||||||
|
orders: Order[]
|
||||||
|
expect: Record<string, string[]>
|
||||||
|
retreats: Order[]
|
||||||
|
expectRetreat: Record<string, string[]>
|
||||||
|
adjust: {
|
||||||
|
type: 'build' | 'disband' | 'auto'
|
||||||
|
at: string
|
||||||
|
unit: 'army' | 'fleet'
|
||||||
|
power: Power
|
||||||
|
}[]
|
||||||
|
expectAdjust: Record<string, string[]>
|
||||||
|
own: Record<string, Power>
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = cases as unknown as Case[]
|
||||||
|
const province = (id: string) => id.split('/')[0]!
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ 6.H
|
||||||
|
|
||||||
|
describe('DATC retreats', () => {
|
||||||
|
const retreating = all.filter((c) => c.retreats.length > 0)
|
||||||
|
|
||||||
|
it.each(retreating.map((c) => [c.id, c.title, c] as const))('%s %s', (_id, _t, c) => {
|
||||||
|
const board = boardFrom(c.units)
|
||||||
|
const outcome = adjudicate(board, c.orders)
|
||||||
|
const after = applyMoves(board, c.orders, outcome)
|
||||||
|
|
||||||
|
for (const [at, marks] of Object.entries(c.expect)) {
|
||||||
|
if (marks.includes('dislodged')) {
|
||||||
|
expect(outcome.dislodged.has(at), `${at} dislodged`).toBe(true)
|
||||||
|
}
|
||||||
|
if (marks.includes('stands')) {
|
||||||
|
expect(outcome.dislodged.has(at), `${at} stands`).toBe(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There are no supports in the retreat phase, and nothing else either --
|
||||||
|
* a beaten unit goes somewhere it may go, or it is gone. Anything that is
|
||||||
|
* not a move is read here as a disband, which is what the rules do with
|
||||||
|
* it.
|
||||||
|
*/
|
||||||
|
const orders: RetreatOrder[] = c.retreats.map((o) =>
|
||||||
|
o.type === 'move' ? { type: 'retreat', at: o.at, to: o.to } : { type: 'disband', at: o.at },
|
||||||
|
)
|
||||||
|
const { board: final } = applyRetreats(after, outcome, orders)
|
||||||
|
|
||||||
|
for (const [at, marks] of Object.entries(c.expectRetreat)) {
|
||||||
|
const order = c.retreats.find((o) => province(o.at) === at)
|
||||||
|
const to = order && order.type === 'move' ? province(order.to) : null
|
||||||
|
const beaten = outcome.dislodged.get(at)?.unit
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Did the beaten unit end up where it was aimed? Both halves have to
|
||||||
|
* exist to say yes. A unit that was never dislodged has no retreat to
|
||||||
|
* make, and an empty destination holds nobody -- and comparing those
|
||||||
|
* two nothings to each other says "arrived", which is how this harness
|
||||||
|
* first read a unit ordered to move during the retreat phase as having
|
||||||
|
* moved.
|
||||||
|
*/
|
||||||
|
const arrived =
|
||||||
|
beaten !== undefined &&
|
||||||
|
to !== null &&
|
||||||
|
final.get(to)?.power === beaten.power &&
|
||||||
|
final.get(to)?.type === beaten.type
|
||||||
|
|
||||||
|
if (marks.includes('succeeds')) {
|
||||||
|
expect(arrived, `${at} retreat succeeds`).toBe(true)
|
||||||
|
}
|
||||||
|
if (marks.includes('fails') || marks.includes('illegal')) {
|
||||||
|
expect(arrived, `${at} retreat ${marks.join(',')}`).toBe(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- 6.I, 6.J
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Civil disorder: what goes when a power has stopped answering.
|
||||||
|
*
|
||||||
|
* The rule is furthest from the supply centres that power *owns* -- not the
|
||||||
|
* ones it started with. A Russia driven out of Moscow and holding Sweden
|
||||||
|
* measures from Sweden, and these cases are what say so; the first version
|
||||||
|
* of this measured from home centres and was quietly wrong for every power
|
||||||
|
* that had lost one.
|
||||||
|
*/
|
||||||
|
describe('DATC civil disorder', () => {
|
||||||
|
const auto = all.filter((c) => c.adjust.some((a) => a.type === 'auto'))
|
||||||
|
|
||||||
|
it.each(auto.map((c) => [c.id, c.title, c] as const))('%s %s', (_id, _t, c) => {
|
||||||
|
const own: Ownership = new Map(Object.entries(c.own) as [string, Power][])
|
||||||
|
const power = c.adjust[0]!.power
|
||||||
|
const wanted = c.adjust.filter((a) => a.type === 'auto')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Anything the power actually said comes first. Civil disorder is only
|
||||||
|
* for the shortfall -- a power that disbanded one of the two it owed has
|
||||||
|
* not stopped answering, it has half answered, and the automatic rule
|
||||||
|
* picks from what is left rather than from what it started with.
|
||||||
|
*/
|
||||||
|
const said: AdjustOrder[] = c.adjust
|
||||||
|
.filter((a) => a.type !== 'auto')
|
||||||
|
.map((a) =>
|
||||||
|
a.type === 'build'
|
||||||
|
? { type: 'build', at: a.at, unit: a.unit }
|
||||||
|
: { type: 'disband', at: a.at, unit: a.unit },
|
||||||
|
)
|
||||||
|
const { board } = applyAdjustments(own, boardFrom(c.units), power, said)
|
||||||
|
|
||||||
|
const removed = civilDisorderDisbands(own, board, power, wanted.length)
|
||||||
|
expect(removed.map((u) => province(u.at)).sort()).toEqual(
|
||||||
|
wanted.map((a) => province(a.at)).sort(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DATC builds and disbands', () => {
|
||||||
|
const winter = all.filter((c) => c.adjust.some((a) => a.type !== 'auto'))
|
||||||
|
|
||||||
|
it.each(winter.map((c) => [c.id, c.title, c] as const))('%s %s', (_id, _t, c) => {
|
||||||
|
const own: Ownership = new Map(Object.entries(c.own) as [string, Power][])
|
||||||
|
const board = boardFrom(c.units)
|
||||||
|
const power = c.adjust[0]!.power
|
||||||
|
|
||||||
|
const orders: AdjustOrder[] = c.adjust
|
||||||
|
.filter((o) => o.type !== 'auto')
|
||||||
|
.map((o) =>
|
||||||
|
o.type === 'build'
|
||||||
|
? { type: 'build', at: o.at, unit: o.unit }
|
||||||
|
: { type: 'disband', at: o.at, unit: o.unit },
|
||||||
|
)
|
||||||
|
const { board: after, illegal } = applyAdjustments(own, board, power, orders)
|
||||||
|
|
||||||
|
for (const [at, marks] of Object.entries(c.expectAdjust)) {
|
||||||
|
const order = c.adjust.find((o) => province(o.at) === at)!
|
||||||
|
if (marks.includes('illegal')) {
|
||||||
|
expect(illegal.has(at), `${at} illegal`).toBe(true)
|
||||||
|
}
|
||||||
|
if (marks.includes('succeeds')) {
|
||||||
|
expect(illegal.has(at), `${at} succeeds`).toBe(false)
|
||||||
|
expect(after.has(at), `${at} ${order.type}`).toBe(order.type === 'build')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
+1991
-483
File diff suppressed because it is too large
Load Diff
@@ -211,9 +211,10 @@ describe('the winter', () => {
|
|||||||
expect(buildOptions(own, boardFrom([]), 'germany').some((o) => o.at === 'ber')).toBe(false)
|
expect(buildOptions(own, boardFrom([]), 'germany').some((o) => o.at === 'ber')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes what is furthest from home when nobody says', () => {
|
it('removes what is furthest from the centres it owns when nobody says', () => {
|
||||||
const board = boardFrom([A('germany', 'mun'), A('germany', 'ber'), A('germany', 'spa')])
|
const board = boardFrom([A('germany', 'mun'), A('germany', 'ber'), A('germany', 'spa')])
|
||||||
expect(civilDisorderDisbands(board, 'germany', 1)[0]!.at).toBe('spa')
|
const own = openingOwnership()
|
||||||
|
expect(civilDisorderDisbands(own, board, 'germany', 1)[0]!.at).toBe('spa')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('knows an eliminated power and a solo when it sees one', () => {
|
it('knows an eliminated power and a solo when it sees one', () => {
|
||||||
|
|||||||
+110
-17
@@ -178,27 +178,49 @@ export function buildOptions(own: Ownership, board: Board, power: Power): BuildO
|
|||||||
* what matters is that it is the same arbitrary everywhere, so two
|
* what matters is that it is the same arbitrary everywhere, so two
|
||||||
* adjudicators never disagree about a game nobody was playing.
|
* adjudicators never disagree about a game nobody was playing.
|
||||||
*/
|
*/
|
||||||
export function civilDisorderDisbands(board: Board, power: Power, howMany: number): Unit[] {
|
export function civilDisorderDisbands(
|
||||||
const homes = Object.entries(PROVINCES)
|
own: Ownership,
|
||||||
.filter(([, p]) => p.home === power)
|
board: Board,
|
||||||
.map(([id]) => id)
|
power: Power,
|
||||||
|
howMany: number,
|
||||||
|
): Unit[] {
|
||||||
|
/*
|
||||||
|
* Distance is measured to the centres this power *owns*, not to the ones
|
||||||
|
* it was born with. A Russia driven out of Moscow and holding Sweden
|
||||||
|
* measures from Sweden -- home is where the supply is, and a unit far from
|
||||||
|
* any of it is the one that is no longer doing anything.
|
||||||
|
*/
|
||||||
|
const homes = [...own.entries()].filter(([, p]) => p === power).map(([id]) => id)
|
||||||
|
|
||||||
const distance = (unit: Unit): number => {
|
/*
|
||||||
const graph = unit.type === 'army' ? ARMY : FLEET
|
* Distance is measured over the whole map, not over the graph the unit
|
||||||
const start = unit.type === 'army' ? base(unit.at) : unit.at
|
* itself moves on.
|
||||||
const seen = new Set([start])
|
*
|
||||||
let edge = [start]
|
* That looks wrong for about a minute. An army counts sea provinces it
|
||||||
for (let step = 0; edge.length > 0; step++) {
|
* would need convoying across -- Albania is two steps from Naples over the
|
||||||
if (edge.some((id) => homes.includes(base(id)))) return step
|
* Adriatic, not unreachable -- and a fleet counts inland provinces it could
|
||||||
const next: string[] = []
|
* never enter, so the Baltic is two steps from Warsaw. Both are what the
|
||||||
for (const id of edge) {
|
* published cases require, and both make sense once you see what is being
|
||||||
for (const to of graph[id] ?? []) if (!seen.has(to)) (seen.add(to), next.push(to))
|
* measured: not how the unit would get home, but how far out of the way it
|
||||||
}
|
* is. A fleet in the Baltic is still in the middle of Russia's business. A
|
||||||
edge = next
|
* fleet in the North Atlantic is not.
|
||||||
|
*/
|
||||||
|
const seen = new Set<string>()
|
||||||
|
let edge = homes.map(base)
|
||||||
|
const away = new Map<string, number>()
|
||||||
|
for (let step = 0; edge.length > 0; step++) {
|
||||||
|
const next: string[] = []
|
||||||
|
for (const id of edge) {
|
||||||
|
if (seen.has(id)) continue
|
||||||
|
seen.add(id)
|
||||||
|
away.set(id, step)
|
||||||
|
for (const to of neighbours(id)) if (!seen.has(to)) next.push(to)
|
||||||
}
|
}
|
||||||
return Number.MAX_SAFE_INTEGER
|
edge = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const distance = (unit: Unit): number => away.get(base(unit.at)) ?? Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
return [...board.values()]
|
return [...board.values()]
|
||||||
.filter((u) => u.power === power)
|
.filter((u) => u.power === power)
|
||||||
.map((u) => ({ u, d: distance(u) }))
|
.map((u) => ({ u, d: distance(u) }))
|
||||||
@@ -220,3 +242,74 @@ export function soloWinner(own: Ownership): Power | null {
|
|||||||
for (const power of POWERS) if (centreCount(own, power) >= 18) return power
|
for (const power of POWERS) if (centreCount(own, power) >= 18) return power
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdjustOrder =
|
||||||
|
| { type: 'build'; at: string; unit: UnitType }
|
||||||
|
| { type: 'disband'; at: string; unit?: UnitType }
|
||||||
|
|
||||||
|
export interface AdjustResult {
|
||||||
|
board: Board
|
||||||
|
/** Orders that were refused, by province. */
|
||||||
|
illegal: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and disbands, taken one at a time in the order they were given.
|
||||||
|
*
|
||||||
|
* One at a time matters. A power owed one unit that orders three builds has
|
||||||
|
* not made three mistakes, it has made two -- the first order is carried out
|
||||||
|
* and the rest are refused. Working out the whole set and rejecting it
|
||||||
|
* wholesale would be tidier and would punish a slip far harder than the
|
||||||
|
* rules do.
|
||||||
|
*/
|
||||||
|
export function applyAdjustments(
|
||||||
|
own: Ownership,
|
||||||
|
board: Board,
|
||||||
|
power: Power,
|
||||||
|
given: readonly AdjustOrder[],
|
||||||
|
): AdjustResult {
|
||||||
|
const next: Board = new Map(board)
|
||||||
|
const illegal = new Set<string>()
|
||||||
|
const owed = adjustmentFor(own, next, power)
|
||||||
|
let done = 0
|
||||||
|
|
||||||
|
for (const order of given) {
|
||||||
|
const at = base(order.at)
|
||||||
|
|
||||||
|
if (order.type === 'build') {
|
||||||
|
const allowed = buildOptions(own, next, power).some(
|
||||||
|
(o) => o.at === order.at && o.type === order.unit,
|
||||||
|
)
|
||||||
|
if (!allowed || done >= owed) {
|
||||||
|
illegal.add(at)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next.set(at, { power, type: order.unit, at: order.at })
|
||||||
|
done++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = next.get(at)
|
||||||
|
const removable =
|
||||||
|
unit !== undefined &&
|
||||||
|
unit.power === power &&
|
||||||
|
(order.unit === undefined || unit.type === order.unit)
|
||||||
|
if (!removable || done >= -owed) {
|
||||||
|
illegal.add(at)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next.delete(at)
|
||||||
|
done++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { board: next, illegal }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Everywhere touching a province, by land or by water, whoever could go. */
|
||||||
|
function neighbours(id: string): string[] {
|
||||||
|
const out = new Set<string>(ARMY[id] ?? [])
|
||||||
|
const coasts = PROVINCES[id]?.coasts
|
||||||
|
const keys = coasts ? coasts.map((c) => `${id}/${c}`) : [id]
|
||||||
|
for (const key of keys) for (const to of FLEET[key] ?? []) out.add(base(to))
|
||||||
|
return [...out]
|
||||||
|
}
|
||||||
|
|||||||
+62
-6
@@ -70,18 +70,69 @@ def main() -> int:
|
|||||||
end = heads[idx + 1][0] if idx + 1 < len(heads) else len(lines)
|
end = heads[idx + 1][0] if idx + 1 < len(heads) else len(lines)
|
||||||
m = re.match(r'\s*(6\.[A-Z]\.\d+)\.\s+TEST CASE,\s*(.*)', head)
|
m = re.match(r'\s*(6\.[A-Z]\.\d+)\.\s+TEST CASE,\s*(.*)', head)
|
||||||
case = {'id': m.group(1), 'title': m.group(2).strip().lower(),
|
case = {'id': m.group(1), 'title': m.group(2).strip().lower(),
|
||||||
'units': [], 'orders': [], 'expect': {}}
|
'units': [], 'orders': [], 'expect': {},
|
||||||
|
'retreats': [], 'expectRetreat': {},
|
||||||
|
'adjust': [], 'expectAdjust': {}, 'own': {}}
|
||||||
|
|
||||||
power = None
|
power = None
|
||||||
ok = True
|
ok = True
|
||||||
|
# Cases in section H run a movement phase and then a retreat phase.
|
||||||
|
phase = 'move'
|
||||||
for line in lines[start + 1:end]:
|
for line in lines[start + 1:end]:
|
||||||
s = line.strip()
|
s = line.strip()
|
||||||
if not s:
|
if not s:
|
||||||
continue
|
continue
|
||||||
|
if s.upper() == 'RETREATS':
|
||||||
|
phase = 'retreat'
|
||||||
|
continue
|
||||||
|
|
||||||
p = s.rstrip(':').strip().lower()
|
p = s.rstrip(':').strip().lower()
|
||||||
if s.endswith(':') and p in POWERS:
|
if s.endswith(':') and p in POWERS:
|
||||||
power = p
|
power = p
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# "Germany owns SC Kiel" / "Germany owns A Prussia": the winter
|
||||||
|
# cases state the position this way rather than by ordering it.
|
||||||
|
if m3 := re.match(r'^(\w+)\s+owns\s+(SC|A|F)\s+(.*?)\s*$', s, re.I):
|
||||||
|
who = m3.group(1).lower()
|
||||||
|
if who not in POWERS:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
where = place(m3.group(3))
|
||||||
|
except KeyError as e:
|
||||||
|
unknown.add(str(e)); ok = False; break
|
||||||
|
if m3.group(2).upper() == 'SC':
|
||||||
|
case['own'][where.split('/')[0]] = who
|
||||||
|
else:
|
||||||
|
kind = 'army' if m3.group(2).upper() == 'A' else 'fleet'
|
||||||
|
case['units'].append({'power': who, 'type': kind, 'at': where})
|
||||||
|
case['own'].setdefault(where.split('/')[0], who) if False else None
|
||||||
|
continue
|
||||||
|
|
||||||
|
# "Build A Kiel" / "Disband F Gulf of Lyon" / "Remove A Paris".
|
||||||
|
if m3 := re.match(r'^(Build|Disband|Remove|Automatic disband)\s+([AF])\s+(.*?)\s*$', s, re.I):
|
||||||
|
body, marks = m3.group(3), []
|
||||||
|
while True:
|
||||||
|
m4 = re.search(r',?\s*(' + '|'.join(ANNOTATIONS) + r')\s*$', body, re.I)
|
||||||
|
if not m4:
|
||||||
|
break
|
||||||
|
marks.insert(0, m4.group(1).lower())
|
||||||
|
body = body[:m4.start()].rstrip()
|
||||||
|
try:
|
||||||
|
where = place(body)
|
||||||
|
except KeyError as e:
|
||||||
|
unknown.add(str(e)); ok = False; break
|
||||||
|
kind = 'army' if m3.group(2).upper() == 'A' else 'fleet'
|
||||||
|
verb = m3.group(1).lower()
|
||||||
|
case['adjust'].append({
|
||||||
|
'type': 'build' if verb == 'build'
|
||||||
|
else 'auto' if verb == 'automatic disband' else 'disband',
|
||||||
|
'at': where, 'unit': kind, 'power': power,
|
||||||
|
})
|
||||||
|
if marks:
|
||||||
|
case['expectAdjust'][where.split('/')[0]] = marks
|
||||||
|
continue
|
||||||
|
|
||||||
if not re.match(r'^[AF]\s', s) or power is None:
|
if not re.match(r'^[AF]\s', s) or power is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -124,12 +175,17 @@ def main() -> int:
|
|||||||
ok = False
|
ok = False
|
||||||
break
|
break
|
||||||
|
|
||||||
case['units'].append({'power': power, 'type': unit_type, 'at': at})
|
if phase == 'move':
|
||||||
case['orders'].append({**order, 'power': power})
|
case['units'].append({'power': power, 'type': unit_type, 'at': at})
|
||||||
if marks:
|
case['orders'].append({**order, 'power': power})
|
||||||
case['expect'][at.split('/')[0]] = marks
|
if marks:
|
||||||
|
case['expect'][at.split('/')[0]] = marks
|
||||||
|
else:
|
||||||
|
case['retreats'].append({**order, 'power': power})
|
||||||
|
if marks:
|
||||||
|
case['expectRetreat'][at.split('/')[0]] = marks
|
||||||
|
|
||||||
if ok and case['orders']:
|
if ok and (case['orders'] or case['adjust']):
|
||||||
cases.append(case)
|
cases.append(case)
|
||||||
|
|
||||||
if unknown:
|
if unknown:
|
||||||
|
|||||||
Reference in New Issue
Block a user