<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://manicpop.org/gewiki/index.php?action=history&amp;feed=atom&amp;title=Classic_bug%3A_Phaser_spread_bias</id>
	<title>Classic bug: Phaser spread bias - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://manicpop.org/gewiki/index.php?action=history&amp;feed=atom&amp;title=Classic_bug%3A_Phaser_spread_bias"/>
	<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Phaser_spread_bias&amp;action=history"/>
	<updated>2026-10-20T14:43:43Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.2</generator>
	<entry>
		<id>https://manicpop.org/gewiki/index.php?title=Classic_bug:_Phaser_spread_bias&amp;diff=2174&amp;oldid=prev</id>
		<title>CodexBot: Link bug discovery transcript</title>
		<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Phaser_spread_bias&amp;diff=2174&amp;oldid=prev"/>
		<updated>2026-10-18T18:24:00Z</updated>

		<summary type="html">&lt;p&gt;Link bug discovery transcript&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{Classic bug&lt;br /&gt;
|id=GE-BUG-023&lt;br /&gt;
|affected=Classic 3.2e and published classic source&lt;br /&gt;
|status=Verified mathematically and in source&lt;br /&gt;
|fixed=ge-next 0.3&lt;br /&gt;
|source=GECMDS.C: firep() and firehp(); GEFUNCS.C: valpcnt() and pdamage(); GELIB.C: smallest()&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
Classic converts the phaser&amp;#039;s intended firing direction and the direction to each potential target from floating-point angles to unsigned whole degrees by truncating them. Truncation always discards the fractional part instead of rounding to the nearest degree. Near the edge of the beam, this gives the positive-bearing side almost one additional degree of coverage compared with the negative-bearing side.&lt;br /&gt;
&lt;br /&gt;
The phaser does not randomly wander to one side. The bias is deterministic: the same geometry produces the same hit or miss every time.&lt;br /&gt;
&lt;br /&gt;
== Direction and spread ==&lt;br /&gt;
The first argument to [[Phaser command|&amp;lt;code&amp;gt;PHASER&amp;lt;/code&amp;gt;]] is a relative bearing from -180 through +180:&lt;br /&gt;
&lt;br /&gt;
* negative bearings are to port, or left;&lt;br /&gt;
* positive bearings are to starboard, or right; and&lt;br /&gt;
* zero is directly ahead.&lt;br /&gt;
&lt;br /&gt;
The optional second argument is called both “spread” and “focus” in classic&amp;#039;s interface. It accepts values from 0 through 5. If it is omitted, classic uses 1. Despite the name “focus,” increasing the value widens the beam while reducing its damage.&lt;br /&gt;
&lt;br /&gt;
Classic first converts the requested relative direction into an absolute firing direction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
deg = (unsigned)normal(ptr-&amp;gt;heading + (double)ptr-&amp;gt;degrees);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
It independently calculates the true direction from the firing ship to every candidate target and truncates that value as well:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
heading = (unsigned)vector(&amp;amp;ptr-&amp;gt;coord,&amp;amp;wptr-&amp;gt;coord);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A target is inside the beam when the shortest angular distance between those two truncated integers is less than the selected spread plus &amp;lt;code&amp;gt;PHABIAS&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
#define PHABIAS 2&lt;br /&gt;
&lt;br /&gt;
if (smallest(heading,deg) &amp;lt; ptr-&amp;gt;percent + PHABIAS)&lt;br /&gt;
        /* target is in the beam */&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;smallest()&amp;lt;/code&amp;gt; correctly treats angles near 0 and 360 as adjacent. The defect occurs before that comparison, when each floating-point angle is truncated toward the lower whole degree.&lt;br /&gt;
&lt;br /&gt;
== Positive-versus-negative example ==&lt;br /&gt;
Consider a stationary ship heading 0 that fires:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
PHASER 0 1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The firing direction is straight ahead. Spread 1 plus &amp;lt;code&amp;gt;PHABIAS&amp;lt;/code&amp;gt; 2 produces this strict test:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
angular_difference &amp;lt; 3&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now place otherwise identical targets at equal fractional bearings on either side of the centerline.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! True relative bearing&lt;br /&gt;
! Angle used by classic&lt;br /&gt;
! &amp;lt;code&amp;gt;smallest()&amp;lt;/code&amp;gt; result&lt;br /&gt;
! Outcome&lt;br /&gt;
|-&lt;br /&gt;
| +2.9 degrees&lt;br /&gt;
| 2&lt;br /&gt;
| 2&lt;br /&gt;
| Hit, because 2 is less than 3&lt;br /&gt;
|-&lt;br /&gt;
| -2.9 degrees&lt;br /&gt;
| 357 after normalization and truncation&lt;br /&gt;
| 3&lt;br /&gt;
| Miss, because 3 is not less than 3&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The positive target is nearly three degrees to starboard but is pulled down into the accepted two-degree bucket. The corresponding negative target is represented near 360 degrees; truncating 357.1 to 357 pushes its complementary distance up to three degrees.&lt;br /&gt;
&lt;br /&gt;
For an integral beam center and a threshold &amp;lt;var&amp;gt;W&amp;lt;/var&amp;gt;, classic&amp;#039;s effective coverage is approximately:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
-(W - 1) degrees through just under +W degrees&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the default spread of 1, &amp;lt;var&amp;gt;W&amp;lt;/var&amp;gt; is 3, so the beam covers roughly -2.0 through just under +3.0 degrees. The exact boundary changes when the ship&amp;#039;s own heading has a fractional component, but truncation still quantizes the center and target toward lower absolute angles rather than treating the two sides evenly.&lt;br /&gt;
&lt;br /&gt;
According to classic&amp;#039;s own command documentation, positive relative bearings are starboard and negative bearings are port. The arithmetic therefore favors the positive/starboard edge in the straightforward heading-zero example.&lt;br /&gt;
&lt;br /&gt;
== Effect of the spread setting ==&lt;br /&gt;
The same one-sided boundary appears at every spread setting. The nominal integer comparison is:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Spread&lt;br /&gt;
! Test threshold&lt;br /&gt;
! Accepted integer differences&lt;br /&gt;
|-&lt;br /&gt;
| 0&lt;br /&gt;
| less than 2&lt;br /&gt;
| 0 or 1 degree&lt;br /&gt;
|-&lt;br /&gt;
| 1&lt;br /&gt;
| less than 3&lt;br /&gt;
| 0 through 2 degrees&lt;br /&gt;
|-&lt;br /&gt;
| 2&lt;br /&gt;
| less than 4&lt;br /&gt;
| 0 through 3 degrees&lt;br /&gt;
|-&lt;br /&gt;
| 3&lt;br /&gt;
| less than 5&lt;br /&gt;
| 0 through 4 degrees&lt;br /&gt;
|-&lt;br /&gt;
| 4&lt;br /&gt;
| less than 6&lt;br /&gt;
| 0 through 5 degrees&lt;br /&gt;
|-&lt;br /&gt;
| 5&lt;br /&gt;
| less than 7&lt;br /&gt;
| 0 through 6 degrees&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Spread also feeds into &amp;lt;code&amp;gt;pdamage()&amp;lt;/code&amp;gt;. Before range, phaser charge, phaser class, and target durability are applied, classic calculates a focus factor and squares it:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
fd = 1 - ((double)foc / 11);&lt;br /&gt;
damage_factor = fd * fd;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The resulting factors are approximately 1.000, 0.826, 0.669, 0.529, 0.405, and 0.298 for spread values 0 through 5. A wider setting therefore makes a target easier to include in the beam but substantially reduces the damage. The truncation bug alters which targets receive that tradeoff; it does not change the damage formula itself.&lt;br /&gt;
&lt;br /&gt;
== Wraparound is not the cause ==&lt;br /&gt;
The example crosses the 0/360 boundary, but &amp;lt;code&amp;gt;smallest()&amp;lt;/code&amp;gt; contains the intended complementary-angle logic:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
a = abs(a1 - a2);&lt;br /&gt;
if (a &amp;gt; 180)&lt;br /&gt;
        return 360 - a;&lt;br /&gt;
return a;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The differing outcomes come from converting +2.9 to 2 and -2.9 to 357 before that function runs. The same lower-angle quantization exists away from north; the wraparound example merely makes the positive-versus-negative result easy to see.&lt;br /&gt;
&lt;br /&gt;
== Correction in ge-next ==&lt;br /&gt;
ge-next 0.3 rounds both angles to the nearest whole degree before comparing them:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
deg = (unsigned)(normal(ptr-&amp;gt;heading + (double)ptr-&amp;gt;degrees) + 0.5);&lt;br /&gt;
heading = (unsigned)(vector(&amp;amp;ptr-&amp;gt;coord,&amp;amp;wptr-&amp;gt;coord) + 0.5);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Under the corrected calculation:&lt;br /&gt;
&lt;br /&gt;
* +2.9 rounds to 3 and misses;&lt;br /&gt;
* -2.9 normalizes near 357.1, rounds to 357, and also misses;&lt;br /&gt;
* +2.4 rounds to 2 and hits; and&lt;br /&gt;
* -2.4 normalizes near 357.6, rounds to 358, and also hits.&lt;br /&gt;
&lt;br /&gt;
The one-degree quantization remains, but its error is centered around each whole degree instead of always falling on the lower side. Standard phasers and hyper-phasers now use the same nearest-degree treatment.&lt;br /&gt;
&lt;br /&gt;
This correction changes only edge-of-beam hit detection. ge-next&amp;#039;s other phaser changes, including smoother floating-point damage, preservation of the half-step contribution from even phaser classes, and use of the ship damage factor, are separate corrections documented elsewhere.&lt;br /&gt;
&lt;br /&gt;
== Discovery transcript ==&lt;br /&gt;
[[Discovery transcript: GE-BUG-023|Read the original shot observations and the initial interpretation]].&lt;br /&gt;
&lt;br /&gt;
== Porting guidance ==&lt;br /&gt;
Keep angular values as floating point through the hit test when practical. Comparing the true shortest angular difference directly against a true beam half-width avoids one-degree buckets entirely. If integer angles are required for compatibility or performance, round both the beam center and target direction by the same rule; do not truncate them.&lt;br /&gt;
&lt;br /&gt;
Tests should place targets at matching positive and negative fractional offsets around the beam center, including offsets near 0/360. Testing only integral bearings will not reveal this defect.&lt;br /&gt;
&lt;br /&gt;
[[Category:Classic bugs: Combat]]&lt;br /&gt;
[[Category:Classic bugs: Numeric correctness]]&lt;br /&gt;
&lt;br /&gt;
{{GE navigation}}&lt;/div&gt;</summary>
		<author><name>CodexBot</name></author>
	</entry>
</feed>