969c0480ba
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.
Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).
UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
radial scene that consumes a BattleReport prop. Planet at the
centre, races on the outer ring at equal angular spacing, race
clusters by (race, className) with <class>:<numLeft> labels;
observer groups (inBattle: false) are not drawn; eliminated
races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
on the next frame. Playback controls: play/pause + step ± +
rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
via api/battle-fetch.ts; synthetic-gameId prefix routes to a
fixture loader, otherwise REST through the gateway. Always-
visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
corners of the planet's circumscribed square (stroke width
clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
marker is a stroke-only ring (yellow when damaged, red when
wiped). Wired into state-binding.ts; click handler dispatches
battle clicks to the viewer and bombing clicks to the matching
Reports row.
- i18n keys for the viewer in en + ru.
Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).
Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
479 lines
16 KiB
TypeScript
479 lines
16 KiB
TypeScript
// Phase 11 helpers for forging FlatBuffers report payloads in e2e
|
|
// tests. Mirrors the engine's `report.Report` shape so the mocked
|
|
// gateway can return realistic data without standing up the real
|
|
// engine container.
|
|
//
|
|
// Phase 11 only renders planets, so the helpers keep the report shape
|
|
// minimal (turn / dimensions / planet vectors). Phase 13 extended the
|
|
// fixture with the optional rich planet fields (size, resources,
|
|
// stockpiles, population, industry, colonists, production, free
|
|
// industry) so the inspector e2e can drive the read-only display
|
|
// against realistic values. Phase 15 adds a minimal `LocalShipClass`
|
|
// projection so the planet inspector's Build-Ship sub-picker has data
|
|
// in e2e specs (`name` only — Phase 17 widens this when ship-class
|
|
// CRUD lands). Phase 21 adds a `LocalScience` projection so the
|
|
// sciences table and the planet production picker's Research sub-row
|
|
// have data in e2e specs.
|
|
|
|
import { Builder } from "flatbuffers";
|
|
|
|
import { UUID } from "../../../src/proto/galaxy/fbs/common";
|
|
import {
|
|
BattleSummary,
|
|
Bombing,
|
|
LocalPlanet,
|
|
OtherPlanet,
|
|
OtherScience,
|
|
OthersShipClass,
|
|
Player,
|
|
Report,
|
|
Route,
|
|
RouteEntry,
|
|
Science,
|
|
ShipClass,
|
|
ShipProduction,
|
|
UnidentifiedPlanet,
|
|
UninhabitedPlanet,
|
|
} from "../../../src/proto/galaxy/fbs/report";
|
|
|
|
export interface PlanetFixture {
|
|
number: number;
|
|
name: string;
|
|
x: number;
|
|
y: number;
|
|
size?: number;
|
|
resources?: number;
|
|
capital?: number;
|
|
material?: number;
|
|
}
|
|
|
|
export interface InhabitedFixture extends PlanetFixture {
|
|
population?: number;
|
|
colonists?: number;
|
|
industry?: number;
|
|
production?: string;
|
|
freeIndustry?: number;
|
|
}
|
|
|
|
export interface OtherPlanetFixture extends InhabitedFixture {
|
|
owner: string;
|
|
}
|
|
|
|
export interface ShipClassFixture {
|
|
name: string;
|
|
drive?: number;
|
|
armament?: number;
|
|
weapons?: number;
|
|
shields?: number;
|
|
cargo?: number;
|
|
}
|
|
|
|
export interface ScienceFixture {
|
|
name: string;
|
|
drive?: number;
|
|
weapons?: number;
|
|
shields?: number;
|
|
cargo?: number;
|
|
}
|
|
|
|
export interface PlayerFixture {
|
|
name: string;
|
|
drive?: number;
|
|
weapons?: number;
|
|
shields?: number;
|
|
cargo?: number;
|
|
population?: number;
|
|
industry?: number;
|
|
planets?: number;
|
|
relation?: "WAR" | "PEACE" | "-";
|
|
votes?: number;
|
|
extinct?: boolean;
|
|
}
|
|
|
|
export interface RouteEntryFixture {
|
|
loadType: "COL" | "CAP" | "MAT" | "EMP";
|
|
destinationPlanetNumber: number;
|
|
}
|
|
|
|
export interface RouteFixture {
|
|
sourcePlanetNumber: number;
|
|
entries: RouteEntryFixture[];
|
|
}
|
|
|
|
export interface OtherScienceFixture extends ScienceFixture {
|
|
race: string;
|
|
}
|
|
|
|
export interface OtherShipClassFixture extends ShipClassFixture {
|
|
race: string;
|
|
mass?: number;
|
|
}
|
|
|
|
export interface BattleSummaryFixture {
|
|
id: string;
|
|
planet: number;
|
|
shots: number;
|
|
}
|
|
|
|
export interface BombingFixture {
|
|
planetNumber: number;
|
|
planet: string;
|
|
owner: string;
|
|
attacker: string;
|
|
production?: string;
|
|
industry?: number;
|
|
population?: number;
|
|
colonists?: number;
|
|
capital?: number;
|
|
material?: number;
|
|
attackPower?: number;
|
|
wiped?: boolean;
|
|
}
|
|
|
|
export interface ShipProductionFixture {
|
|
planet: number;
|
|
class: string;
|
|
cost?: number;
|
|
prodUsed?: number;
|
|
percent?: number;
|
|
free?: number;
|
|
}
|
|
|
|
export interface ReportFixture {
|
|
turn: number;
|
|
mapWidth?: number;
|
|
mapHeight?: number;
|
|
localPlanets?: InhabitedFixture[];
|
|
otherPlanets?: OtherPlanetFixture[];
|
|
uninhabitedPlanets?: PlanetFixture[];
|
|
unidentifiedPlanets?: { number: number; x: number; y: number }[];
|
|
localShipClass?: ShipClassFixture[];
|
|
localScience?: ScienceFixture[];
|
|
race?: string;
|
|
players?: PlayerFixture[];
|
|
routes?: RouteFixture[];
|
|
myVotes?: number;
|
|
myVoteFor?: string;
|
|
otherScience?: OtherScienceFixture[];
|
|
otherShipClass?: OtherShipClassFixture[];
|
|
battles?: BattleSummaryFixture[];
|
|
bombings?: BombingFixture[];
|
|
shipProductions?: ShipProductionFixture[];
|
|
}
|
|
|
|
export function buildReportPayload(fixture: ReportFixture): Uint8Array {
|
|
const builder = new Builder(512);
|
|
|
|
const localOffsets = (fixture.localPlanets ?? []).map((planet) => {
|
|
const name = builder.createString(planet.name);
|
|
const production =
|
|
planet.production !== undefined
|
|
? builder.createString(planet.production)
|
|
: null;
|
|
LocalPlanet.startLocalPlanet(builder);
|
|
LocalPlanet.addNumber(builder, BigInt(planet.number));
|
|
LocalPlanet.addX(builder, planet.x);
|
|
LocalPlanet.addY(builder, planet.y);
|
|
LocalPlanet.addName(builder, name);
|
|
LocalPlanet.addSize(builder, planet.size ?? 10);
|
|
LocalPlanet.addResources(builder, planet.resources ?? 0.5);
|
|
LocalPlanet.addCapital(builder, planet.capital ?? 0);
|
|
LocalPlanet.addMaterial(builder, planet.material ?? 0);
|
|
LocalPlanet.addPopulation(builder, planet.population ?? 0);
|
|
LocalPlanet.addIndustry(builder, planet.industry ?? 0);
|
|
LocalPlanet.addColonists(builder, planet.colonists ?? 0);
|
|
if (production !== null) LocalPlanet.addProduction(builder, production);
|
|
LocalPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
|
|
return LocalPlanet.endLocalPlanet(builder);
|
|
});
|
|
|
|
const otherOffsets = (fixture.otherPlanets ?? []).map((planet) => {
|
|
const name = builder.createString(planet.name);
|
|
const owner = builder.createString(planet.owner);
|
|
const production =
|
|
planet.production !== undefined
|
|
? builder.createString(planet.production)
|
|
: null;
|
|
OtherPlanet.startOtherPlanet(builder);
|
|
OtherPlanet.addNumber(builder, BigInt(planet.number));
|
|
OtherPlanet.addX(builder, planet.x);
|
|
OtherPlanet.addY(builder, planet.y);
|
|
OtherPlanet.addName(builder, name);
|
|
OtherPlanet.addOwner(builder, owner);
|
|
OtherPlanet.addSize(builder, planet.size ?? 9);
|
|
OtherPlanet.addResources(builder, planet.resources ?? 0.5);
|
|
OtherPlanet.addCapital(builder, planet.capital ?? 0);
|
|
OtherPlanet.addMaterial(builder, planet.material ?? 0);
|
|
OtherPlanet.addPopulation(builder, planet.population ?? 0);
|
|
OtherPlanet.addIndustry(builder, planet.industry ?? 0);
|
|
OtherPlanet.addColonists(builder, planet.colonists ?? 0);
|
|
if (production !== null) OtherPlanet.addProduction(builder, production);
|
|
OtherPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
|
|
return OtherPlanet.endOtherPlanet(builder);
|
|
});
|
|
|
|
const uninhabitedOffsets = (fixture.uninhabitedPlanets ?? []).map(
|
|
(planet) => {
|
|
const name = builder.createString(planet.name);
|
|
UninhabitedPlanet.startUninhabitedPlanet(builder);
|
|
UninhabitedPlanet.addNumber(builder, BigInt(planet.number));
|
|
UninhabitedPlanet.addX(builder, planet.x);
|
|
UninhabitedPlanet.addY(builder, planet.y);
|
|
UninhabitedPlanet.addName(builder, name);
|
|
UninhabitedPlanet.addSize(builder, planet.size ?? 6);
|
|
UninhabitedPlanet.addResources(builder, planet.resources ?? 0.5);
|
|
UninhabitedPlanet.addCapital(builder, planet.capital ?? 0);
|
|
UninhabitedPlanet.addMaterial(builder, planet.material ?? 0);
|
|
return UninhabitedPlanet.endUninhabitedPlanet(builder);
|
|
},
|
|
);
|
|
|
|
const unidentifiedOffsets = (fixture.unidentifiedPlanets ?? []).map(
|
|
(planet) => {
|
|
UnidentifiedPlanet.startUnidentifiedPlanet(builder);
|
|
UnidentifiedPlanet.addNumber(builder, BigInt(planet.number));
|
|
UnidentifiedPlanet.addX(builder, planet.x);
|
|
UnidentifiedPlanet.addY(builder, planet.y);
|
|
return UnidentifiedPlanet.endUnidentifiedPlanet(builder);
|
|
},
|
|
);
|
|
|
|
const localShipClassOffsets = (fixture.localShipClass ?? []).map((cls) => {
|
|
const name = builder.createString(cls.name);
|
|
ShipClass.startShipClass(builder);
|
|
ShipClass.addName(builder, name);
|
|
ShipClass.addDrive(builder, cls.drive ?? 0);
|
|
ShipClass.addArmament(builder, BigInt(cls.armament ?? 0));
|
|
ShipClass.addWeapons(builder, cls.weapons ?? 0);
|
|
ShipClass.addShields(builder, cls.shields ?? 0);
|
|
ShipClass.addCargo(builder, cls.cargo ?? 0);
|
|
return ShipClass.endShipClass(builder);
|
|
});
|
|
|
|
const localScienceOffsets = (fixture.localScience ?? []).map((sci) => {
|
|
const name = builder.createString(sci.name);
|
|
Science.startScience(builder);
|
|
Science.addName(builder, name);
|
|
Science.addDrive(builder, sci.drive ?? 0);
|
|
Science.addWeapons(builder, sci.weapons ?? 0);
|
|
Science.addShields(builder, sci.shields ?? 0);
|
|
Science.addCargo(builder, sci.cargo ?? 0);
|
|
return Science.endScience(builder);
|
|
});
|
|
|
|
const playerOffsets = (fixture.players ?? []).map((p) => {
|
|
const name = builder.createString(p.name);
|
|
const relation =
|
|
p.relation === undefined ? null : builder.createString(p.relation);
|
|
Player.startPlayer(builder);
|
|
Player.addName(builder, name);
|
|
Player.addDrive(builder, p.drive ?? 1);
|
|
Player.addWeapons(builder, p.weapons ?? 0);
|
|
Player.addShields(builder, p.shields ?? 0);
|
|
Player.addCargo(builder, p.cargo ?? 0);
|
|
Player.addPopulation(builder, p.population ?? 0);
|
|
Player.addIndustry(builder, p.industry ?? 0);
|
|
Player.addPlanets(builder, p.planets ?? 0);
|
|
if (relation !== null) Player.addRelation(builder, relation);
|
|
Player.addVotes(builder, p.votes ?? 0);
|
|
Player.addExtinct(builder, p.extinct ?? false);
|
|
return Player.endPlayer(builder);
|
|
});
|
|
|
|
const routeOffsets = (fixture.routes ?? []).map((route) => {
|
|
const entryOffsets = route.entries.map((entry) => {
|
|
const valueOffset = builder.createString(entry.loadType);
|
|
RouteEntry.startRouteEntry(builder);
|
|
RouteEntry.addKey(builder, BigInt(entry.destinationPlanetNumber));
|
|
RouteEntry.addValue(builder, valueOffset);
|
|
return RouteEntry.endRouteEntry(builder);
|
|
});
|
|
const entriesVec = Route.createRouteVector(builder, entryOffsets);
|
|
Route.startRoute(builder);
|
|
Route.addPlanet(builder, BigInt(route.sourcePlanetNumber));
|
|
Route.addRoute(builder, entriesVec);
|
|
return Route.endRoute(builder);
|
|
});
|
|
|
|
const otherScienceOffsets = (fixture.otherScience ?? []).map((sci) => {
|
|
const race = builder.createString(sci.race);
|
|
const name = builder.createString(sci.name);
|
|
OtherScience.startOtherScience(builder);
|
|
OtherScience.addRace(builder, race);
|
|
OtherScience.addName(builder, name);
|
|
OtherScience.addDrive(builder, sci.drive ?? 0);
|
|
OtherScience.addWeapons(builder, sci.weapons ?? 0);
|
|
OtherScience.addShields(builder, sci.shields ?? 0);
|
|
OtherScience.addCargo(builder, sci.cargo ?? 0);
|
|
return OtherScience.endOtherScience(builder);
|
|
});
|
|
|
|
const otherShipClassOffsets = (fixture.otherShipClass ?? []).map((cls) => {
|
|
const race = builder.createString(cls.race);
|
|
const name = builder.createString(cls.name);
|
|
OthersShipClass.startOthersShipClass(builder);
|
|
OthersShipClass.addRace(builder, race);
|
|
OthersShipClass.addName(builder, name);
|
|
OthersShipClass.addDrive(builder, cls.drive ?? 0);
|
|
OthersShipClass.addArmament(builder, BigInt(cls.armament ?? 0));
|
|
OthersShipClass.addWeapons(builder, cls.weapons ?? 0);
|
|
OthersShipClass.addShields(builder, cls.shields ?? 0);
|
|
OthersShipClass.addCargo(builder, cls.cargo ?? 0);
|
|
OthersShipClass.addMass(builder, cls.mass ?? 0);
|
|
return OthersShipClass.endOthersShipClass(builder);
|
|
});
|
|
|
|
const bombingOffsets = (fixture.bombings ?? []).map((b) => {
|
|
const planet = builder.createString(b.planet);
|
|
const owner = builder.createString(b.owner);
|
|
const attacker = builder.createString(b.attacker);
|
|
const production = builder.createString(b.production ?? "");
|
|
Bombing.startBombing(builder);
|
|
Bombing.addNumber(builder, BigInt(b.planetNumber));
|
|
Bombing.addPlanet(builder, planet);
|
|
Bombing.addOwner(builder, owner);
|
|
Bombing.addAttacker(builder, attacker);
|
|
Bombing.addProduction(builder, production);
|
|
Bombing.addIndustry(builder, b.industry ?? 0);
|
|
Bombing.addPopulation(builder, b.population ?? 0);
|
|
Bombing.addColonists(builder, b.colonists ?? 0);
|
|
Bombing.addCapital(builder, b.capital ?? 0);
|
|
Bombing.addMaterial(builder, b.material ?? 0);
|
|
Bombing.addAttackPower(builder, b.attackPower ?? 0);
|
|
Bombing.addWiped(builder, b.wiped ?? false);
|
|
return Bombing.endBombing(builder);
|
|
});
|
|
|
|
const shipProductionOffsets = (fixture.shipProductions ?? []).map((sp) => {
|
|
const className = builder.createString(sp.class);
|
|
ShipProduction.startShipProduction(builder);
|
|
ShipProduction.addPlanet(builder, BigInt(sp.planet));
|
|
ShipProduction.addClass(builder, className);
|
|
ShipProduction.addCost(builder, sp.cost ?? 0);
|
|
ShipProduction.addProdUsed(builder, sp.prodUsed ?? 0);
|
|
ShipProduction.addPercent(builder, sp.percent ?? 0);
|
|
ShipProduction.addFree(builder, sp.free ?? 0);
|
|
return ShipProduction.endShipProduction(builder);
|
|
});
|
|
|
|
const localVec =
|
|
localOffsets.length === 0
|
|
? null
|
|
: Report.createLocalPlanetVector(builder, localOffsets);
|
|
const otherVec =
|
|
otherOffsets.length === 0
|
|
? null
|
|
: Report.createOtherPlanetVector(builder, otherOffsets);
|
|
const uninhabitedVec =
|
|
uninhabitedOffsets.length === 0
|
|
? null
|
|
: Report.createUninhabitedPlanetVector(builder, uninhabitedOffsets);
|
|
const unidentifiedVec =
|
|
unidentifiedOffsets.length === 0
|
|
? null
|
|
: Report.createUnidentifiedPlanetVector(builder, unidentifiedOffsets);
|
|
const localShipClassVec =
|
|
localShipClassOffsets.length === 0
|
|
? null
|
|
: Report.createLocalShipClassVector(builder, localShipClassOffsets);
|
|
const localScienceVec =
|
|
localScienceOffsets.length === 0
|
|
? null
|
|
: Report.createLocalScienceVector(builder, localScienceOffsets);
|
|
const playerVec =
|
|
playerOffsets.length === 0
|
|
? null
|
|
: Report.createPlayerVector(builder, playerOffsets);
|
|
const routeVec =
|
|
routeOffsets.length === 0
|
|
? null
|
|
: Report.createRouteVector(builder, routeOffsets);
|
|
const otherScienceVec =
|
|
otherScienceOffsets.length === 0
|
|
? null
|
|
: Report.createOtherScienceVector(builder, otherScienceOffsets);
|
|
const otherShipClassVec =
|
|
otherShipClassOffsets.length === 0
|
|
? null
|
|
: Report.createOtherShipClassVector(builder, otherShipClassOffsets);
|
|
const bombingVec =
|
|
bombingOffsets.length === 0
|
|
? null
|
|
: Report.createBombingVector(builder, bombingOffsets);
|
|
const shipProductionVec =
|
|
shipProductionOffsets.length === 0
|
|
? null
|
|
: Report.createShipProductionVector(builder, shipProductionOffsets);
|
|
// Phase 27 — `battle` carries `BattleSummary` tables, each with
|
|
// an inline `id:UUID` struct plus `planet` and `shots` slots.
|
|
const battleVec = (() => {
|
|
const summaries = fixture.battles ?? [];
|
|
if (summaries.length === 0) return null;
|
|
const offsets = summaries.map((s) => {
|
|
const [hi, lo] = uuidToHiLo(s.id);
|
|
BattleSummary.startBattleSummary(builder);
|
|
BattleSummary.addId(builder, UUID.createUUID(builder, hi, lo));
|
|
BattleSummary.addPlanet(builder, BigInt(s.planet));
|
|
BattleSummary.addShots(builder, BigInt(s.shots));
|
|
return BattleSummary.endBattleSummary(builder);
|
|
});
|
|
Report.startBattleVector(builder, offsets.length);
|
|
for (let i = offsets.length - 1; i >= 0; i--) {
|
|
builder.addOffset(offsets[i]);
|
|
}
|
|
return builder.endVector();
|
|
})();
|
|
const raceOffset =
|
|
fixture.race === undefined ? null : builder.createString(fixture.race);
|
|
const voteForOffset =
|
|
fixture.myVoteFor === undefined
|
|
? null
|
|
: builder.createString(fixture.myVoteFor);
|
|
|
|
const totalPlanets =
|
|
(fixture.localPlanets ?? []).length +
|
|
(fixture.otherPlanets ?? []).length +
|
|
(fixture.uninhabitedPlanets ?? []).length +
|
|
(fixture.unidentifiedPlanets ?? []).length;
|
|
|
|
Report.startReport(builder);
|
|
Report.addTurn(builder, BigInt(fixture.turn));
|
|
Report.addWidth(builder, fixture.mapWidth ?? 4000);
|
|
Report.addHeight(builder, fixture.mapHeight ?? 4000);
|
|
Report.addPlanetCount(builder, totalPlanets);
|
|
if (raceOffset !== null) Report.addRace(builder, raceOffset);
|
|
if (fixture.myVotes !== undefined) Report.addVotes(builder, fixture.myVotes);
|
|
if (voteForOffset !== null) Report.addVoteFor(builder, voteForOffset);
|
|
if (playerVec !== null) Report.addPlayer(builder, playerVec);
|
|
if (localVec !== null) Report.addLocalPlanet(builder, localVec);
|
|
if (otherVec !== null) Report.addOtherPlanet(builder, otherVec);
|
|
if (uninhabitedVec !== null) Report.addUninhabitedPlanet(builder, uninhabitedVec);
|
|
if (unidentifiedVec !== null) Report.addUnidentifiedPlanet(builder, unidentifiedVec);
|
|
if (localShipClassVec !== null)
|
|
Report.addLocalShipClass(builder, localShipClassVec);
|
|
if (localScienceVec !== null)
|
|
Report.addLocalScience(builder, localScienceVec);
|
|
if (routeVec !== null) Report.addRoute(builder, routeVec);
|
|
if (otherScienceVec !== null)
|
|
Report.addOtherScience(builder, otherScienceVec);
|
|
if (otherShipClassVec !== null)
|
|
Report.addOtherShipClass(builder, otherShipClassVec);
|
|
if (battleVec !== null) Report.addBattle(builder, battleVec);
|
|
if (bombingVec !== null) Report.addBombing(builder, bombingVec);
|
|
if (shipProductionVec !== null)
|
|
Report.addShipProduction(builder, shipProductionVec);
|
|
const reportOff = Report.endReport(builder);
|
|
builder.finish(reportOff);
|
|
return builder.asUint8Array();
|
|
}
|
|
|
|
function uuidToHiLo(value: string): [bigint, bigint] {
|
|
const hex = value.replace(/-/g, "").toLowerCase();
|
|
if (hex.length !== 32 || /[^0-9a-f]/.test(hex)) {
|
|
throw new Error(`buildReportPayload: invalid battle uuid ${value}`);
|
|
}
|
|
const hi = BigInt(`0x${hex.slice(0, 16)}`);
|
|
const lo = BigInt(`0x${hex.slice(16, 32)}`);
|
|
return [hi, lo];
|
|
}
|