LB, DL, and a game that speaks one language

milestone tactics, positions, vocabulary

A tactics board that prints DL next to a role badge that prints LB isn't being precise. It's being bilingual by accident, and the manager is the one left doing the translation.

The annoying part is that both codes are correct. They answer different questions, and I'd been treating them as one thing across about six screens.

So I sat down and separated the three.

Who a player is — that's his role. LB, LWB, FC, and the rest of the sixteen. It travels with him wherever he goes.

Where a body stands in this particular shape — that's a formation slot. DL, WBL, FCL. Laterality lives here and it's persisted on the lineup, because a left-back is a left-back in any 4-4-2, while DCL is a specific place in this 4-4-2.

And what a manager reads is neither of those. Empty markers and headers print LB, LWB, CB (C), FC. Nobody ever sees the slot dialect. The raw codes are deliberately ugly so that whoever writes the next screen notices they've skipped the display helper 😄.

One home map ties roles to slots, and everything that needs the mapping reads that map: role lighting on the pitch, awards, assignment, the Go side that picks AI tactics. An LWB lights the wing-back marker and an LB lights the full-back, and neither of them needs its own private table to do it. Formation shapes stay local lists, since a 4-3-3 is just a list of slots and doesn't need sharing. The TypeScript contract has a Go twin, locked by a golden file, which is the bit I actually care about — I've been burned enough times by two copies of a table that agree right up until they don't.

Tactics cover moved to the server. That's the rule that lets you drop a centre-back on the wing on a Saturday because the actual left-back is injured, and the client used to work out how bad an idea that was. Now it asks the server for a coarse band — perfect, good or poor — computed from listed roles, visible attributes and hidden versatility. Coarse and server-side on purpose: if the exact formula rides along in the payload then hidden versatility isn't hidden, it's just inconvenient to read. The client keeps a string table as a fallback. It costs a round trip before the picker lights up, which I'll take.

Things I didn't do. I didn't rename the Postgres enums into manager English — that's a migration bill for a cosmetic win. I didn't add Football Manager's DML/DMR height, because that's a dialect this game doesn't speak and inventing one is how you end up with four names for the same shirt. I didn't forbid out-of-position picks either; punishing an injury crisis with an empty shirt isn't football.

And I left the match engine's in-position scoring alone in this pass. That belonged with the familiarity work landing the next morning, and changing both at once is how you lose the ability to say which one moved the results.

← All entries