program FormationMathTest; // CC 20260924 console checks for uFormationMath. Expected values were worked // out by hand from the GE's C (Gci.c PositionOffsetEx / GetPositionOffsets / // iatanl, formedit.c FindShipRingSector). Exit code = number of failures. {$mode objfpc}{$H+} uses SysUtils, uHCDefines, uFormationMath; var Passed, Failed: Integer; procedure Check(cond: Boolean; const name, detail: string); begin if cond then begin Inc(Passed); WriteLn('PASS ', name); end else begin Inc(Failed); WriteLn('FAIL ', name, ' ', detail); end; end; procedure CheckInt(const name: string; got, want: Int64); begin Check(got = want, name, Format('got %d, want %d', [got, want])); end; procedure CheckStr(const name, got, want: string); begin Check(got = want, name, Format('got "%s", want "%s"', [got, want])); end; var // CC 20260924 zones used by the ring tests (NM): the standard 6-ring group // and an 8-ring extended group (decision 7) Z, Z8: TFormationZones; function MakeZones(const v: array of SmallInt): TFormationZones; var i: Integer; begin Result := Default(TFormationZones); for i := 0 to System.High(v) do // uHCDefines declares a const High Result[i] := v[i]; end; const // 30 deg N in pole-based slivers: SLIVER90 - SLIVER30 LAT30N = SLIVER90 - SLIVER30; // 22369621 EQUATOR = SLIVER90; NORTHPOLE = 0; LON0 = 1000000; procedure CheckRingSectorZ(const name: string; const zones: TFormationZones; xOff, yOff: LongInt; wantRings, wantSectors: UInt64); var r, s: UInt64; begin FindRingSector(xOff, yOff, zones, r, s); Check((r = wantRings) and (s = wantSectors), name, Format('got rings $%x sectors $%x, want rings $%x sectors $%x', [r, s, wantRings, wantSectors])); end; procedure CheckRingSector(const name: string; xOff, yOff: LongInt; wantRings, wantSectors: UInt64); begin CheckRingSectorZ(name, Z, xOff, yOff, wantRings, wantSectors); end; procedure CheckCell(const name: string; const c: TRegionCell; inner, outer, startDeg, endDeg: Integer); begin Check((c.InnerNM = inner) and (c.OuterNM = outer) and (c.StartDeg = startDeg) and (c.EndDeg = endDeg), name, Format('got %d..%d NM %d..%d deg, want %d..%d NM %d..%d deg', [c.InnerNM, c.OuterNM, c.StartDeg, c.EndDeg, inner, outer, startDeg, endDeg])); end; procedure TestTrig; begin // icos(SLIVER90 - sLat) = cos(lat) * 32768, truncated // equator: isinl(SLIVER90) indexes 2048, which Trig.c clamps to 2047 with // zero interpolation, so the GE gets sin(89.956 deg) * 2^24 >> 9 = 32767 CheckInt('icos equator = 32767 (Trig.c clamp)', GEIcos(SLIVER90 - EQUATOR), 32767); // cos 30 = 0.8660254 -> 0.8660254 * 2^24 = 14529495.4, >> 9 = 28377.9 -> 28377 CheckInt('icos 30N = 28377', GEIcos(SLIVER90 - LAT30N), 28377); CheckInt('icos north pole = 0', GEIcos(SLIVER90 - NORTHPOLE), 0); // iatanl(-yOff, xOff): compass bearings on the axes and diagonals CheckInt('iatanl N', GEIatanl(1000, 0), 0); CheckInt('iatanl NE', GEIatanl(1000, 1000), SLIVER45); CheckInt('iatanl E', GEIatanl(0, 1000), SLIVER90); CheckInt('iatanl SE', GEIatanl(-1000, 1000), SLIVER135); CheckInt('iatanl S', GEIatanl(-1000, 0), SLIVER180); CheckInt('iatanl SW', GEIatanl(-1000, -1000), SLIVER225); CheckInt('iatanl W', GEIatanl(0, -1000), SLIVER270); CheckInt('iatanl NW', GEIatanl(1000, -1000), SLIVER315); CheckInt('iatanl (0,0) = 0', GEIatanl(0, 0), 0); // 30 deg: tan 30 = 0.57735; within a sliver-table step of SLIVER30 Check(Abs(GEIatanl(1732, 1000) - SLIVER30) < SLIVER01, 'iatanl 30 deg', Format('got %d, want ~%d', [GEIatanl(1732, 1000), SLIVER30])); end; procedure TestOffsets; var lat, lon, x, y, xo, yo, i, j: LongInt; worst: Int64; begin // 10 NM due east at the equator: i_cos = 32767, 62100 * 32768 div 32767 = 62101 FormationOffsetToLatLon(EQUATOR, LON0, 62100, 0, lat, lon); CheckInt('equator 10 NM E: lat', lat, EQUATOR); CheckInt('equator 10 NM E: lon + 62101', lon, LON0 + 62101); // back: 62101 * 32767 div 32768 = 62099.1 -> 62099 LatLonToFormationOffset(EQUATOR, LON0, lat, lon, x, y); CheckInt('equator 10 NM E: back xOff = 62099', x, 62099); // 10 NM due east at 30N: dLon = 62100 * 32768 div 28377 = 2034892800 div 28377 = 71709 FormationOffsetToLatLon(LAT30N, LON0, 62100, 0, lat, lon); CheckInt('30N 10 NM E: lat unchanged', lat, LAT30N); CheckInt('30N 10 NM E: lon + 71709', lon, LON0 + 71709); // back: 71709 * 28377 div 32768 = 2034886293 div 32768 = 62099 (GE truncation) LatLonToFormationOffset(LAT30N, LON0, lat, lon, x, y); CheckInt('30N 10 NM E: back xOff = 62099', x, 62099); CheckInt('30N 10 NM E: back yOff = 0', y, 0); // +yOff is south: sLat counts down from the north pole FormationOffsetToLatLon(LAT30N, LON0, 0, -62100, lat, lon); CheckInt('30N 10 NM N: sLat - 62100', lat, LAT30N - 62100); CheckInt('30N 10 NM N: lon unchanged', lon, LON0); Check(90 - lat * 360.0 / SLIVER360 > 30.0, '30N 10 NM N: latitude increases', ''); // west, negative xOff: C division truncates toward zero // -62100 * 32768 div 28377 = -71709 (not -71710) FormationOffsetToLatLon(LAT30N, LON0, -62100, 0, lat, lon); CheckInt('30N 10 NM W: lon - 71709', lon, LON0 - 71709); // pole: i_cos 0 is guarded to 1 FormationOffsetToLatLon(NORTHPOLE, LON0, 5, 0, lat, lon); CheckInt('pole guard: dLon = 5 * 32768', lon, LON0 + 5 * 32768); // round trip offset -> latlon -> offset within 1 DU over a grid of offsets worst := 0; for i := -18 to 18 do for j := -18 to 18 do begin xo := i * 6210 + 37; yo := j * 6210 - 11; FormationOffsetToLatLon(LAT30N, LON0, xo, yo, lat, lon); LatLonToFormationOffset(LAT30N, LON0, lat, lon, x, y); if Abs(x - xo) > worst then worst := Abs(x - xo); if Abs(y - yo) > worst then worst := Abs(y - yo); end; Check(worst <= 1, 'round trip offset->latlon->offset within 1 DU (30N)', Format('worst %d DU', [worst])); worst := 0; for i := -18 to 18 do begin xo := i * 6210 + 5; FormationOffsetToLatLon(SLIVER90 + SLIVER45, LON0, xo, 0, lat, lon); // 45S LatLonToFormationOffset(SLIVER90 + SLIVER45, LON0, lat, lon, x, y); if Abs(x - xo) > worst then worst := Abs(x - xo); end; Check(worst <= 1, 'round trip offset->latlon->offset within 1 DU (45S)', Format('worst %d DU', [worst])); end; procedure TestRingSector; begin CheckRingSector('zero offset -> Main Body', 0, 0, MainBodyFlag, 0); // 100 DU: 100 div 621 = 0 tenths -> Main Body, sector cleared CheckRingSector('tiny offset -> Main Body', 100, 0, MainBodyFlag, 0); // 10 NM due east: 62100 div 621 = 100 tenths, 100^2 = 10000; // 4^2*100 = 1600, 8^2*100 = 6400, 12^2*100 = 14400 -> ring 2 (ASW); bearing 90 -> 090-135 CheckRingSector('10 NM E -> ASW 090-135', 62100, 0, ASWRingFlag, FormationSector090_135); // Main Body edge at 4 NM: 24840 div 621 = 40, 1600 < 1600 false -> AAW CheckRingSector('4.0 NM E on MB edge -> AAW', 24840, 0, AAWRingFlag, FormationSector090_135); CheckRingSector('just inside 4 NM -> Main Body', 24839, 0, MainBodyFlag, 0); // truncation toward zero: -24839 div 621 = -39 -> Main Body CheckRingSector('just inside 4 NM W -> Main Body', -24839, 0, MainBodyFlag, 0); CheckRingSector('4.0 NM N -> AAW 000-045', 0, -24840, AAWRingFlag, FormationSector000_045); // 8 NM: 49680 div 621 = 80, 6400 < 6400 false -> ASW CheckRingSector('8.0 NM S -> ASW 180-225', 0, 49680, ASWRingFlag, FormationSector180_225); CheckRingSector('just inside 8 NM S -> AAW', 0, 49679, AAWRingFlag, FormationSector180_225); // 17.9 NM: 111779 div 621 = 179, 32041 < 32400 -> Picket 3 CheckRingSector('17.99 NM W -> Picket 3', -111779, 0, PicketRing3Flag, FormationSector270_315); // 16 -> 17: 16.0 NM = 99360 -> 160, 25600 < 25600 false, < 28900 -> Picket 2 CheckRingSector('16.0 NM E -> Picket 2', 99360, 0, PicketRing2Flag, FormationSector090_135); // 18 NM: 111780 -> 180, 32400 < 32400 false: loop ends at i = 6 -> 1 << 6 // (for this 6-ring group OutsideRingFlag = 1 shl 6 = $40, as the GE) CheckInt('6-ring OutsideRingFlag = $40', OutsideRingFlag(Z), $40); CheckRingSector('18.0 NM W -> beyond outer ring', -111780, 0, OutsideRingFlag(Z), FormationSector270_315); CheckRingSector('50 NM N -> beyond outer ring', 0, -310500, OutsideRingFlag(Z), FormationSector000_045); // diagonal: each axis truncated separately: (24839, 24839) -> 39,39 -> 3042 -> AAW; SE spoke 135 -> 135-180 CheckRingSector('diagonal SE 39,39 tenths -> AAW 135-180', 24839, 24839, AAWRingFlag, FormationSector135_180); // sector boundaries at 10 NM (ASW ring): a spoke belongs to the clockwise sector CheckRingSector('bearing 000 -> 000-045', 0, -62100, ASWRingFlag, FormationSector000_045); CheckRingSector('bearing 045 -> 045-090', 43912, -43912, ASWRingFlag, FormationSector045_090); CheckRingSector('bearing 090 -> 090-135', 62100, 0, ASWRingFlag, FormationSector090_135); CheckRingSector('bearing 135 -> 135-180', 43912, 43912, ASWRingFlag, FormationSector135_180); CheckRingSector('bearing 180 -> 180-225', 0, 62100, ASWRingFlag, FormationSector180_225); CheckRingSector('bearing 225 -> 225-270', -43912, 43912, ASWRingFlag, FormationSector225_270); CheckRingSector('bearing 270 -> 270-315', -62100, 0, ASWRingFlag, FormationSector270_315); CheckRingSector('bearing 315 -> 315-000', -43912, -43912, ASWRingFlag, FormationSector315_000); // just anticlockwise of spokes stays in the previous sector CheckRingSector('bearing 44.9 -> 000-045', 43911, -43912, ASWRingFlag, FormationSector000_045); CheckRingSector('bearing 359.9 -> 315-000', -1, -62100, ASWRingFlag, FormationSector315_000); CheckRingSector('bearing 89.9 -> 045-090', 62100, -1, ASWRingFlag, FormationSector045_090); CheckInt('OffsetBearingDeg N', OffsetBearingDeg(0, -62100), 0); CheckInt('OffsetBearingDeg E', OffsetBearingDeg(62100, 0), 90); CheckInt('OffsetBearingDeg S', OffsetBearingDeg(0, 62100), 180); CheckInt('OffsetBearingDeg W', OffsetBearingDeg(-62100, 0), 270); CheckInt('OffsetBearingDeg NW', OffsetBearingDeg(-1000, -1000), 315); CheckInt('OffsetBearingDeg 359', OffsetBearingDeg(-1, -62100), 359); // atan(1000/1732) = 30.0007 deg CheckInt('OffsetBearingDeg 30', OffsetBearingDeg(1000, -1732), 30); Check(Abs(OffsetRangeNM(62100, 0) - 10.0) < 1e-9, 'OffsetRangeNM 10 NM', ''); Check(Abs(OffsetRangeNM(3 * 6210, 4 * 6210) - 5.0) < 1e-9, 'OffsetRangeNM 3-4-5', ''); CheckInt('LowestRingIndex 0', LowestRingIndex(0), -1); CheckInt('LowestRingIndex MB', LowestRingIndex(MainBodyFlag), 0); CheckInt('LowestRingIndex ASW+P1', LowestRingIndex(ASWRingFlag or PicketRingFlag), 2); CheckInt('LowestRingIndex ring 7', LowestRingIndex($40), 6); CheckInt('LowestRingIndex ring 64', LowestRingIndex(UInt64(1) shl 63), 63); end; procedure TestCells; var c: TRegionCells; begin c := PatrolRegionCells(MainBodyFlag, 0, Z); CheckInt('cells MB count', Length(c), 1); if Length(c) = 1 then CheckCell('cells MB full circle', c[0], 0, 4, 0, 360); c := PatrolRegionCells(AAWRingFlag, 0, Z); CheckInt('cells AAW no sectors count', Length(c), 1); if Length(c) = 1 then CheckCell('cells AAW no sectors full', c[0], 4, 8, 0, 360); c := PatrolRegionCells(AAWRingFlag, FormationSector315_000 or FormationSector000_045, Z); CheckInt('cells wrap north count', Length(c), 1); if Length(c) = 1 then CheckCell('cells wrap north 315..405', c[0], 4, 8, 315, 405); c := PatrolRegionCells(ASWRingFlag or PicketRingFlag, FormationSectorAll, Z); CheckInt('cells all sectors count', Length(c), 2); if Length(c) = 2 then begin CheckCell('cells all sectors ASW full', c[0], 8, 12, 0, 360); CheckCell('cells all sectors P1 full', c[1], 12, 16, 0, 360); end; c := PatrolRegionCells(PicketRing3Flag, FormationSector000_045 or FormationSector090_135, Z); CheckInt('cells split arcs count', Length(c), 2); if Length(c) = 2 then begin CheckCell('cells split arc 1', c[0], 17, 18, 0, 45); CheckCell('cells split arc 2', c[1], 17, 18, 90, 135); end; c := PatrolRegionCells(PicketRing2Flag, FormationSector090_135 or FormationSector135_180 or FormationSector180_225, Z); CheckInt('cells 3-sector arc count', Length(c), 1); if Length(c) = 1 then CheckCell('cells 3-sector arc 90..225', c[0], 16, 17, 90, 225); // 7 sectors (all but 000-045) -> one arc 045..360 c := PatrolRegionCells(AAWRingFlag, FormationSectorAll and not FormationSector000_045, Z); CheckInt('cells 7 sectors count', Length(c), 1); if Length(c) = 1 then CheckCell('cells 7 sectors 45..360', c[0], 4, 8, 45, 360); // 7 sectors (all but 180-225) -> one arc 225..540 wrapping north c := PatrolRegionCells(AAWRingFlag, FormationSectorAll and not FormationSector180_225, Z); CheckInt('cells 7 sectors wrap count', Length(c), 1); if Length(c) = 1 then CheckCell('cells 7 sectors wrap 225..540', c[0], 4, 8, 225, 540); // Main Body plus a sectored ring: MB stays a full disc c := PatrolRegionCells(MainBodyFlag or AAWRingFlag, FormationSector045_090, Z); CheckInt('cells MB+AAW count', Length(c), 2); if Length(c) = 2 then begin CheckCell('cells MB+AAW MB full', c[0], 0, 4, 0, 360); CheckCell('cells MB+AAW AAW arc', c[1], 4, 8, 45, 90); end; c := PatrolRegionCells(0, FormationSector045_090, Z); CheckInt('cells no rings', Length(c), 0); // ring 7 of the 6-ring group: zones[6] = 0 <= zones[5], not drawable c := PatrolRegionCells($40, FormationSector045_090, Z); CheckInt('cells disabled ring 7 skipped', Length(c), 0); end; procedure TestText; begin CheckStr('state slaved', FormationStateText(FM_SLAVED), 'Slaved'); CheckStr('state reforming', FormationStateText(FM_REFORMING), 'Reforming'); CheckStr('state patrolling/drift start', FormationStateText(FM_DRIFT_START), 'Patrolling'); CheckStr('state drifting', FormationStateText(FM_DRIFT_INSIDE), 'Drifting'); CheckStr('state sprint start', FormationStateText(FM_SPRINT_START), 'Sprinting'); CheckStr('state sprint inside', FormationStateText(FM_SPRINT_INSIDE), 'Sprinting'); CheckStr('state on station', FormationStateText(FM_POSITION_REACHED), 'On station'); CheckStr('state independent', FormationStateText(FM_INDEPENDENT), 'Independent'); CheckStr('state RTB', FormationStateText(FM_RTB), 'Returning to base'); CheckStr('state unknown', FormationStateText(7), 'State 7'); CheckStr('type ASuW', PatrolTypeText(ASuW_PATROL), 'ASuW'); CheckStr('type ASW', PatrolTypeText(ASW_PATROL), 'ASW'); CheckStr('type AAW', PatrolTypeText(AAW_PATROL), 'AAW'); CheckStr('type AEW', PatrolTypeText(AEW_PATROL), 'AEW'); CheckStr('type EW', PatrolTypeText(EW_PATROL), 'EW'); CheckStr('type unknown', PatrolTypeText(0), 'Type 0'); CheckStr('status start', PatrolStatusText(PATROL_START), 'Waiting to launch'); CheckStr('status ready', PatrolStatusText(PATROL_READY), 'Ready'); CheckStr('status launch', PatrolStatusText(PATROL_LAUNCH), 'Launching'); CheckStr('status transit', PatrolStatusText(PATROL_TRANSIT), 'In transit'); CheckStr('status return', PatrolStatusText(PATROL_RETURN), 'Returning'); CheckStr('status on station', PatrolStatusText(PATROL_ONSTATION), 'On station'); CheckStr('status delete', PatrolStatusText(PATROL_DELETE), 'Cancelled'); CheckInt('PATROL_DELETE = -6', PATROL_DELETE, -6); CheckStr('rings none', RingsText(0), '-'); CheckStr('rings MB', RingsText(MainBodyFlag), 'Main Body'); CheckStr('rings AAW, ASW', RingsText(AAWRingFlag or ASWRingFlag), 'AAW, ASW'); CheckStr('rings pickets', RingsText(PicketRingFlag or PicketRing2Flag or PicketRing3Flag), 'Picket 1, Picket 2, Picket 3'); CheckStr('rings ring 7', RingsText($40), 'Ring 7'); CheckStr('rings high bits', RingsText(PicketRing3Flag or $40 or $80 or (UInt64(1) shl 63)), 'Picket 3, Ring 7, Ring 8, Ring 64'); CheckStr('sectors none', SectorsText(0), '-'); CheckStr('sectors all', SectorsText(FormationSectorAll), 'All'); CheckStr('sectors two', SectorsText(FormationSector000_045 or FormationSector045_090), '000-045, 045-090'); CheckStr('sectors 315-000', SectorsText(FormationSector315_000), '315-000'); end; procedure TestExtendedRings; var zz: TFormationZones; c: TRegionCells; i: Integer; begin // enabled run CheckInt('enabled 6-ring group', EnabledRingCount(Z), 6); CheckInt('enabled 6-ring mask', EnabledRingsMask(Z), $3F); CheckInt('enabled 8-ring group', EnabledRingCount(Z8), 8); CheckInt('enabled 8-ring mask', EnabledRingsMask(Z8), $FF); CheckInt('8-ring OutsideRingFlag', OutsideRingFlag(Z8), $100); zz := MakeZones([4, 8, 12, 0, 17, 18]); CheckInt('zero zone ends run', EnabledRingCount(zz), 3); zz := MakeZones([4, 8, 12, 12, 17, 18]); CheckInt('equal zone ends run', EnabledRingCount(zz), 3); zz := MakeZones([4, 8, 12, 10, 17, 18]); CheckInt('smaller zone ends run', EnabledRingCount(zz), 3); zz := MakeZones([0, 8, 12]); CheckInt('zones[0] = 0 -> none', EnabledRingCount(zz), 0); CheckInt('zones[0] = 0 -> mask 0', EnabledRingsMask(zz), 0); CheckInt('zones[0] = 0 -> outside is bit 0', OutsideRingFlag(zz), MainBodyFlag); // no enabled rings: every station falls to bit 0 = Main Body, sectors 0 CheckRingSectorZ('zones[0] = 0 -> Main Body', zz, 62100, 0, MainBodyFlag, 0); zz := MakeZones([-4, 8]); CheckInt('negative zones[0] -> none', EnabledRingCount(zz), 0); // run cut to 3 rings: 13 NM is outside ($08), not Picket 1 zz := MakeZones([4, 8, 12, 0, 17, 18]); CheckRingSectorZ('run of 3: 13 NM E -> outside $08', zz, 13 * 6210, 0, $08, FormationSector090_135); // all 64 enabled: mask is every bit, no outside bit for i := 0 to MAX_FORMATION_ZONES - 1 do zz[i] := 4 + i * 10; // 4 .. 634 NM CheckInt('all 64 enabled', EnabledRingCount(zz), 64); Check(EnabledRingsMask(zz) = System.High(UInt64), 'all 64 mask = all bits', Format('got $%x', [EnabledRingsMask(zz)])); CheckInt('all 64 OutsideRingFlag = 0', OutsideRingFlag(zz), 0); // ring index 63 band 624..634 NM: 630 NM N = 3912300 DU -> 6300 tenths CheckRingSectorZ('all 64: 630 NM N -> ring 64', zz, 0, -3912300, UInt64(1) shl 63, FormationSector000_045); // 634 NM = 3937140 DU -> 6340 tenths, not < 6340 -> beyond: rings = 0 CheckRingSectorZ('all 64: 634 NM N -> beyond, rings 0', zz, 0, -3937140, 0, FormationSector000_045); // 8-ring group (4,8,12,16,17,18,24,30): stations in rings 7 and 8 and beyond // 18.0 NM W: 180 tenths, 32400 < 24^2*100 = 57600 -> ring 7 (bit 6) CheckRingSectorZ('8-ring: 18.0 NM W -> Ring 7', Z8, -111780, 0, $40, FormationSector270_315); // 23.99 NM: 148979 div 621 = 239 -> 57121 < 57600 -> ring 7 CheckRingSectorZ('8-ring: 23.99 NM S -> Ring 7', Z8, 0, 148979, $40, FormationSector180_225); // 24.0 NM: 149040 -> 240, 57600 < 57600 false, < 90000 -> ring 8 (bit 7) CheckRingSectorZ('8-ring: 24.0 NM S -> Ring 8', Z8, 0, 149040, $80, FormationSector180_225); // 27 NM NE: 118561 -> 190 tenths per axis, 72200 -> ring 8 CheckRingSectorZ('8-ring: 27 NM NE -> Ring 8', Z8, 118561, -118561, $80, FormationSector045_090); // 30.0 NM: 186300 -> 300, 90000 < 90000 false -> beyond: 1 shl 8 CheckRingSectorZ('8-ring: 30.0 NM E -> beyond ($100)', Z8, 186300, 0, $100, FormationSector090_135); // inner rings unchanged from the 6-ring group CheckRingSectorZ('8-ring: 10 NM E -> ASW', Z8, 62100, 0, ASWRingFlag, FormationSector090_135); CheckRingSectorZ('8-ring: 17.99 NM W -> Picket 3', Z8, -111779, 0, PicketRing3Flag, FormationSector270_315); // region cells for ring 7 (18..24 NM) and ring 8 (24..30 NM) c := PatrolRegionCells($40, FormationSector315_000 or FormationSector000_045, Z8); CheckInt('cells ring 7 count', Length(c), 1); if Length(c) = 1 then CheckCell('cells ring 7 arc 315..405', c[0], 18, 24, 315, 405); c := PatrolRegionCells($40 or $80, 0, Z8); CheckInt('cells rings 7+8 count', Length(c), 2); if Length(c) = 2 then begin CheckCell('cells ring 7 full', c[0], 18, 24, 0, 360); CheckCell('cells ring 8 full', c[1], 24, 30, 0, 360); end; // ring 9 of the 8-ring group is not drawable (zones[8] = 0) c := PatrolRegionCells($100, FormationSector045_090, Z8); CheckInt('cells ring 9 skipped', Length(c), 0); // ring 64 of the all-64 group c := PatrolRegionCells(UInt64(1) shl 63, FormationSector090_135, zz); CheckInt('cells ring 64 count', Length(c), 1); if Length(c) = 1 then CheckCell('cells ring 64 arc', c[0], 624, 634, 90, 135); // names CheckStr('RingName 0', RingName(0), 'Main Body'); CheckStr('RingName 1', RingName(1), 'AAW'); CheckStr('RingName 2', RingName(2), 'ASW'); CheckStr('RingName 3', RingName(3), 'Picket 1'); CheckStr('RingName 4', RingName(4), 'Picket 2'); CheckStr('RingName 5', RingName(5), 'Picket 3'); CheckStr('RingName 6', RingName(6), 'Ring 7'); CheckStr('RingName 63', RingName(63), 'Ring 64'); CheckStr('RingsText all 64 head', Copy(RingsText(System.High(UInt64)), 1, 14), 'Main Body, AAW'); Check(Pos(', Ring 63, Ring 64', RingsText(System.High(UInt64))) > 0, 'RingsText all 64 tail', ''); end; // CC 20260924 patrol area membership and the derived patrol state text procedure TestPatrolState; const E10 = 62100; // 10 NM east (xOff); +yOff is south begin WriteLn('-- patrol state'); // 10 NM east is ASW ring, sector 090-135 Check(InPatrolArea(E10, 0, ASWRingFlag, FormationSector090_135, Z), 'in ASW 090-135', ''); Check(not InPatrolArea(E10, 0, ASWRingFlag, FormationSector000_045, Z), 'wrong sector', ''); Check(not InPatrolArea(E10, 0, AAWRingFlag, FormationSector090_135, Z), 'wrong ring', ''); Check(InPatrolArea(E10, 0, ASWRingFlag or AAWRingFlag, FormationSector090_135, Z), 'ring OR', ''); Check(InPatrolArea(E10, 0, ASWRingFlag, 0, Z), 'sectors 0 = whole ring', ''); Check(InPatrolArea(E10, 0, ASWRingFlag, FormationSectorAll, Z), 'all sectors', ''); // 2 NM north is Main Body, which has no sector Check(InPatrolArea(0, -12420, MainBodyFlag, FormationSector000_045, Z), 'main body any sector', ''); Check(not InPatrolArea(0, -12420, AAWRingFlag, FormationSector000_045, Z), 'main body not AAW', ''); // 30 NM east is beyond Picket 3 Check(not InPatrolArea(186300, 0, PicketRing3Flag, FormationSectorAll, Z), 'beyond outer ring', ''); CheckStr('state start', PatrolStateText(PATROL_START, 1, False, False), 'Waiting for aircraft'); CheckStr('state transit', PatrolStateText(PATROL_TRANSIT, 1, False, False), 'Launching'); CheckStr('state transit airborne', PatrolStateText(PATROL_TRANSIT, 1, True, False), 'In transit'); CheckStr('state transit airborne in area', PatrolStateText(PATROL_TRANSIT, 1, True, True), 'On station'); CheckStr('state onstation outside', PatrolStateText(PATROL_ONSTATION, 1, True, False), 'In transit'); CheckStr('state onstation inside', PatrolStateText(PATROL_ONSTATION, 1, True, True), 'On station'); CheckStr('state onstation no aircraft', PatrolStateText(PATROL_ONSTATION, 1, False, False), 'On station'); CheckStr('state return', PatrolStateText(PATROL_RETURN, 1, True, True), 'In transit'); CheckStr('state deleted in Number', PatrolStateText(PATROL_RETURN, Byte(PATROL_DELETE), True, True), 'Cancelled'); CheckStr('state unknown', PatrolStateText(9, 1, False, False), 'Status 9'); end; // CC 20260924 phase 2: ring size clamping and station scaling procedure TestRingEdit; var zz, zNew: TFormationZones; x, y: LongInt; k: Integer; lR, lS: UInt64; begin WriteLn('-- ring edit'); // GE Dialog04Dlg on 6 rings zz := MakeZones([0, 0, 0, 0, 0, 0]); ClampFormationZones(zz, 6); CheckInt('clamp zeros 0', zz[0], 1); CheckInt('clamp zeros 5', zz[5], 6); zz := MakeZones([10, 5, 5, 20, 20, 21]); ClampFormationZones(zz, 6); CheckInt('clamp widen 1', zz[1], 11); CheckInt('clamp widen 2', zz[2], 12); CheckInt('clamp widen 3', zz[3], 20); CheckInt('clamp widen 4', zz[4], 21); CheckInt('clamp widen 5', zz[5], 22); zz := MakeZones([4, 8, 12, 16, 17, 5000]); ClampFormationZones(zz, 6); CheckInt('clamp ceiling 5', zz[5], MAX_FORMATION_RADIUS); zz := MakeZones([4, 8, 12, 16, 2000, 3000]); ClampFormationZones(zz, 6); CheckInt('clamp ceiling 4', zz[4], MAX_FORMATION_RADIUS - 1); CheckInt('clamp ceiling 5b', zz[5], MAX_FORMATION_RADIUS); // extended: 8 rings, slots beyond cleared zz := MakeZones([4, 8, 12, 16, 17, 18, 18, 25, 99]); ClampFormationZones(zz, 8); CheckInt('clamp ext 6', zz[6], 19); CheckInt('clamp ext 7', zz[7], 25); CheckInt('clamp ext cleared', zz[8], 0); CheckInt('clamp ext count', EnabledRingCount(zz), 8); zz := Z; ClampFormationZones(zz, 6); for k := 0 to 5 do CheckInt('clamp valid unchanged ' + IntToStr(k), zz[k], Z[k]); // scaling: 10 NM east is ASW (band 8+12=20); ASW 12 -> 22 gives band 30: x1.5 zNew := MakeZones([4, 8, 22, 26, 27, 28]); ScaleStationForZones(62100, 0, Z, zNew, x, y); CheckInt('scale ASW x', x, 93150); CheckInt('scale ASW y', y, 0); FindRingSector(x, y, zNew, lR, lS); CheckInt('scale keeps ASW', lR, ASWRingFlag); // Main Body scales by zones[0]: 2 NM north, MB 4 -> 6 zNew := MakeZones([6, 8, 12, 16, 17, 18]); ScaleStationForZones(0, -12420, Z, zNew, x, y); CheckInt('scale MB y', y, -18630); // Picket 2 uses its own band (16+17=33 -> 16+27=43), not Picket 3's (the GE bug) zNew := MakeZones([4, 8, 12, 16, 27, 28]); ScaleStationForZones(0, 102465, Z, zNew, x, y); // 16.5 NM south = Picket 2 CheckInt('scale P2 own band', y, LongInt((Int64(102465) * 43) div 33)); // negative offsets truncate toward zero zNew := MakeZones([4, 8, 22, 26, 27, 28]); ScaleStationForZones(-62101, 0, Z, zNew, x, y); CheckInt('scale trunc toward zero', x, -93151); // zero offset and zero bands stay put ScaleStationForZones(0, 0, Z, zNew, x, y); CheckInt('scale zero', x, 0); zz := MakeZones([0, 0, 0, 0, 0, 0]); ScaleStationForZones(62100, 0, zz, zNew, x, y); CheckInt('scale no old rings', x, 62100); // beyond the outer ring: outermost band (17+18=35 -> 17+28=45) zNew := MakeZones([4, 8, 12, 16, 17, 28]); ScaleStationForZones(186300, 0, Z, zNew, x, y); CheckInt('scale outside', x, LongInt((Int64(186300) * 45) div 35)); end; // CC 20260924 Shift+click area extension procedure TestExtendArea; var r, s: UInt64; begin WriteLn('-- extend area'); // ring OR from a non-Main-Body area r := ASWRingFlag; s := FormationSector000_045; ExtendPatrolArea(r, s, PicketRingFlag, FormationSector000_045); CheckInt('ring or', r, ASWRingFlag or PicketRingFlag); CheckInt('same sector', s, FormationSector000_045); // from Main Body: fill AAW..clicked, no Main Body r := MainBodyFlag; s := 0; ExtendPatrolArea(r, s, PicketRingFlag, FormationSector090_135); CheckInt('mb fill rings', r, AAWRingFlag or ASWRingFlag or PicketRingFlag); CheckInt('mb fill sector', s, FormationSector090_135); // Main Body click / outside click change nothing r := ASWRingFlag; s := FormationSector000_045; ExtendPatrolArea(r, s, MainBodyFlag, 0); CheckInt('mb click rings', r, ASWRingFlag); ExtendPatrolArea(r, s, 0, FormationSector180_225); CheckInt('outside click sectors', s, FormationSector000_045); // adjacent sectors just add r := ASWRingFlag; s := FormationSector000_045; ExtendPatrolArea(r, s, ASWRingFlag, FormationSector045_090); CheckInt('adjacent', s, FormationSector000_045 or FormationSector045_090); // gap filled on the shorter side: 000-045 + 090-135 -> fill 045-090 r := ASWRingFlag; s := FormationSector000_045; ExtendPatrolArea(r, s, ASWRingFlag, FormationSector090_135); CheckInt('fill short gap', s, FormationSector000_045 or FormationSector045_090 or FormationSector090_135); // across north: 315-000 + 045-090 -> fill 000-045 (shorter than the long way) r := ASWRingFlag; s := FormationSector315_000; ExtendPatrolArea(r, s, ASWRingFlag, FormationSector045_090); CheckInt('fill across north', s, FormationSector315_000 or FormationSector000_045 or FormationSector045_090); // opposite sectors: equal gaps, GE fills the cc side (cc <= ccw) r := ASWRingFlag; s := FormationSector000_045; ExtendPatrolArea(r, s, ASWRingFlag, FormationSector180_225); CheckInt('opposite count', PopCnt(s), 5); end; // CC 20260924 merged outlines and random points inside an area procedure TestOutlineAndRandom; var c: TRegionCells; i, lBad: Integer; x, y: LongInt; lRings, lSectors: UInt64; begin WriteLn('-- outline and random point'); // ASW + Picket 1 + Picket 2 in one arc: a single band 8..17 c := PatrolOutlineCells(ASWRingFlag or PicketRingFlag or PicketRing2Flag, FormationSector000_045 or FormationSector045_090, Z); CheckInt('merge count', Length(c), 1); if Length(c) = 1 then begin CheckInt('merge inner', c[0].InnerNM, 8); CheckInt('merge outer', c[0].OuterNM, 17); CheckInt('merge start', c[0].StartDeg, 0); CheckInt('merge end', c[0].EndDeg, 90); end; // a gap between rings keeps two bands c := PatrolOutlineCells(AAWRingFlag or PicketRingFlag, FormationSector000_045, Z); CheckInt('gap count', Length(c), 2); // Main Body stays its own full circle c := PatrolOutlineCells(MainBodyFlag or AAWRingFlag, FormationSector000_045, Z); CheckInt('mb separate', Length(c), 2); // a split arc gives one band per arc c := PatrolOutlineCells(ASWRingFlag or PicketRingFlag, FormationSector000_045 or FormationSector180_225, Z); CheckInt('split arcs', Length(c), 2); // random points always land inside the area RandSeed := 20260924; lRings := ASWRingFlag or PicketRingFlag; lSectors := FormationSector315_000 or FormationSector000_045; lBad := 0; for i := 1 to 500 do begin if not RandomPointInArea(lRings, lSectors, Z, x, y) then Inc(lBad) else if not InPatrolArea(x, y, lRings, lSectors, Z) then Inc(lBad); end; CheckInt('random inside (500)', lBad, 0); lBad := 0; for i := 1 to 200 do if RandomPointInArea(PicketRing3Flag, FormationSectorAll, Z8, x, y) and not InPatrolArea(x, y, PicketRing3Flag, FormationSectorAll, Z8) then Inc(lBad); CheckInt('random thin band', lBad, 0); Check(RandomPointInArea(MainBodyFlag, 0, Z, x, y) and (x = 0) and (y = 0), 'random main body centre', ''); Check(not RandomPointInArea(0, FormationSectorAll, Z, x, y), 'random no rings', ''); end; // CC 20260924 phase 4 default patrol type procedure TestDefaultPatrolType; begin WriteLn('-- default patrol type'); CheckInt('ATA', DefaultPatrolTypeForMission(Ord(MISSION_ATA)), AAW_PATROL); CheckInt('ASW', DefaultPatrolTypeForMission(Ord(MISSION_ASW)), ASW_PATROL); CheckInt('standoff', DefaultPatrolTypeForMission(Ord(MISSION_STANDOFF)), ASuW_PATROL); CheckInt('antirun', DefaultPatrolTypeForMission(Ord(MISSION_ANTIRUN)), ASuW_PATROL); CheckInt('patrol', DefaultPatrolTypeForMission(Ord(MISSION_PATROL)), AEW_PATROL); CheckInt('unknown', DefaultPatrolTypeForMission(250), AEW_PATROL); end; begin Z := MakeZones([4, 8, 12, 16, 17, 18]); Z8 := MakeZones([4, 8, 12, 16, 17, 18, 24, 30]); Passed := 0; Failed := 0; TestTrig; TestOffsets; TestRingSector; TestCells; TestText; TestExtendedRings; TestPatrolState; TestRingEdit; TestExtendArea; TestOutlineAndRandom; TestDefaultPatrolType; WriteLn; WriteLn(Format('%d passed, %d failed', [Passed, Failed])); if Failed > 0 then Halt(1); end.