fix tracks to all be EW. no more NS tracks
This commit is contained in:
+135
-102
@@ -21,10 +21,11 @@
|
||||
|
||||
import { applyIntent, areaOf, canAdvanceLoad, destinationsFor, facilityCarTypes, laborersLeft } from '../engine/apply.ts';
|
||||
import { MAX_CONSIST, nextOfficeTier } from '../engine/content.ts';
|
||||
import type { Hand } from '../engine/content.ts';
|
||||
import type { GameEvent } from '../engine/events.ts';
|
||||
import type { Intent } from '../engine/intents.ts';
|
||||
import { legalActions } from '../engine/legal.ts';
|
||||
import { connectionsFor, exitsFrom, facilityVariants, hasPort, neighbour, opposite, variantsFor } from '../engine/track.ts';
|
||||
import { connectionsFor, exitsFrom, facilityVariants, hasPort, joins, neighbour, opposite, variantsFor } from '../engine/track.ts';
|
||||
import type { Port } from '../engine/track.ts';
|
||||
import { coordKey } from '../engine/state.ts';
|
||||
import type { Facility, GameState, GridCoord, OfficeArea, PlayerIndex, RollingStock, TrackCard } from '../engine/state.ts';
|
||||
@@ -311,24 +312,41 @@ function needsClearing(s: GameState, player: PlayerIndex): boolean {
|
||||
* district keeps Crew Moves cheap, and Moves are the real currency of Local Operations.
|
||||
*/
|
||||
/**
|
||||
* Walk the Secondary Track from the Running Track, using the ENGINE's own `exitsFrom`.
|
||||
* Which side of the Running Track a district is being walked on. `-1` is the rows below, `+1` the
|
||||
* rows above — both real now that a turnout turned 180° sends its 45° leg north. Every walk below
|
||||
* is written once and run for each side; the old code hard-coded "below" throughout and so could
|
||||
* not see half the board.
|
||||
*/
|
||||
type Side = -1 | 1;
|
||||
const SIDES: readonly Side[] = [-1, 1];
|
||||
|
||||
/** The edge of a main-line card that a district on this side hangs from. */
|
||||
const legPort = (side: Side): Port => (side === -1 ? 's' : 'n');
|
||||
|
||||
/**
|
||||
* Walk the Secondary Track away from the Running Track, using the ENGINE's own `exitsFrom`.
|
||||
*
|
||||
* Shared deliberately. Track laying and facility placement both need to know what a crew can
|
||||
* actually reach, and two copies of this walk would eventually disagree about whether a district
|
||||
* connects — which is the one thing both decisions rest on. A turnout's legs never join each other
|
||||
* (§A.1), so this traverses in (cell, entry port) space rather than treating a cell as one node.
|
||||
*
|
||||
* Every step tests `joins`, not `hasPort`: two 45° legs can meet at the same point on a card edge
|
||||
* and still not be a rail, if they lie on opposite diagonals.
|
||||
*/
|
||||
function descendFrom(
|
||||
area: OfficeArea,
|
||||
col: number,
|
||||
side: Side,
|
||||
): { rejoins: Set<number>; seen: Set<string> } {
|
||||
const main = area.runningRow;
|
||||
const rejoins = new Set<number>();
|
||||
const seen = new Set<string>();
|
||||
const start = area.grid.get(`${main - 1},${col}`);
|
||||
if (!start || !hasPort(start, 'n')) return { rejoins, seen };
|
||||
const entry = opposite(legPort(side));
|
||||
const start = area.grid.get(`${main + side},${col}`);
|
||||
if (!start || !hasPort(start, entry)) return { rejoins, seen };
|
||||
|
||||
const stack: { row: number; col: number; entry: Port }[] = [{ row: main - 1, col, entry: 'n' }];
|
||||
const stack: { row: number; col: number; entry: Port }[] = [{ row: main + side, col, entry }];
|
||||
const visited = new Set<string>();
|
||||
while (stack.length > 0) {
|
||||
const cur = stack.pop()!;
|
||||
@@ -341,8 +359,9 @@ function descendFrom(
|
||||
for (const exit of exitsFrom(card, cur.entry)) {
|
||||
const n = neighbour({ row: cur.row, col: cur.col }, exit);
|
||||
const next = area.grid.get(`${n.row},${n.col}`);
|
||||
if (!next || !hasPort(next, opposite(exit))) continue;
|
||||
if (n.row >= main) {
|
||||
if (!next || !joins(card, exit, next)) continue;
|
||||
// Back onto the main, or past it onto the far side: either way the siding has rejoined.
|
||||
if (side === -1 ? n.row >= main : n.row <= main) {
|
||||
if (n.col !== col) rejoins.add(n.col);
|
||||
continue;
|
||||
}
|
||||
@@ -352,38 +371,43 @@ function descendFrom(
|
||||
return { rejoins, seen };
|
||||
}
|
||||
|
||||
/** Every way down off the main, by column. */
|
||||
function waysDown(area: OfficeArea): number[] {
|
||||
/** Every way off the main on one side, by column. */
|
||||
function waysOff(area: OfficeArea, side: Side): number[] {
|
||||
const out: number[] = [];
|
||||
const p = legPort(side);
|
||||
for (const [k, c] of area.grid.entries()) {
|
||||
const [r, col] = k.split(',').map(Number);
|
||||
if (r !== area.runningRow || !hasPort(c, 's')) continue;
|
||||
const under = area.grid.get(`${area.runningRow - 1},${col}`);
|
||||
if (under && hasPort(under, 'n')) out.push(col!);
|
||||
if (r !== area.runningRow) continue;
|
||||
const beyond = area.grid.get(`${area.runningRow + side},${col}`);
|
||||
if (beyond && joins(c, p, beyond)) out.push(col!);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Below-main squares a crew can reach from the Running Track at all — siding or stub. */
|
||||
function reachableBelowMain(area: OfficeArea): Set<string> {
|
||||
/** Off-main squares a crew can reach from the Running Track at all — siding or stub, either side. */
|
||||
function reachableOffMain(area: OfficeArea): Set<string> {
|
||||
const reached = new Set<string>();
|
||||
for (const col of waysDown(area)) {
|
||||
for (const k of descendFrom(area, col).seen) reached.add(k);
|
||||
for (const side of SIDES) {
|
||||
for (const col of waysOff(area, side)) {
|
||||
for (const k of descendFrom(area, col, side).seen) reached.add(k);
|
||||
}
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Below-main squares that lie on a RUN-AROUND — a siding reaching the main at both ends, so a crew
|
||||
* Off-main squares that lie on a RUN-AROUND — a siding reaching the main at both ends, so a crew
|
||||
* can pass its own standing cars (§A.5). A facility here can be worked from either direction; one
|
||||
* on a stub can only be reached by shoving in and backing out.
|
||||
*/
|
||||
function runAroundCells(area: OfficeArea): Set<string> {
|
||||
const onLoop = new Set<string>();
|
||||
for (const col of waysDown(area)) {
|
||||
const { rejoins, seen } = descendFrom(area, col);
|
||||
if (rejoins.size === 0) continue;
|
||||
for (const k of seen) onLoop.add(k);
|
||||
for (const side of SIDES) {
|
||||
for (const col of waysOff(area, side)) {
|
||||
const { rejoins, seen } = descendFrom(area, col, side);
|
||||
if (rejoins.size === 0) continue;
|
||||
for (const k of seen) onLoop.add(k);
|
||||
}
|
||||
}
|
||||
return onLoop;
|
||||
}
|
||||
@@ -411,8 +435,7 @@ function bestFacilityPlay(s: GameState, player: PlayerIndex, options: Intent[]):
|
||||
if (!area) return null;
|
||||
|
||||
const onLoop = runAroundCells(area);
|
||||
const reachable = reachableBelowMain(area);
|
||||
const sidingRow = area.runningRow - 1;
|
||||
const reachable = reachableOffMain(area);
|
||||
|
||||
let best: Intent | null = null;
|
||||
let bestScore = -Infinity;
|
||||
@@ -422,9 +445,10 @@ function bestFacilityPlay(s: GameState, player: PlayerIndex, options: Intent[]):
|
||||
if (s.cards.get(i.cardId)?.kind.kind !== 'freightFacility') continue;
|
||||
|
||||
const { row, col } = i.placement;
|
||||
const axis = facilityVariants()[i.variant ?? 0]?.axis;
|
||||
if (!axis) continue;
|
||||
const ports: Port[] = axis === 'ns' ? ['n', 's'] : ['e', 'w'];
|
||||
if (!facilityVariants()[i.variant ?? 0]) continue;
|
||||
// An industry card is a plain east-west straight, as every one on the printed sheet is. There
|
||||
// is no north-south rotation to weigh any more; a facility joins its neighbours along the row.
|
||||
const ports: Port[] = ['e', 'w'];
|
||||
|
||||
// Closer to the Office is fewer Moves to reach, same as track scoring.
|
||||
let score = -(Math.abs(row - area.officeCoord.row) * 2 + Math.abs(col - area.officeCoord.col));
|
||||
@@ -440,15 +464,14 @@ function bestFacilityPlay(s: GameState, player: PlayerIndex, options: Intent[]):
|
||||
if (!nc || !hasPort(nc, opposite(p))) continue;
|
||||
const k = `${n.row},${n.col}`;
|
||||
if (onLoop.has(k)) meetsLoop = true;
|
||||
if (reachable.has(k) || n.row >= area.runningRow) meetsReachable = true;
|
||||
if (reachable.has(k) || n.row === area.runningRow) meetsReachable = true;
|
||||
}
|
||||
|
||||
// A facility cannot be inserted into a loop that is already closed — one card per square — so
|
||||
// the placement that lands ON a run-around is the one laid IN LINE with a siding still being
|
||||
// built, east-west on the siding row. That is scored highest deliberately.
|
||||
// built, along a row beside the main. That is scored highest deliberately.
|
||||
const inLineWithSiding =
|
||||
row === sidingRow &&
|
||||
axis === 'ew' &&
|
||||
Math.abs(row - area.runningRow) === 1 &&
|
||||
(reachable.has(`${row},${col - 1}`) || reachable.has(`${row},${col + 1}`));
|
||||
|
||||
if (inLineWithSiding) score += 26;
|
||||
@@ -469,11 +492,6 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
if (!area) return null;
|
||||
|
||||
const at = (row: number, col: number): TrackCard | undefined => area.grid.get(`${row},${col}`);
|
||||
const below = area.runningRow - 1;
|
||||
|
||||
/** Does this card send a leg DOWN off the Running Track? */
|
||||
const divergesSouth = (c: TrackCard | undefined): boolean =>
|
||||
!!c && c.geometry.kind === 'track' && c.geometry.geometry === 'turnout';
|
||||
|
||||
/**
|
||||
* Can the siding continue THROUGH this neighbour, into the square being scored?
|
||||
@@ -488,27 +506,34 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
!!c && hasPort(c, toward);
|
||||
|
||||
/**
|
||||
* A main-line card a siding can actually climb back onto — asked of the ENGINE, not guessed from
|
||||
* the card kind. Turnouts diverge south, and so does the Office (its `e-s`/`w-s` pairs), so
|
||||
* testing for a turnout alone missed a way up that is on every board.
|
||||
* A main-line card a siding on this side can climb back onto — asked of the ENGINE, not guessed
|
||||
* from the card kind. It is not enough that the card has the edge: the arc climbing to it must lie
|
||||
* on the same diagonal, so this takes the arc's own slope and compares.
|
||||
*/
|
||||
const wayUp = (c: TrackCard | undefined): boolean => !!c && hasPort(c, 's');
|
||||
const wayBack = (c: TrackCard | undefined, side: Side, from: TrackCard): boolean =>
|
||||
!!c && joins(from, opposite(legPort(side)), c);
|
||||
|
||||
/** Main-line columns a siding could rejoin at. Without one of these beyond it, a run goes nowhere. */
|
||||
const waysUp = [...area.grid.entries()]
|
||||
.filter(([k, c]) => Number(k.split(',')[0]) === area.runningRow && wayUp(c))
|
||||
.map(([k]) => Number(k.split(',')[1]));
|
||||
/** Main-line columns with any leg on this side. Without one beyond it, a run goes nowhere. */
|
||||
const waysBack = (side: Side): number[] =>
|
||||
[...area.grid.entries()]
|
||||
.filter(([k, c]) => Number(k.split(',')[0]) === area.runningRow && hasPort(c, legPort(side)))
|
||||
.map(([k]) => Number(k.split(',')[1]));
|
||||
|
||||
const reachableBelow = reachableBelowMain(area);
|
||||
const reachableOff = reachableOffMain(area);
|
||||
|
||||
/**
|
||||
* Straights already on the Running Track — the only thing an Enhancement can attach to.
|
||||
*
|
||||
* Interlocking, Water Column and Telegraph all print "any Running Track Straight", and Telephone
|
||||
* and Radio chain off Telegraph. The bot builds minimal two-arc run-arounds (`ne` meets `nw`
|
||||
* directly), so it never needed a straight and never laid one: 0.00 on the Running Track across
|
||||
* 100 games, and with it 13 of the 18 enhancement cards that go on the board were structurally
|
||||
* unplayable. Enhancements were drawn 3.78 a game and placed 0.64.
|
||||
* and Radio chain off Telegraph. The bot used to build minimal two-arc run-arounds, so it never
|
||||
* needed a straight and never laid one: 0.00 on the Running Track across 100 games, and with it 13
|
||||
* of the 18 enhancement cards that go on the board were structurally unplayable. Enhancements were
|
||||
* drawn 3.78 a game and placed 0.64.
|
||||
*
|
||||
* The 45° geometry has since made straights unavoidable — a leg off the main only ever reaches the
|
||||
* next row, so the run back along it is straights — and the bot now lays 3.44 a game against 1.01
|
||||
* under the old rules. This bonus still earns its place for the FIRST one, which the sequence
|
||||
* would otherwise reach only by accident.
|
||||
*
|
||||
* Interlocking is the expensive one. It is the designed answer to the ONLY penalty in the game —
|
||||
* "no free A/D track", which cost 2.40 revenue a game, 27% of gross — and it had never once been
|
||||
@@ -521,8 +546,27 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
c.geometry.geometry === 'straight',
|
||||
).length;
|
||||
|
||||
/** The card a lay would place, built exactly as the reducer builds it. */
|
||||
const probeOf = (i: Extract<Intent, { type: 'track.lay' }>): TrackCard => {
|
||||
const v = variantsFor(i.geometry, i.hand)[i.variant ?? 0];
|
||||
return {
|
||||
geometry: {
|
||||
kind: 'track',
|
||||
geometry: i.geometry,
|
||||
...(v?.arc ? { arc: v.arc } : {}),
|
||||
...(v?.turnout ? { turnout: v.turnout } : {}),
|
||||
...(i.hand !== 'none' ? { hand: i.hand } : {}),
|
||||
},
|
||||
baseOperationalRail: true,
|
||||
standing: [],
|
||||
facility: null,
|
||||
modifiers: [],
|
||||
enhancements: [],
|
||||
} as unknown as TrackCard;
|
||||
};
|
||||
|
||||
const arcOf = (i: Extract<Intent, { type: 'track.lay' }>): string | undefined =>
|
||||
variantsFor(i.geometry)[i.variant ?? 0]?.arc;
|
||||
variantsFor(i.geometry, i.hand)[i.variant ?? 0]?.arc;
|
||||
|
||||
/**
|
||||
* RAIL THAT CAN NEVER GO ANYWHERE.
|
||||
@@ -538,49 +582,37 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
* district instead of tidying it.
|
||||
*/
|
||||
const deadEnds = (i: Extract<Intent, { type: 'track.lay' }>): number => {
|
||||
const v = variantsFor(i.geometry)[i.variant ?? 0];
|
||||
const probe = {
|
||||
geometry: {
|
||||
kind: 'track',
|
||||
geometry: i.geometry,
|
||||
...(v?.axis ? { axis: v.axis } : {}),
|
||||
...(v?.arc ? { arc: v.arc } : {}),
|
||||
...(v?.turnout ? { turnout: v.turnout } : {}),
|
||||
},
|
||||
baseOperationalRail: true,
|
||||
standing: [],
|
||||
facility: null,
|
||||
modifiers: [],
|
||||
enhancements: [],
|
||||
} as unknown as TrackCard;
|
||||
const probe = probeOf(i);
|
||||
let dead = 0;
|
||||
for (const p of new Set(connectionsFor(probe).flat())) {
|
||||
const n = neighbour(i.placement, p);
|
||||
const nb = at(n.row, n.col);
|
||||
if (!nb) continue; // empty — may still be built on
|
||||
if (hasPort(nb, opposite(p))) continue; // joins
|
||||
if (!nb) continue; // empty — may still be built on
|
||||
if (joins(probe, p, nb)) continue; // a real rail crosses the edge
|
||||
dead++;
|
||||
}
|
||||
return dead;
|
||||
};
|
||||
|
||||
/**
|
||||
* Arcs left to build BENEATH a turnout.
|
||||
* Arcs left that could meet THIS turnout's leg.
|
||||
*
|
||||
* A turnout is a hole in the Running Track and is worth cutting only for what hangs under it, so
|
||||
* cutting one with nothing left to hang there is pure loss. Measured at 0% today — the bot builds
|
||||
* the arc immediately after the turnout and never runs the supply dry first — so this is a guard
|
||||
* against the supply changing rather than a fix for something happening now.
|
||||
* A turnout is a hole in the Running Track and is worth cutting only for what hangs off it, so
|
||||
* cutting one with nothing left to hang there is pure loss. Asked per hand, because handedness is
|
||||
* the slope: a left-hand turnout's 45° leg can only be continued by a left-hand curve, and a
|
||||
* supply full of right-hand ones is no help to it at all.
|
||||
*/
|
||||
const arcsLeft =
|
||||
(area.trackSupply.get('curved:left') ?? 0) +
|
||||
(area.trackSupply.get('curved:right') ?? 0) +
|
||||
(area.trackSupply.get('sharpCurved:left') ?? 0) +
|
||||
(area.trackSupply.get('sharpCurved:right') ?? 0);
|
||||
const arcsLeft = (hand: Hand): number =>
|
||||
(area.trackSupply.get(`curved:${hand}`) ?? 0) + (area.trackSupply.get(`sharpCurved:${hand}`) ?? 0);
|
||||
|
||||
// Where the district already turns down off the main.
|
||||
// Where the district already turns off the main, either side.
|
||||
const turnouts = [...area.grid.entries()]
|
||||
.filter(([k, c]) => Number(k.split(',')[0]) === area.runningRow && divergesSouth(c))
|
||||
.filter(
|
||||
([k, c]) =>
|
||||
Number(k.split(',')[0]) === area.runningRow &&
|
||||
c.geometry.kind === 'track' &&
|
||||
c.geometry.geometry === 'turnout',
|
||||
)
|
||||
.map(([k]) => Number(k.split(',')[1]));
|
||||
|
||||
let best: Intent | null = null;
|
||||
@@ -606,17 +638,12 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
* change the order of the cars. Scored as a sequence, so each piece is laid because the previous
|
||||
* one asked for it.
|
||||
*/
|
||||
if (
|
||||
row === area.runningRow &&
|
||||
i.geometry === 'straight' &&
|
||||
variantsFor('straight')[i.variant ?? 0]?.axis === 'ew' &&
|
||||
mainStraights < 1
|
||||
) {
|
||||
if (row === area.runningRow && i.geometry === 'straight' && mainStraights < 1) {
|
||||
// Two is enough: Interlocking needs one, and a second gives Telegraph somewhere to go
|
||||
// without competing further with the ways down. Scored below a first turnout, which is still
|
||||
// the most valuable single piece on the board, and below closing a run-around.
|
||||
bonus += 13;
|
||||
} else if (row === area.runningRow && i.geometry === 'turnout' && arcsLeft > 0) {
|
||||
} else if (row === area.runningRow && i.geometry === 'turnout' && arcsLeft(i.hand) > 0) {
|
||||
// A first way down is the most valuable single piece on the board; a second closes the
|
||||
// run-around. Beyond that they are just holes in the Running Track — measured at 6.4 per game
|
||||
// when unrestrained, which consumed the whole 26-piece supply on ways down and none on the
|
||||
@@ -625,44 +652,50 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int
|
||||
// (revenue 3.3 -> 2.5, freight 1.1 -> 0.6). More ways off the main means more industries the
|
||||
// crew can actually reach, which matters more than a tidy Running Track.
|
||||
bonus += turnouts.length === 0 ? 14 : 3;
|
||||
} else if (row === below) {
|
||||
} else if (Math.abs(row - area.runningRow) === 1) {
|
||||
// Which side of the main this square is on. Both are ordinary now: a turnout turned 180°
|
||||
// sends its leg north, so a siding above the Running Track is built exactly like one below.
|
||||
const side: Side = row < area.runningRow ? -1 : 1;
|
||||
const arc = arcOf(i);
|
||||
// What this square is anchored to, and therefore which way a run through it would extend. The
|
||||
// anchor must be reachable from the main: extending a stranded fragment builds nothing.
|
||||
const anchoredAt = (c: number): boolean =>
|
||||
reachableBelow.has(`${row},${c}`) && carriesInto(at(row, c), c < col ? 'e' : 'w');
|
||||
reachableOff.has(`${row},${c}`) && carriesInto(at(row, c), c < col ? 'e' : 'w');
|
||||
const anchorWest = anchoredAt(col - 1);
|
||||
const anchorEast = anchoredAt(col + 1);
|
||||
|
||||
if (arc === 'ne' || arc === 'nw') {
|
||||
// Does this arc climb back toward the main, rather than one row further away from it?
|
||||
if (arc !== undefined && arc.includes(opposite(legPort(side)))) {
|
||||
/**
|
||||
* AN ARC IS ONLY WORTH LAYING WHERE IT CAN REACH THE MAIN.
|
||||
*
|
||||
* Both of these used to score on shape alone: "close the loop back up to the main" was
|
||||
* awarded to any arc beside an east-west run, whether or not there was anything above it to
|
||||
* join. Measured, that produced 0 run-arounds in 100 games — the siding terminated in an arc
|
||||
* pointing north into empty space, past the east end of the Running Track, every game.
|
||||
* This used to score on shape alone: "close the loop back up to the main" was awarded to any
|
||||
* arc beside an east-west run, whether or not there was anything above it to join. Measured,
|
||||
* that produced 0 run-arounds in 100 games — the siding terminated in an arc pointing north
|
||||
* into empty space, past the east end of the Running Track, every game.
|
||||
*
|
||||
* An arc also has exactly two ports, so which way it faces decides what it meets: `nw` joins
|
||||
* north and WEST, `ne` joins north and EAST. Scoring them alike let the bot close a run from
|
||||
* the wrong side, which connects nothing.
|
||||
* Two things now decide whether it actually reaches. An arc has exactly two ports, so which
|
||||
* way it faces decides what it meets — `nw` joins north and WEST, `ne` north and EAST — and
|
||||
* on top of that the main-line card's own leg must lie on the SAME DIAGONAL, or the two 45°
|
||||
* rails meet at a point on the card edge and form a V. `wayBack` asks the engine both.
|
||||
*/
|
||||
if (wayUp(at(area.runningRow, col))) {
|
||||
const meetsRun = (arc === 'nw' && anchorWest) || (arc === 'ne' && anchorEast);
|
||||
if (wayBack(at(area.runningRow, col), side, probeOf(i))) {
|
||||
const meetsRun = (arc.includes('w') && anchorWest) || (arc.includes('e') && anchorEast);
|
||||
// Closing an existing run beats opening a new one: that is the whole difference between a
|
||||
// run-around and one more stub.
|
||||
bonus += meetsRun ? 15 : 13;
|
||||
}
|
||||
// An arc with nothing above it earns no bonus but is not forbidden — reaching a facility
|
||||
// two rows down is still worth doing when there is no loop to close.
|
||||
} else if (i.geometry === 'straight' && variantsFor('straight')[i.variant ?? 0]?.axis === 'ew') {
|
||||
// An arc with nothing beyond it earns no bonus but is not forbidden — reaching a facility
|
||||
// two rows out is still worth doing when there is no loop to close.
|
||||
} else if (i.geometry === 'straight') {
|
||||
/**
|
||||
* Extend the siding beside the main, but only from something that already turned AND only
|
||||
* while there is still somewhere to climb back up. Unbounded, this rule ran the siding east
|
||||
* while there is still somewhere to climb back. Unbounded, this rule ran the siding east
|
||||
* past the last usable column in 96 of 100 games — the loop then missed by one card.
|
||||
*/
|
||||
const back = waysBack(side);
|
||||
const canStillClose =
|
||||
(anchorWest && waysUp.some((c) => c > col)) || (anchorEast && waysUp.some((c) => c < col));
|
||||
(anchorWest && back.some((c) => c > col)) || (anchorEast && back.some((c) => c < col));
|
||||
if ((anchorWest || anchorEast) && canStillClose) {
|
||||
bonus += 11;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user