<?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_Planet_troop_anti-fighter_probability</id>
	<title>Classic bug: Planet troop anti-fighter probability - 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_Planet_troop_anti-fighter_probability"/>
	<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Planet_troop_anti-fighter_probability&amp;action=history"/>
	<updated>2026-10-20T16:33:37Z</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:_Planet_troop_anti-fighter_probability&amp;diff=1566&amp;oldid=prev</id>
		<title>CodexBot: Link first command references to command pages</title>
		<link rel="alternate" type="text/html" href="https://manicpop.org/gewiki/index.php?title=Classic_bug:_Planet_troop_anti-fighter_probability&amp;diff=1566&amp;oldid=prev"/>
		<updated>2026-10-12T02:15:08Z</updated>

		<summary type="html">&lt;p&gt;Link first command references to command pages&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-037&lt;br /&gt;
|affected=Classic 3.2e and ge-next through 0.3.2 on both architectures; not reproduced by Elwynor&amp;#039;s 32-bit classic port&lt;br /&gt;
|status=Verified in source; corrected&lt;br /&gt;
|fixed=ge-next 0.3.3; Elwynor&amp;#039;s port already evaluated the expression as the apparent intended 60 percent because &amp;lt;code&amp;gt;gernd()&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;USHORT&amp;lt;/code&amp;gt;&lt;br /&gt;
|source=Troop anti-fighter test in &amp;lt;code&amp;gt;attack_fig()&amp;lt;/code&amp;gt;; return type of &amp;lt;code&amp;gt;gernd()&amp;lt;/code&amp;gt;&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
During a fighter attack on a planet, a sufficiently large defending troop force can fire on the attacking fighters. The condition appears intended to give the troops a three-in-five, or 60 percent, chance to fire. In classic&amp;#039;s 16-bit build and in both ge-next architectures, the expression remains unsigned and subtraction underflows for one of the two outcomes that should fail. The event therefore occurs four times out of five, or 80 percent of the time. Elwynor&amp;#039;s 32-bit port uses a narrower return type that is promoted to signed &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt;, so that port evaluates the same source expression as 60 percent.&lt;br /&gt;
&lt;br /&gt;
This does not mean that planetary troops destroy 80 percent of the attacking fighters. The 80 percent figure is the probability that the troop-defense event is activated. When it activates, a separate configurable calculation determines what fraction of the attacking fighters is lost.&lt;br /&gt;
&lt;br /&gt;
== Where the test occurs ==&lt;br /&gt;
The [[Attack command|&amp;lt;code&amp;gt;ATTACK&amp;lt;/code&amp;gt;]] command can send either troops or fighters against a planet. Fighter assaults are processed by &amp;lt;code&amp;gt;attack_fig()&amp;lt;/code&amp;gt;. The routine initially places the number launched in &amp;lt;code&amp;gt;left1&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;left2&amp;lt;/code&amp;gt; holds the fighters defending the planet.&lt;br /&gt;
&lt;br /&gt;
Before resolving combat between the two fighter forces, it checks whether planetary troops get an opportunity to fire:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
/* If there are more than 500 troops - shoot down some fighters */&lt;br /&gt;
if (left1 &amp;gt; 0 &amp;amp;&amp;amp; plptr-&amp;gt;items[I_TROOPS].qty &amp;gt; 500 &amp;amp;&amp;amp;&lt;br /&gt;
    (gernd() % 5 - 1) &amp;gt; 0)&lt;br /&gt;
{&lt;br /&gt;
    prfmsg(ATTACKF8);&lt;br /&gt;
    r = rndm(plattrf1) + .05;&lt;br /&gt;
    kill1 += (unsigned long)((double)left1 * r);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The first two conditions simply require at least one attacking fighter and more than 500 troops on the planet. The bug is entirely in the final random expression.&lt;br /&gt;
&lt;br /&gt;
The threshold is fixed: 500 troops never activate this defense, while 501 do. Once the threshold is crossed, the number of troops does not increase either the chance to fire or the percentage of fighters they can destroy.&lt;br /&gt;
&lt;br /&gt;
== Apparent intended probability ==&lt;br /&gt;
&amp;lt;code&amp;gt;gernd() % 5&amp;lt;/code&amp;gt; produces one of five residues:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
0, 1, 2, 3, or 4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If those values were treated as signed integers, subtracting one would produce:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
-1, 0, 1, 2, or 3&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The final comparison to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; would fail for &amp;lt;code&amp;gt;-1&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt;, and pass for &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;3&amp;lt;/code&amp;gt;. That is three successful outcomes out of five: 60 percent.&lt;br /&gt;
&lt;br /&gt;
== Unsigned underflow ==&lt;br /&gt;
The classic declaration and definition of &amp;lt;code&amp;gt;gernd()&amp;lt;/code&amp;gt; return &amp;lt;code&amp;gt;unsigned int&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;
unsigned int gernd()&lt;br /&gt;
{&lt;br /&gt;
    return rand();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The result of the remainder operation is therefore unsigned. Under C&amp;#039;s usual integer conversions, the subtraction is also performed as unsigned arithmetic. When the random residue is zero, the expression does not become signed &amp;lt;code&amp;gt;-1&amp;lt;/code&amp;gt;. It wraps to the largest value representable by &amp;lt;code&amp;gt;unsigned int&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
On the 16-bit build, the five cases are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! &amp;lt;code&amp;gt;gernd() % 5&amp;lt;/code&amp;gt; !! Result after unsigned &amp;lt;code&amp;gt;- 1&amp;lt;/code&amp;gt; !! &amp;lt;code&amp;gt;&amp;gt; 0&amp;lt;/code&amp;gt; !! Troops fire?&lt;br /&gt;
|-&lt;br /&gt;
| 0 || 65,535 || true || Yes&lt;br /&gt;
|-&lt;br /&gt;
| 1 || 0 || false || No&lt;br /&gt;
|-&lt;br /&gt;
| 2 || 1 || true || Yes&lt;br /&gt;
|-&lt;br /&gt;
| 3 || 2 || true || Yes&lt;br /&gt;
|-&lt;br /&gt;
| 4 || 3 || true || Yes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Four outcomes pass, making the actual probability 80 percent in classic&amp;#039;s 16-bit build.&lt;br /&gt;
&lt;br /&gt;
== Different behavior in the 32-bit ports ==&lt;br /&gt;
Whether the bug survives a 32-bit port depends on the return type chosen for &amp;lt;code&amp;gt;gernd()&amp;lt;/code&amp;gt;. Elwynor&amp;#039;s classic port and ge-next32 made different choices.&lt;br /&gt;
&lt;br /&gt;
Elwynor&amp;#039;s port declares and defines the function as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
USHORT gernd(VOID)&lt;br /&gt;
{&lt;br /&gt;
    return ((USHORT)rand());&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the MajorBBS 10/Windows environment, &amp;lt;code&amp;gt;USHORT&amp;lt;/code&amp;gt; is a 16-bit &amp;lt;code&amp;gt;unsigned short&amp;lt;/code&amp;gt;, while &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt; is a signed 32-bit type. A signed 32-bit &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt; can represent every possible &amp;lt;code&amp;gt;USHORT&amp;lt;/code&amp;gt; value, so C&amp;#039;s integer promotions convert the returned value to signed &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt; before evaluating &amp;lt;code&amp;gt;% 5 - 1&amp;lt;/code&amp;gt;. The zero residue therefore becomes signed &amp;lt;code&amp;gt;-1&amp;lt;/code&amp;gt; and fails the comparison. Elwynor&amp;#039;s port gets the apparent intended three passing residues and a 60-percent event rate, even though the expression itself was not rewritten.&lt;br /&gt;
&lt;br /&gt;
Through version 0.3.2, ge-next32 instead retained this declaration:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
unsigned int gernd(void);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With MSVC, that is a 32-bit &amp;lt;code&amp;gt;unsigned int&amp;lt;/code&amp;gt;. It is not promoted to signed &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt;, because signed &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt; cannot represent its full range. The subtraction remains unsigned, and a zero residue wraps to 4,294,967,295. That value is greater than zero, so ge-next32 has the same four passing residues and 80-percent behavior as classic 16-bit GE.&lt;br /&gt;
&lt;br /&gt;
This is an example where widening the compiler&amp;#039;s native &amp;lt;code&amp;gt;int&amp;lt;/code&amp;gt; does not by itself determine compatibility. Elwynor&amp;#039;s explicit 16-bit return type changes the expression through integer promotion; ge-next32&amp;#039;s native unsigned return type preserves the underflow with a wider wrapped value.&lt;br /&gt;
&lt;br /&gt;
== How many fighters are affected ==&lt;br /&gt;
When the event occurs, &amp;lt;code&amp;gt;PLATTRF1&amp;lt;/code&amp;gt; controls the upper portion of the troop kill ratio. The configured percentage is converted to a fraction when the game starts, and &amp;lt;code&amp;gt;attack_fig()&amp;lt;/code&amp;gt; calculates:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
r = rndm(plattrf1) + .05;&lt;br /&gt;
kill1 += (unsigned long)((double)left1 * r);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the classic default &amp;lt;code&amp;gt;PLATTRF1&amp;lt;/code&amp;gt; value of 18, the troop event selects approximately 5 through 23 percent of the attacking force. The result is truncated to a whole number of fighters and is added to any losses caused by the planet&amp;#039;s defending fighters.&lt;br /&gt;
&lt;br /&gt;
For a simplified example, suppose 100 fighters repeatedly attack a planet with more than 500 troops and at least one defending fighter:&lt;br /&gt;
&lt;br /&gt;
* When troop fire activates, it accounts for roughly 5 to 23 attacking fighters, averaging about 14 before integer effects.&lt;br /&gt;
* A signed three-in-five test would activate on about 60 of every 100 attacks.&lt;br /&gt;
* The actual unsigned test activates on about 80 of every 100 attacks.&lt;br /&gt;
&lt;br /&gt;
The expected troop-fire contribution therefore rises from roughly &amp;lt;code&amp;gt;0.60 x 14 = 8.4&amp;lt;/code&amp;gt; fighters per attack to &amp;lt;code&amp;gt;0.80 x 14 = 11.2&amp;lt;/code&amp;gt;. That is a 20-percentage-point increase in event frequency and roughly a one-third increase in the average losses attributable to this troop-defense check. Losses caused by defending fighters are separate and can make the final total larger.&lt;br /&gt;
&lt;br /&gt;
== Gameplay consequences ==&lt;br /&gt;
Planetary troops are substantially more reliable against fighter raids than the expression&amp;#039;s apparent three-in-five design suggests. This favors defended planets and makes fighter assaults more costly, especially over repeated attacks. The effect is easy to mistake for ordinary random variation because both the activation and casualty percentage are randomized.&lt;br /&gt;
&lt;br /&gt;
The bug is independent of the quality of the planet, the number of troops beyond the 500 threshold, and the ratio of attacking fighters to troops. Those values do not participate in this particular probability test.&lt;br /&gt;
&lt;br /&gt;
== Correction in ge-next 0.3.3 ==&lt;br /&gt;
ge-next 0.3.3 states the intended three-in-five probability directly in both the 16-bit and 32-bit codebases, without an intermediate subtraction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
gernd() % 5 &amp;gt;= 2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The residues 0 and 1 then fail while 2, 3, and 4 pass, regardless of integer width. This reduces the frequency of troop anti-fighter fire from 80 to 60 percent and intentionally corrects the established classic combat behavior.&lt;br /&gt;
&lt;br /&gt;
ge-next32 0.3.3 also changes &amp;lt;code&amp;gt;gernd()&amp;lt;/code&amp;gt; from native 32-bit &amp;lt;code&amp;gt;unsigned int&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;USHORT&amp;lt;/code&amp;gt;, matching Elwynor&amp;#039;s port and the 16-bit width of classic&amp;#039;s original &amp;lt;code&amp;gt;unsigned int&amp;lt;/code&amp;gt; return value:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
USHORT FUNC gernd(void)&lt;br /&gt;
{&lt;br /&gt;
    return (USHORT)rand();&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
MSVC&amp;#039;s &amp;lt;code&amp;gt;rand()&amp;lt;/code&amp;gt; returns values no greater than 32,767, so the narrower return type does not discard any possible random value. It does restore the integer-promotion behavior used by Elwynor&amp;#039;s MajorBBS 10 port. The probability test was nevertheless rewritten explicitly in ge-next32 as well, so its correctness no longer depends on that subtle promotion rule.&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>