6

I am developing a game that works on Windows and Android but it has an issue that I cannot solve. Basically I have a 4x5 grid with some buttons and these buttons are filled each second with a random number that must be 2, 4 or 8. If you tap on two buttons with the same number, the sum is calculated. This is a firemonkey project.

The game works fine but you can see the problem in the pictures below. When I run the game in my windows machine it generates 2, 4 or 8. Under android It generates 2, 4, 7 and 8. Random numbers are created in this way:

valueToOutput := Trunc(Exp(Ln(2) * (1+Random(3))));

That variable holds the number to be displayed in the button. Why do I get different results in windows and android? These are two screenshots

I am sure that the function is correct because I have plotted it ( exp(ln(2)*(1+x)) = http://prnt.sc/dmdc3u) and when x is 0,1 or 2 (= when the random number is 0,1 or 2). Could this be an issue with the compiler?

Note: I have already solved this problem using the workaround you can see below, but at first I used the code that you can see in the problem and I'd like to understand what's going on.

valueToOutput := Trunc(Exp(Ln(2) * (1+Random(3))));

//this will always give 2, 4 or 8
if valueToOutput = 7 then
 valueToOutput := valueToOutput + 1;
Alberto Miola
  • 4,643
  • 8
  • 35
  • 49
  • 8
    What about simply `valueToOutput := 2 shl Random(3)`? Then you can skip all the floating-point junk. – Rob Kennedy Dec 21 '16 at 22:53
  • Yes sure, that is a very good solution (also faster than multiplying) but I wanted to know about that formula. I wanted to know if it was a loss of precision only in android – Alberto Miola Dec 21 '16 at 23:58
  • What is `sizeof(Extended)` on Android? On Win32, it is 10, but on Win64 it is 8. Maybe there is a similar loss of precision on mobile? It is not documented on Embarcadero's DocWiki. – Remy Lebeau Dec 22 '16 at 00:08
  • Related [Is floating point math broken?](http://stackoverflow.com/q/588004/224704) However that doesn't explain why this function ins't deterministic. `Random(3)` should return 3 possible `Integer` values which means a function where nothing else varies should have 3 possible results. The only thing I can think of that might be at play is something changing FPU settings between calls. This could lead to subtle differences in internal values where the pre-`Trunc` value is slightly less than `8` _NOTE: `Trunc()` discards a lot of **value**_ – Disillusioned Dec 22 '16 at 00:10
  • 1
    As an aside: I suspect you may get different behaviour on different Android hardware. – Disillusioned Dec 22 '16 at 00:11
  • @remy 10 byte extended is an Intel x87 type. Not found on ARM. Not used on win 64 because floating point is done on sse. I would not call this a loss of precision. That almost implies that 10 bytes floats are somehow a good thing. But in any case floating point calculations are repeatable. – David Heffernan Dec 22 '16 at 06:44
  • At the and I have used @RobKennedy 's solution and I avoided the floating point calculation (a bad beast) – Alberto Miola Dec 22 '16 at 23:40

3 Answers3

6

Floating point calculations are repeatable, for the same input, for the same floating point control state.

With three distinct inputs your expression should therefore have three distinct possible outputs. The only explanation therefore is that something changes the floating point control state, e.g. the rounding mode, precision, etc. during program execution.

If floating point arithmetic could perform the calculation exactly you would not need to round to an integer. But if you must round, at least round to the nearest using Round rather than Trunc.

That said, this is categorically the wrong way to perform a discrete task of picking at random one of 2, 4 and 8. Do that like so:

case Random(3) of
0:
  Result := 2;
1:
  Result := 4;
2:
  Result := 8;
end;

Another way is to place the possible outputs into an array and then choose them like this:

Result := arr[Random(3)];

This becomes more attractive when there are more values to choose from.

A golden rule is that if you can avoid using floating point do so. Floating point is slower, and harder to reason about than integer arithmetic. Use it only when necessary.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
2

Posting this as an answer because it doesn't fit the comments

I know next to nothing about androïd but I would use following approach on Windows to narrow it down. Likely, a similar approach exists for Androïd.

  1. Keep temporary calculations in global variables. This makes it easier to extract the values from the crashdump we are going to take. Raise an exception when hitting a wrong value.
  2. Attach procdump to the running process. Procdump from Sysinternals allows you to create a dumpfile for each exception encountered. The commandline would be something like procdump -ma -e 1 Project1.exe
  3. Run the calculation and wait for it to raise the exception.
  4. Analyze the dump. The values of the tmp variables can be extracted from memory.

Code

var
  tmpRandom: Integer;
  tmpLn: Extended;
  tmpLnRandom: Extended;
  tmpExp: Extended;
  tmpTrunc: Int64;

procedure TForm1.btn1Click(Sender: TObject);
var
  I: Int64;
begin
  while true do
  begin
      tmpRandom := Random(3);
      tmpLn := Ln(2);
      tmpLnRandom := tmpLn * (1+tmpRandom);
      tmpExp := Exp(tmpLnRandom);
      tmpTrunc := Trunc(tmpExp);
      I := tmpTrunc;
      if (I and 1 = 1) then
        raise Exception.CreateFmt('I = %0:d', [I]);
  end;
end;

Example layout of variables

Example layout of variables

Lieven Keersmaekers
  • 57,207
  • 13
  • 112
  • 146
0

We are not seeing the whole thing. For instance you say that windows generates 2, 4 or 8, but we see 16 in the grid. You say that Android generates 7, but we see 14 in the grid. So clearly you are multiplying by 2 somewhere. My analysis would be, since 2 * 4 is 8, that the android version only generates 2, 4, and 7, (not 8), since as David says, you only have 3 starting states, so there must only be three ending states. The use of Trunc in this situation is asking for trouble. I think things would be alleviated by using Round instead of Trunc, like this.

valueToOutput := Round(Exp(Ln(2) * (1+Random(3))));

That said, as David says, this is not the right way to do it.

Dsm
  • 5,870
  • 20
  • 24
  • 1
    Don't know why you're getting downvotes. Nothing in the question disproves your hypothesis, and sometimes developers do miss the obvious. (Though question does explain why some values are doubled.) – Disillusioned Dec 31 '16 at 11:46
  • @CraigYoung. Thank you. At the time I posted the answer had not been accepted, and I felt I was adding a potential solution to what should have been theoretically impossible. I also thought (and still think) I was identifying the cause of his problem which he did not understand at the time, even though there was a better approach, – Dsm Dec 31 '16 at 18:01