An enemy can face a target without first calculating an angle. Find the relative position, compare the two magnitudes and choose a useful sprite direction. The important detail is which sector boundaries the game actually wants.

Include and initialise
Reserve public D/U/V/X/Y/A/B/O, private S/T and lines 3000–3300. There are no arrays. Combine routines.bas with a caller using build.py, keeping END, STOP or a jump before the helper. C64/BBC use GOSUB; Spectrum uses GO SUB. Call 3000 at an intended reset boundary; it clears all public fields.
Supply origin X/Y and target A/B as integral byte coordinates in the same space. Screen Y increases downwards. Call 3100. U/V become unsigned horizontal/vertical magnitudes and O=1 reports valid input. Inputs survive. Invalid coordinates clear D/U/V/O and preserve the inputs.
Read the facing
D is zero for coincident points. Otherwise the direction codes run clockwise: one north, two north-east, three east, four south-east, five south, six south-west, seven west and eight north-west. These codes are local to this interface; they differ from the four-way input/grid direction codes.
Reusable numbered routines
3000 LET D=0: LET U=0: LET V=0: LET X=0: LET Y=0: LET A=0: LET B=0: LET O=0: RETURN
3100 LET D=0: LET U=0: LET V=0: LET O=0
3110 IF X<0 OR X>255 OR X<>INT (X) THEN RETURN
3120 IF Y<0 OR Y>255 OR Y<>INT (Y) THEN RETURN
3130 IF A<0 OR A>255 OR A<>INT (A) THEN RETURN
3140 IF B<0 OR B>255 OR B<>INT (B) THEN RETURN
3150 LET S=A-X: LET T=B-Y: LET U=ABS (S): LET V=ABS (T): LET O=1
3160 IF U=0 AND V=0 THEN RETURN
3170 IF U>=2*V THEN GO TO 3250
3180 IF V>=2*U THEN GO TO 3280
3190 LET D=8
3200 IF S>0 AND T<0 THEN LET D=2
3210 IF S>0 AND T>0 THEN LET D=4
3220 IF S<0 AND T>0 THEN LET D=6
3230 RETURN
3250 LET D=7
3260 IF S>0 THEN LET D=3
3270 RETURN
3280 LET D=1
3290 IF T>0 THEN LET D=5
3300 RETURNA cardinal direction wins when its magnitude is at least twice the other. Remaining vectors are diagonal. The doubled intermediate can reach 510, so it stays wide before comparison. Signed displacement is computed before taking its magnitude: subtracting byte coordinates must not wrap a negative result.
Compare two targets
The normal caller faces from 100,100 towards 103,107. Vertical magnitude seven is at least twice horizontal three, so it returns south, code five. The custom target 91,94 returns north-west, code eight, with magnitudes nine and six. Further calls cover coincident points, an exact threshold and extreme coordinates.
Example calls from the maintained caller
70 GO SUB 3000: LET AA=0: LET BB=0
80 LET X=100: LET Y=100: LET A=103: LET B=107: GO SUB 3100: GO SUB 2000
90 IF D=5 THEN LET AA=1
100 IF V=7 THEN LET BB=1
110 LET X=0: LET Y=0: LET A=0: LET B=0: GO SUB 3100: GO SUB 2000
120 LET X=0: LET Y=0: LET A=2: LET B=1: GO SUB 3100: GO SUB 2000
130 LET X=255: LET Y=255: LET A=0: LET B=0: GO SUB 3100: GO SUB 2000
140 PRINT "FACE RESULTS";AA;BB
150 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOPLine 2000, AA/BB/JJ (Z on Spectrum) and the mailbox are diagnostic observations. A game maps D to its own animation frames or facing policy. The helper chooses a facing; it does not move the actor, normalise diagonal speed or decide whether the target is visible.
Choose your sectors deliberately
This is a 2:1 slope quantiser, not eight equal angular sectors with exact 22.5-degree boundaries. Its comparison rule is cheap and predictable. Finer angle tables or another ratio can give a different visual result, with additional data or arithmetic.
A facing can chatter near a boundary when positions change slightly. Hysteresis needs retained direction and a deliberate switching rule; this independent query has neither. Wider or signed world coordinates need wider input bounds and corresponding verification. Separate facing contexts need renamed globals/line references when combined with other helpers.
Mistake and repair
The wrong caller replaces the target’s vertical coordinate with the origin’s Y. Vertical magnitude becomes zero, producing east or west instead of the intended facing. Both saved direction and magnitude checks fail. Keep both coordinates in the same space through the query. Repaired source and loaded program match normal exactly.

Cost and Maths for Games
The query uses fixed validation, signed differences, magnitudes and comparisons. It needs no trigonometric function, lookup table, allocation or device access. Assembly may compare half-magnitudes to avoid a narrow doubled value; BASIC can retain the widened intermediate here. That difference does not imply a whole-game frame budget.
Maths for Games connects displacement, quadrants, slope and quantisation. The Craft connects discrete facings to readable animation and aiming.
Global names and line numbers
BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.
Machine and measured size
Spectrum FOR variables must be single letters: this diagnostic uses Z instead of the other callers’ JJ. The helper uses scalar numeric fields and allocates no arrays. AND/OR in these listings join comparisons; neither is being used as a bitwise operator. The diagnostic reserves its mailbox above CLEAR 59999.
The numbered helper occupies 639 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, Z and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through Spectrum’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.
An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.
Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-world-policy.py eight-way-direction sinclair-zx-spectrum --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-world-policy.py eight-way-direction.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.