Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.


Messages - ff7man

Pages: [1] 2
1
I was reading qhimm 20661 and gamefaq's battle mechanics guide and unless I'm doing something wrong they aren't correct in regards to physical hit% calculations on Ruby weapon.

So the claim is:
hit% = (Dex/4 + Atk%) + Def% - EnemyDef%

Where Def% is Dex/4+Armor Def% Value

Here's an example:
A Level 51 Cloud with 42 Dex equipped with an apocalpyse with 120 atk% and a mystile giving 50 def% Ribbon fighting Ruby Weapon with 100 Def%
(42/4+110)+(42/4+50)-100 = 81%

If you believe EnemyDef% uses Dex then:
(42/4+110)+(42/4+50)-(253/4+100) = 17.75%

However when I fight Ruby I only see 3-4 misses occur out of 64 from lucky 7777's giving a ~95% success.

Ruby has a terrible luck of 10 while Cloud has a luck of 24 so you can expect some misses to be critical's and lucky strikes, but this seems way off.
Lucky strikes should be luck/4 = 6% of the time
Criticals should be luck+level-elevel/4 = 4% of the time
I saw multiple critical so that can't be right...

Using Ultima Weapon instead of Apocalpyse, Cloud never misses.

Magic has also never missed.

Example #2
Red XIII Level 57 60 Dex 25 luck Plus Barrette 104atk% Mystile 50 Def% Ribbon
15+15+104+50-100 = 84%
15+15+104+50-163.25 = 20.75%
In game: 0-1 Misses

Swapping Mystile for Rune Armlet 5 Def%:
15+15+104+5-100 = 39%
15+15+104+5-163.25 = -24.25%
In Game: 23/64 Misses = 64%

It's not just lucky 7777 strikes either as doing individual attacks manually seems to follow the same luck as well.

Same results on PC and PSX.

2
AceZephyr was nice and shared some things with me, so I added them to my repo https://github.com/ff7man/chocobo-betting/tree/main/acezephyr

1. A reverse engineered script that can generate items for frames when racing. I modified it to be able to generate betting data as well. You can use it to tell you what item you will win when manipulating RNG on psx. Quick modifications could also tell you what frames have items you care about too.
2. Modifications to Duckstation to race and extract data at a faster pace
3. Data generated from the modified Duckstation (The data is from racing not betting so it's not too useful for me)

It's possible the script could work on PC as well. You would need to change some things as PC uses 45 names instead of 43 and has different prize tables for ranks. Current time will also play a role.

ergonomy_joes source is super helpful, he lists them here: https://github.com/ergonomy-joe/ff7-chocobo/blob/main/NEWFF7/D_0097C8A8.cpp
The chocobo generation logic is here: https://github.com/ergonomy-joe/ff7-chocobo/blob/main/NEWFF7/C_00772340.cpp
The random seed is set via time() in https://github.com/ergonomy-joe/ff7-chocobo/blob/main/NEWFF7/C_00779C90.cpp

3
I just realized I made a big mistake when calculating winners I wasn't checking if first and second were both in the final set. The odds of winning are actually much lower.

Code: [Select]
    success = 0
    if chocobos2[0].place <3:
        success +=1
    if chocobos2[1].place <3:
        success +=1
    if len(chocobos2)> 2:
        if chocobos2[2].place <3:
            success += 1
    if success >1:
        wins +=1
    else:
        losses += 1
should really be
Code: [Select]
    guesses = []
    c0 = chocobos2[0].place
    c1 = chocobos2[1].place
    c2 = 7
    if len(chocobos2) >2:
        c2 = chocobos2[2].place
    guesses.append([c0,c1])
    guesses.append([c0,c2])
    if len(chocobos2) >2:
        guesses.append([c1,c2])
       
    win = False
    for guess in guesses:
        if 0 in guess and 1 in guess:
            win = True
    if win:
        wins +=1
    else:
        losses += 1

The fuzzfinger method is only 63.38% accurate
Code: [Select]
Class   Wins    Losses  Avg
C       649     351     64.9
B       657     343     65.7
A       606     394     60.6
S       623     377     62.3
total   2535    1465    63.38
My modification is 73.70%
Code: [Select]
Class   Wins    Losses  Avg
C       712     288     71.2
B       775     225     77.5
A       741     259     74.1
S       720     280     72.0
total   2948    1052    73.7
Deeplearning is around ~76% knowing all data and at ~63% for visible data.

4
I wanted to take another stab to see if using machine learning instead of me guessing could render better results.

Initial results showed that with only 4000 races machine learning and deep learning models aren't very effective ~50% at guessing winners.

I needed a faster way to generate data so I took a look into lua scripts with bizhawk.

With bizhawk address 0x0B7654 has the first chocobo name and I could map out the rest of the stats from there.

Thanks to https://gamehacking.org/game/88853 I found the correct race position is actually in the chocobo bytes at -24 from the name.

If I read all the positions when position 6 is set I will know the correct order and that the race has finished.

Piecing it all together that script looks like this: https://raw.githubusercontent.com/ff7man/chocobo-betting/main/ml/race.lua

Disabling audio and and video rendering with the nymashock core and I'm getting about 300fps receiving data from a race every ~29s (class c) ~25s (class b) ~19s (class a) ~17s (class s)

AceZephyr1 modified duckstation to run races and can do the same in ~2-3s, I wish that code and data set was public somewhere.

Bizhawk is slower because it is single threaded, but thankfully as a workaround you can run multiple copies of the emulator at the same time without impacting the speed of the others too much.
Running 4 emulators at the same time I was getting ~250fps on each on a ryzen 5 3600 with only a 36% cpu utilization.

I let this run for a week and a half or so and collected 50mb of data. https://github.com/ff7man/chocobo-betting/tree/main/ml has data for every race for the first 7+ min from psx power on.

Following https://www.kaggle.com/code/cullensun/deep-learning-model-for-hong-kong-horse-racing
I used that model to test and evaluate the data set.

https://raw.githubusercontent.com/ff7man/chocobo-betting/main/ml/deeplearn.py

Even with the large dataset and full access to the race data it is only able to guess the correct winning tickets ~76% of the time.

I guess statistical analysis beats out deeplearning/machine learning today. At least with my abilities.





5
in case anyone else struggles running ergonomy_joe's stuff I made a quick guide https://ff7man.github.io/ff7decompilations.html

6
I legit don't know how you pulled this off... I understand a little about reverse engineering and these decompilations are just absolutely insane. I am still trying to wrap my head around this. You have a gift, good work.

7
Brought a smile to my day :) tyvm

8
You are right.... balls. Thanks for correcting me, I don't want to be giving out wrong stats.

Your math checks out with a simple python script

Code: [Select]
import random

n = 0
win = 0
loss = 0
while n<1000000:
 ehealth = 0
 mhealth = 0
 while (ehealth < 10 and mhealth < 10):
  num = random.randint(0,3)
  if num == 1:
    ehealth +=1
  else:
    mhealth +=1
 if ehealth > mhealth:
  win+=1
 else:
  loss+=1
 n+=1

print(n)
print(win)
print(loss)
print(win/n)

Python random isn't precise but gives a .87-.90% depending on the run

So on the website I'm guessing you did:
Dice Type: Tetrahedron (4 faces)
Total number of dice: 19
I want to get: At least X dice equal to
The value: 1 (any of them really)
.. or: 0
Target number of dice (X): 10

How'd you get the numbers in the edit edit?

9
One thing I didn't mention is the RNGLUT is really sporadic and time spent on certain frames depends a lot.

For example on PSX each cycle 0-256 of RNG will wait different amounts of frames before incrementing the RNG value

Code: [Select]
RNG - Cycle1 - Cycle2
6 - 24 - 4
7 - 2  - 14
8 - 18 - 14
9 - 12 - 4
10 - 2 - 14
11 - 18 - 14
12 - 12 - 4
13 - 2 - 28
14 - 30 - 4
15 - 2 - 12
16 - 16 - 16
Once you start the battler it is sped up even more.
Code: [Select]
Frames before increment / Incremented value
8 - 1
8 - 2
8 - 2
4 - 1
4 - 1
8 - 2
8 - 1
8 - 2
8 - 1

I haven't manually counted out all the frames in two cycles either, but there's doesn't seem to be a pattern it follows.

10
I didn't know the last move on fighter 4 impacts fighter 5's move set. Good to know and I updated the ff7 wiki with the correct stats. I also made a video using my charts. I still think a turbo controller has a chance of winning on certain seeds, it's a simple test. Buy a controller and set O+X to turbo, set a book on it and leave the game running for a while, if it gets to the end we can. A human timing that is a different story.

11
Scripting and Reverse Engineering / [FF7] 3D Battler RNG
« on: 2022-11-11 04:11:29 »
I didn't see much online about the 3D Battler so I took a look at it.

https://gamefaqs.gamespot.com/boards/197341-final-fantasy-vii/54471793

Here's what I learned.

It does not matter what you pick, just when you pick it. It doesn't look at your input at all, whether you win or lose is based entirely on random.
However random is a single byte that increments at about 10-12 per second depending on your movement to the arena and is continually generated by movement of the background characters.
This random random value is at 095dc8 on PSX and FF7_EN.exe+8BF588 on PC
Damage is a counter that increments to 10 at 75e30/FF7_EN.exe+8C14DC (you) and 75e31/FF7_EN.exe+8C14DD (enemy)

Fighter 1: You have a 65.93% chance of winning
Fighter 2: You have a 51.17% chance of winning
Fighter 3: You have a 33.33% chance of winning
Fighter 4: You have a 25% chance of winning
Fighter 5: Is a ghost you cannot kill. Low is a Hit, but the value does not increment in memory. Mid is a Tie. High is a loss.

All considered and random being fair you have a 2.81% chance of getting to the ghost and winning 300gp.

I was thinking maybe it would be possible to find a starting seed that won with a turbo controller, but with how fast RNG increments, this would be very hard to get frame perfectly.
With a turbo controller this could likely be exploited by simply running the game for a while pressing O+X repeatedly as it won't stop until you run out of money or hit the ghost.
That said you are definitely better off doing chocobo races for GP, but it is nice knowing how this mechanic works.


Here are the charts for each fighter. Each seed value has a guaranteed result no matter what input you pick.
If you can see the random seed, you can use these charts to better time your inputs to increase your chances of success.








12
Sorry to hear about your laptop, hopefully you get something new soon.

Good news, I figured out how to exploit the PC version :)

I discovered today chocobo betting on PC 100% does get it's initial random value from the system clock. And how do I know this?
Well although I couldn't hook all the time functions in windows. There is a program for linux that is well developed called libfaketime.
But ff7 isn't on linux, how'd you get around that?

I compiled a 32bit libfaketime and installed ff7 1998 in 32bit wine on ubuntu 22.04.
I was able to set a clock time at load and the races were always one of three sets of chocobos.
I'm guessing this is because there are slight differences to loading time.

Code: [Select]
ubuntu@ubuntu:~/.wine/drive_c/Program Files/Square Soft, Inc/Final Fantasy VII$ WINEARCH=win32 LDFLAGS=-m32 LD_PRELOAD=/home/ubuntu/libfaketime32/src/libfaketime.so.1 FAKETIME="@2000-01-01 11:12:13" FAKETIME_DONT_RESET=1 /home/ubuntu/wine-7.0-4-proton-x86/bin/wine ff7.exe

I got the idea from here: https://www.youtube.com/watch?v=-pkf-AaFXLo

https://github.com/wolfcw/libfaketime -> To compile as 32bit you need to add -m32 to CFLAG and LDFLAGS in src/Makefile

The version of Wine I used was prebuilt and I got it's libraries from `apt install wine winetricks`.
https://github.com/Kron4ek/Wine-Builds/releases/download/7.0-4-proton/wine-7.0-4-proton-x86.tar.xz

Installing ff7 was not easy. Here's some guides I needed.
https://appdb.winehq.org/objectManager.php?sClass=version&iId=10317
https://forums.qhimm.com/index.php?topic=11994.0
https://www.codeweavers.com/support/wiki/linux/faq/videomemorysize
One thing to note, winetricks didn't like some of the download links for the dependencies, so some of them had to be manually added to the winetricks cache folder.

With this new knowledge you might be excited to learn, libfaketime is actually not even needed, setting the date and then immediately starting the game has the same effect.

Code: [Select]
date -s "9 NOV 2022 18:00:00" && WINEARCH=win32 /home/ubuntu/wine-7.0-4-proton-x86/bin/wine ff7.exe
Even running the commands back to back with a split second in between works.

On windows you can disable automatic time updates, set a time from command prompt, and pretty quickly press the play button.

Code: [Select]
time 23:00:00.00

On an xbox you can go offline, reboot the xbox.
Now from settings you can set a time, start the game, kill the game, set a time, start the game, and you will get the same result.

I'm really surprised they didn't use a higher precision of time or use uptime.

If someone was really inclined you could probably set up a dns server, to redirect the ntp client of whatever device you are using to a server you control and do the same trick.
A container running https://github.com/btfak/sntp and dnsmasq could easily do the job.

13
In my adventures in figuring out how chocobo betting works. I was taking a look at RNG and stumbled upon the Wonder Catcher.

I don't think anyone has really taken a look at it yet.

So here's what you need to know about it.

Your odds of a prize are determined solely by the single byte random seed. (The same long or short selector address for chocobos and for most of random in the game)

Here are the prizes:
Code: [Select]
0 nothing
1 nothing
2 potion
3 potion
4 potion
5 1gp
6 potion
7 nothing
8 100gp
9 nothing
10 nothing
11 nothing
12 potion
13 potion
14 nothing
15 1gp
16 potion
17 phoenix down
18 potion
19 nothing
20 potion
21 potion
22 potion
23 potion
24 potion
25 potion
26 potion
27 3gp
28 potion
29 potion
30 nothing
31 potion
32 potion
33 nothing
34 potion
35 nothing
36 potion
37 potion
38 nothing
39 nothing
40 potion
41 nothing
42 nothing
43 nothing
44 potion
45 potion
46 1gp
47 nothing
48 1gp
49 nothing
50 nothing
51 potion
52 nothing
53 nothing
54 1gp
55 1gp
56 potion
57 potion
58 nothing
59 potion
60 nothing
61 potion
62 potion
63 1gp
64 1gp
65 1gp
66 1gp
67 potion
68 phoenix
69 3gp
70 1gp
71 nothing
72 nothing
73 potion
74 potion
75 phoenix
76 nothing
77 potion
78 1gp
79 potion
80 3gp
81 potion
82 1gp
83 nothing
84 nothing
85 1gp
86 nothing
87 nothing
88 3gp
89 1gp
90 nothing
91 3gp
92 potion
93 1gp
94 nothing
95 nothing
96 potion
97 1gp
98 potion
99 potion
100 1gp
101 1gp
102 potion
103 potion
104 nothing
105 3gp
106 1gp
107 potion
108 potion
109 potion
110 1gp
111 potion
112 nothing
113 nothing
114 nothing
115 3gp
116 1gp
117 potion
118 3gp
119 3gp
120 nothing
121 potion
122 nothing
123 1gp
124 potion
125 nothing
126 1gp
127 potion
128 phoenix
129 1gp
130 potion
131 potion
132 1gp
133 nothing
134 potion
135 3gp
136 3gp
137 nothing
138 3gp
139 potion
140 potion
141 potion
142 potion
143 nothing
144 nothing
145 nothing
146 potion
147 potion
148 nothing
149 3gp
150 nothing
151 nothing
152 1gp
153 elixir
154 1gp
155 phoenix
156 nothing
157 potion
158 nothing
159 1gp
160 1gp
161 potion
162 3gp
163 potion
164 1gp
165 3gp
166 1gp
167 nothing
168 nothing
169 potion
170 potion
171 potion
172 3gp
173 potion
174 1gp
175 nothing
176 1gp
177 nothing
178 1gp
179 potion
180 phoenix
181 1gp
182 nothing
183 phoenix
184 potion
185 potion
186 potion
187 potion
188 1gp
189 3gp
190 potion
191 nothing
192 potion
193 potion
194 1gp
195 potion
196 potion
197 potion
198 1gp
199 1gp
200 3gp
201 nothing
202 nothing
203 1gp
204 potion
205 nothing
206 potion
207 1gp
208 nothing
209 potion
210 nothing
211 potion
212 potion
213 nothing
214 phoenix
215 1gp
216 potion
217 elixir
218 potion
219 nothing
220 nothing
221 potion
222 nothing
223 potion
224 potion
225 3gp
226 1gp
227 nothing
228 nothing
229 potion
230 potion
231 nothing
232 1gp
233 potion
234 potion
235 nothing
236 1gp
237 nothing
238 nothing
239 1gp
240 nothing
241 1gp
242 nothing
243 1gp
244 nothing
245 potion
246 potion
247 1gp
248 potion
249 potion
250 potion
251 nothing
252 potion
253 potion
254 3gp
255 potion

To get the top prize 100gp you need the end seed value to be 8. Meaning you need to press the "Try it" prompt at ~253/0xFD. On playstation I found there was only a 8 frame window to do so at 6988-6996 frames since save load  (~116s) . The next window occurs 92 seconds later at frame 12524. Manipulating this window would require pressing the button in 0.133s which isn't easy and tbh you would be much better off with chocobo betting to earn gp.

14
I created a snapshot shortly after starting a game and it generates the same chocobos. If snapshots are not broken it's setting the seed at game load, not save load.

Looking at procmon I saw this address at the start of the stack 770800 and eventually found 7af06b and 7b1ecf when loading chocobo.lgp. These function call GetLocalTime and GetSystemTime.

I tried to hook the functions using detours to spoof what data they return and show a message box (https://renenyffenegger.ch/notes/Windows/tools/event-hooking/set-date-in-process/index) but it doesn't appear to be calling my hooked function with ff7.exe even though it works with notepad. I'm not sure why. Perhaps they aren't used and its calling some other time function instead or something jank about how they are called.

I'm using the non steam ff7 for pc as I'm not sure how to bypass steam ff7_en.exe calling the loader.

It would make sense for the snapshots to be working and for the PC variants to be using a variable based off time we can't manipulate.

15
It would be nice if we could save states and reload them on pc.

I installed dosbox-x with ff7 in windows 98. When you take a save state and load it, the screen goes black.
I installed yuzu with ff7 to emulate the switch version, but the game is not supported, and only plays the loading screen music with a black screen.
I installed vmware workstation and created a virtual machine with ff7 steam installed and it is able to save and reload snapshots.

So
I started the game and the moment after I accepted the race, but before the dialog closed, I swapped windows pausing the game.
I noticed the frame counter still increases even when the game is paused. This might explain the frame counter not being used.
Regardless I could then create a snapshot and run the race. When I restored the snapshot I could wait seconds later before unpausing the game and get the same chocobo stats.

That's promising.

Just as a test I took a new snapshot on the menu and restored it, I noticed I'm also get the same chocobo stats no matter what else I do... that doesn't seem right.
I took yet another new snapshot to verify. This time after loading the save and it too generates the same chocobos...
Either the vmware snapshots are causing problems or it's setting some variable on game load and race finish.
The course selector byte is still incrementing after a restore showing that randomness is not broken.

Searching memory for values that don't change during the game, but do after a race. I eventually got 9 results.
Code: [Select]
FF7_EN.exe+8FFEE0
FF7_EN.exe+9E0DF4
FF7_EN.exe+9E0DF8
FF7_EN.exe+9E0E20
FF7_EN.exe+A3B8D4
FF7_EN.exe+A3B8DC
FF7_EN.exe+A3BA30
FF7_EN.exe+A3BA38
FF7_EN.exe+A3BAF4

Freezing them all crashed the game. Individually freezing them didn't create the same stats. Neither did freezing regions of memory as groups.

I installed android-x86 with ff7 in vmware workstation and it also generates the same races per snapshot.
The same happened when I used virtualbox snapshots with android-x86.

So weird.

16
I took another go at things to try and understand the pc side.

FF7_EN.exe+5A8730 -> Frames since last button press
FF7_EN.exe+5A8718 -> Windows uptime in milliseconds
FF7_EN.exe+5A8720 -> Windows uptime in milliseconds
FF7_EN.exe+9DFEB0 -> Windows uptime in milliseconds
FF7_EN.exe+9E65BC -> Windows uptime in milliseconds
FF7_EN.exe+8C0990 -> Direction person in background walks

I froze these values by nulling out the operations that increment the counters with nops in cheat engine.

I also did the same with IGT, the byte that sets the course, and the frame counter, but each race still generates different stats...

Taking a look at ghidra

sub_772357 -> Sets memory for chocobo attributes, calling random multiple times
sub_7ae9c0(rand via fft-hackers) -> Gets a random value by calling sub_40de00 (_getptd via fft-hackers) then modifying it and returning a value less than 0x7fff (32,767)
sub_40dee0(_getptd via fft-hackers) ->

Calls TlsGetValue(dwTlsIndex) where dwTlsIndex when called from 7ae9c0 it is always 2B.

Thread local storage is a way for Windows to store a variable that can be shared between threads
https://guidedhacking.com/threads/thread-local-storage-tls-variants-how-to-read-them.17036/
Task Manager shows my ff7_en.exe to have 22 threads, modifying the process name and updating the threads 10->22 in DynamicTLSExternal.exe
Dumps some data... but it doesn't make sense to me.

Looking for references that call TlsSetValue we get:
40df16 -> Current function, updating the value if it is 0
40de86 -> Calls TlsAlloc and sets lpTlsValue using 410e30(1,0x74) -> which then gets complicated
40e008 -> retrieves the value, gets 6 sets of 4 bytes and does something with them, then sets the value to 0
7b0ad0 -> sets the value to whatever calls 7b0aa0 -> which is set to the threadid which was set from 410e30(1,0x74)

410e30 uses 007beff4
calls 40cb10(9) -> uses 7bbf08 -> starts critical section
calls 411240() -> 7beff0, does data manip with for loops -> gets more complicated
calls 40cb90(9) -> uses 7bbf08 -> ends critical section
Allocates memory on the heap
checks to make sure 9a0c14 is 0
updates 9a0c18 to be 0x74
loops

It then uses sub_410e30(calloc via fft-hackers) to allocate 116 bytes of memory and updates the value if its not zero...
Which doesn't make sense as calloc should zero the values.
Calloc itself is a tad messy and ida and ghidra show almost entirely different psuedocode for it.

In ida it heap allocates, loops a few times, calls some weird functions which also loop, and memsets to 0
In ghidra it does a heapalloc zeroing memory then does a bunch of looping changing various values
Tbh I'm not sure what's going on... It doesn't appear to be using random or system time anywhere... But something must be changing the data from one frame to the next.

If it succeeds at setting the thread value with TlsSetValue() it'll call sub_40dec0(lpTlsValue) (aka _initpd) which just takes the 20th byte and puts it to 1 and the 80th byte to the value at address 0x7bc128 (05?)

To take a different approach.

Using Cheat Engine I overwrite the return value from rand() to always give the same value (4).
This can be done by looking at the memory at 7ae9e7 and modifying the op code to be mov, eax, 4

Upon starting the chocobo betting screen the game gets stuck in a loop near 0x77737D in sub_7772ae. I'm not sure why.

Changing the value at 7ae9e7 to a different value (3) and clicking on the ff7 window will show a gray screen.
Again stuck in a loop in the same space, changing to (2) and clicking on the ff7 window and the game loads.

All the chocobos have the same stats and jockey color 88/300 in class B and run in a single file line neat.

On reruns they are the same :) too bad we had to tamper the game to do this. 

5,3,2-> gold 88/300
6,3,2-> bronze 88/300
7,3,2-> silver 88/300
8,3,2-> plat sprinting 88/300
9,3,2-> hangs -> 1 -> gold 88/300
a,3,2-> bronze 88/301
b,3,2-> silver 88/301
c,3,2-> hangs ->1 -> plat 88/301 not sprinting
d,3,2-> gold 88/301
f,3,2-> plat sprinting 88/301

So the first hang point is the jockey and if it is a multiple of 8 it's sprinting.
After some testing I found it's definitely hanging because it is waiting for a new random to get new item values.
If it already has an item it asks rand again in a loop until different.
 
1 = Potion
2 = Mega-elixir
3 = Hi potion
6 = Turbo Ether
7 = Swift Bolt

Names are stored in memory close to FF7_EN.exe+57CC74 (JOHN)
Breaking on memory write shows 0x773063 in sub_772357
Which shows the chocobo names are set via two random calls. Random is not called for each chocobo.

In a race random is called ~23,497 times
Loading the betting screen random is called 720 times

Look for what accesses FF7_EN.exe+8BF588 shows nothing... which is weird as it determines if the race is gonna be long or short.

I want to say I'm closer to solving the PC side, but the more I dig the more confusing it gets to me.


17
PSX data set complete. I wrote a tool to search it and recorded a video (https://www.youtube.com/watch?v=jxXfq9jtcNk) of it working. The tool is available on my github at https://ff7man.github.io/rng.html.

18
Taking a look at the PC side of things. Random should be baked into kernel.bin/kernel2.bin so that makes me think it should share how it's generated with the psx.

With cheat engine I found the variable that determines course length is a single byte at FF7_EN.exe+8BF588 which can be frozen using a 1ms interval as a thread always creating long or short races. That's promising.

FF7_EN.exe+5A070C appears to be incrementing with each frame. I can't progress the game frame by frame to verify, but it does increase by 600/10s. This counter starts once you load a save. The names don't match and the speed and stamina values in our time window from PSX do not work for when this value gets to 5400. I tried freezing the value but it does not create the same chocobos and neither does nulling out what increments this value. You can find it in ghidra at FUN_0040ab81 which 0xDC08B8 stores the in game time and increments the counter. Cheat engine can null out the change by searching for what modifies that address. You can also change A1 0C 07 9A 00 83 C0 01 to A1 0C 07 9A 00 90 90 90 with hxd to have it not add one to the counter. Telling cheat engine to follow what accesses this value doesn't show anything extra on the betting menu load, although it doesn't show anything for the course byte either which we know impacts the result.

When I pause with the counter not nulled or patched then unpause the game the frame counter jumps by the time it was paused, kinda weird.

19
Ok the first batch finished and the data set is confirmed working for class B short courses with PSX for the 90-100s window on actual hardware :)

https://pastebin.com/ChgYj9kD
 
You can also use a PS2 with fast load as well. I'll share once the script runs for all the classes.

Xbox/iOS/PS4/PC does not work. They must not count frames to seed the minigame. I'm not sure what it's doing, perhaps @nfitc1 could chime in.

The pointers you listed here don't contain values that change
https://forums.qhimm.com/index.php?topic=11514.0

20
Ok I wrote a macro to automate binhawk to edit the memory address, run the race, pull out the win order with opencv, dump the memory, pull the chocobo stats, and repeat.

The 10 second window is actually ~600 of the counter at 0x51568, I timed myself running from the save at the ropeway station which took about 70s, I then added 20s for padding.
At the 90 second mark you should be at frame 5400.
To make matters worse whether or not it is a short or long course is a 50/50 determined by 0x95DC8
So we'll have to guess two tickets and do 1200 automations ~1min/piece for each rank.

It's going to take a while to run this. About 20 hrs for each rank assuming the script doesn't break.

I'm a little concerned chocobo behavior might be linked to some other variable and and I'm gonna sink a lot of time to be able to only get a 95% or so level of accuracy.

I won't be able to test this until 20hrs of automation run without issue. 

21
I took another look at this and wanted to consider possible RNG manipulation and I think I've found something.

With bizhawk on the psx version I found there is a sole single 4 byte memory address at 0x51568 that when frozen will generate the same chocobo races, winning items, and end result. That address starts at 0 and increments seemingly by one for every frame after the playstation logo, I stop watched it at ~33/s. We can power cycle the console and have it always start at 0. Using the bizhawk emulator we can set the seed, frame perfectly start the race, and pull out the results, then repeat for a window of time we care about. So if it takes 60s to run up to the chocobo counter from a save we would generate the races from 60s to 70s to make a 10s window of the races where we know the result. This is a much smaller task instead of needing to generate 4.3 billion races per class, now we only need to generate 300 races per class. The timer method seems like a very viable strategy, I just need to script up something to pull out the race data to search through and confirm.

22


https://anonfiles.com/z63aZ2Ifx0/chocotrainer_zip

I wrote a python trainer to quickly edit values. Because of the extreme similarity between the memory layouts between pc and psx, it works on both pc (ff7_en.exe) and on a playstation emulator (ePSXe.exe). You will need to manually update the process name and base address used by your computer. I recommend using cheat engine to search for the name of the first chocobo (ie: PAU = 0x352130, my string encoding script can generate this) and counting up 164 bytes in the memory viewer.

Updating the jockey does not update the color displayed.

Setting 2byte values to less than 256 does not work (how I wrote the trainer, easy to change if need be).
Setting run speed -> 256 has the chocobo run at a really slow speed
Setting top speed -> 256 has the chocobo sprint at a really slow speed
Bronze is the only jockey that has a variable run speed (run2) which will adjust well above or below run1.
If stamina is set really low, none of them will choose to run fully out of stamina
Whether or not a chocobo gets stuck behind another seems to be largely luck based. I think intel helps, but idk how much.
With save states, I've observed, sometimes increasing a chocobos run speed/top speed will cause it to rank lower.

Here's the best I've done so far at guessing the winners.
Code: [Select]
#evaluator.py
from operator import attrgetter
import copy
count = 0
def merge(c1,c2):
    for i in range(len(c1)):
        c1[i].val = i
    for i in range(len(c2)):
        c2[i].val = i
    for i in range(len(c1)):
        for y in c2:
            if c1[i].name == y.name:
                c1[i].val += y.val
    return c1

def race(chocobos):
    #remove bronze/lowest jockey
    chocobos2 = []
    lowest = 0

    course = ""
    for c in chocobos:
        if c.place != 0:
            course = c.course

    for c in chocobos:
        if c.jockey != lowest:
            chocobos2.append(c)

    # we need to select 2 winners so,
    # if < 2 chocobos don't delete any
    if len(chocobos2) < 2:
        chocobos2 = chocobos

    chocobos3 = copy.deepcopy(chocobos2)
    # Sort by top speed, then jockey
    chocobos2.sort(key=attrgetter('topspeed','jockey'),reverse=True)
    # Sort by intel and run speed
    chocobos3.sort(key=attrgetter('runspeed'), reverse=True)
    #chocobos4 = copy.deepcopy(chocobos2)
    #chocobos4.sort(key=attrgetter('intel'), reverse=True)
    # check if we predicted the correct winners

    #chocobos2 = merge(chocobos2,chocobos3).copy()
    #chocobos2 = merge(chocobos2,chocobos4).copy()

    notchocobos2 = copy.deepcopy(chocobos2)
    for i in range(len(notchocobos2)):
        notchocobos2[i].val = float(i)
    nothing = 0
    for i in range(len(chocobos2)):
        chocobos2[i].val = float(i)
        # if we are a top runner bump it up
        for j in range(len(chocobos3)):
            if chocobos2[i].name == chocobos3[j].name:
                if j == 0:
                    chocobos2[i].val -= 3
                if j == 1:
                    chocobos2[i].val -= 2
                if j == 2:
                    chocobos2[i].val -= 0.5
                if j == 3:
                    chocobos2[i].val += 0
                if j == 4:
                    chocobos2[i].val += 1
                if j == 5:
                    chocobos2[i].val += 2

        # if we are smart bump it up
        if chocobos2[i].intel == 100:
            chocobos2[i].val -= 2
        # adjust for jockey course advantage/disadvantage
        if "2003" in course:
            if chocobos2[i].jockey == 3:
                chocobos2[i].val += 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val += .5
            elif chocobos2[i].jockey == 2:
                chocobos2[i].val -= 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val -= .5
            else:
                nothing +=1
        else:
            if chocobos2[i].jockey == 3:
                chocobos2[i].val -= 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val -= .5
            elif chocobos2[i].jockey == 2:
                chocobos2[i].val += 0.5
                if chocobos2[i].sprinting == 2:
                    chocobos2[i].val += .5
            else:
                nothing +=1
    chocobos2.sort(key=lambda x: (x.val,-x.topspeed, -x.jockey))
    success = 0
    if chocobos2[0].place <3:
        success +=1
    if chocobos2[1].place <3:
        success +=1
    if len(chocobos2)> 2:
        if chocobos2[2].place <3:
            success += 1
    #print("Predicted: "+str(chocobos2))
    #input("Hello")
    if success >1:
        return 1
    else:
        #print("og: "+str(notchocobos2))
        #print("predicted: "+str(chocobos2))
        chocobos2.sort(key=attrgetter('place'))
        #print("actual: "+str(chocobos2))
        #input("here")
        return 2

class Chocobo:
    def __init__(self,name,ts,stamina,sprinting,jockey,jockey2,course,order,place,intel,runspeed):
        self.topspeed = ts
        self.stamina = stamina
        self.sprinting = sprinting
        self.jockey = jockey
        self.jockey2 = jockey2
        self.name = name
        self.course = course
        self.order = order
        self.place = place
        self.intel = intel
        self.runspeed = int(runspeed)
        self.rss = int(runspeed)+int(ts)
        self.val = float(0)
    def __str__(self):
        return "name=%s speed=%s stamina=%s sprinting=%s jockey=%s ogjockey=%s course=%s order=%s place=%s intel=%s rs=%s rss=%s val=%s\n" % (self.name, self.topspeed, self.stamina/10, self.sprinting, self.jockey, self.jockey2, self.course, self.order, self.place, self.intel, self.runspeed, self.rss, self.val)
    def __repr__(self):
        return str(self)

def parse_csv(theFile):
    wins = 0
    losses = 0
    with open(theFile) as f:
        data = f.read()
    sdata = data.split("win-order,")
    for x in sdata:
        chocobos = []
        if "order-b152" in x:
            lines = x.split("\n")
            chocobos = []
            for i in range(0,len(lines)):
                if i >0 and i <7:
                    sline = lines[i].split(",")
                    name = sline[6]
                    place = sline[0]
                    jockey = int(sline[5])
                    # bronze -> silver -> gold -> plat
                    if jockey == 0: #plat
                        jockey = 3
                    elif jockey == 1: #gold
                        jockey = 2
                    elif jockey == 2: #bronze
                        jockey = 0
                    elif jockey == 3: #silver
                        jockey = 1
                    else:
                        print("not a jockey")
                    speed = sline[7]
                    stamina = sline[10]
                    end_stamina = sline[11]
                    sprinting = sline[12]
                    runspeed = sline[13]
                    intel = sline[14]
                    course = sline[17]
                    order = i-1
                    c = Chocobo(name,int(speed),int(stamina)/2,int(sprinting),jockey,jockey,course,int(order),int(place),int(intel),int(runspeed))
                    chocobos.append(c)
        if len(chocobos) == 6:
            res = race(chocobos)
            if res == 1:
             wins +=1
            elif res == 2:
             losses +=1
            else:
             print("how")
    return wins,losses

def run_rank(rank,thefile):
    w,l = parse_csv(thefile)
    avg = round(((w / (w + l) * 100)),2)
    print(rank+"\t"+str(w)+"\t"+str(l)+"\t"+str(avg))
    return w,l

if __name__ == '__main__':
    tw,tl = 0,0
    print("Class\tWins\tLosses\tAvg")
    w,l = run_rank("C","c1000.csv")
    tw += w
    tl += l
    w,l = run_rank("B","b1000.csv")
    tw += w
    tl += l
    w,l = run_rank("A","a1000.csv")
    tw += w
    tl += l
    w,l = run_rank("S","s1000.csv")
    tw += w
    tl += l
    avg = round(((tw / (tw + tl)) * 100),2)
    print("total\t"+str(tw)+"\t"+str(tl)+"\t"+str(avg))


Code: [Select]
Class Wins Losses Avg
C 940 60 94.0
B 943 57 94.3
A 957 43 95.7
S 951 49 95.1
total 3791 209 94.77


23
I learned to use python pandas and the groupby functions was able to quickly answer some questions.

Code: [Select]
import pandas as pd
adf = pd.read_csv('b1000.csv')
adf = adf.dropna()
adf = adf.drop(['order-b152','mystery_typeb156','intel_adjb124','intel_typeb104','all'],1)
adf = adf.drop(['b4-chocop','b73','b81','b104','b118','b124','b141','Cooperation','Acceleration'],1)
adf = adf.drop(['b8','b22-camera1','b38-camera2','b62','b85','b90','b114','b146','b2-jockeyp'],1)
# change rank bronze->plat->silver->gold
adf.loc[adf['jockeyb28'] == "0",'jockey'] = "3" # plat #1
adf.loc[adf['jockeyb28'] == "1",'jockey'] = "2" # gold #
adf.loc[adf['jockeyb28'] == "2",'jockey'] = "0" # bronze #0
adf.loc[adf['jockeyb28'] == "3",'jockey'] = "1" # silver #2
adf.loc[adf['Sprinting'] == "2",'Sprinting'] = "1" # Change sprinting to be 1 instead of 2
dfs = []
for i in range(0,len(adf),8):
 tempdf = adf.iloc[i:i+6].reset_index(drop=True)
 tempdf.iloc[0]['Willingness to Sprint'] = tempdf.iloc[1]['Willingness to Sprint']
 dfs.append(tempdf)
ndf = pd.concat(dfs)
cols = ndf.columns.drop(['Willingness to Sprint','name'])
ndf[cols] = ndf[cols].apply(pd.to_numeric,errors='coerce')
ndf['Willingness to Sprint'] = ndf['Willingness to Sprint'].astype(str)
ndf['name'] = ndf['name'].astype(str)

print("Sorting by Win-Order")
result = ndf.groupby(['win-order']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by Sprinting")
result = ndf.groupby(['Sprinting']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by Intelligence")
result = ndf.groupby(['Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by sprinting and intelligence")
result = ndf.groupby(['Sprinting','Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by sprinting and jockey")
result = ndf.groupby(['Sprinting','jockey']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by sprinting, jockey, and intelligence")
result = ndf.groupby(['Sprinting','jockey','Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by jockey, sprinting, and intelligence")
result = ndf.groupby(['jockey','Sprinting','Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by jockey and intelligence")
result = ndf.groupby(['jockey','Intelligence']).mean().sort_values('win-order',ascending=True)
result = ndf.groupby(['jockey','Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by jockey and course")
result = ndf.groupby(['jockey','Willingness to Sprint']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by jockey, course, and intel")
result = ndf.groupby(['jockey','Willingness to Sprint','Intelligence']).mean().sort_values('win-order',ascending=True)
print(result)

print("Sorting by jockey, course, and sprinting")
result = ndf.groupby(['jockey','Willingness to Sprint','Sprinting']).mean().sort_values('win-order',ascending=True)
print(result)

Code: [Select]
           jockeyb28  Top Speed  Raw Top Speed  Stamina  Raw Stamina  Raw Stamina2  Sprinting  Run Speed  Intelligence  jockey
win-order
0              1.294     99.497       3383.286  629.596     6295.898      2517.730      0.131   2391.816         85.75   2.010
1              1.335     98.224       3339.311  629.063     6290.610      2459.703      0.115   2359.365         75.00   1.987
2              1.407     96.652       3285.818  629.828     6298.272      2398.962      0.119   2342.110         73.05   1.881
3              1.507     95.789       3257.094  630.105     6301.026      2256.965      0.118   2338.647         71.60   1.595
4              1.663     95.841       3259.200  630.472     6304.766      2080.643      0.149   2343.339         72.85   1.057
5              1.831     96.326       3274.975  629.105     6291.018      1762.689      0.127   2324.116         70.95   0.459
The higher your top speed the more likely you are to win
On average stamina is about the same
There's a 1/8 chance you'll be sprinting
The higher your Run Speed the more likely you are to win
The higher your Intelligence the more likely you are to win
The higher your Jockey in the order bronze->silver->gold->plat the more likely you are to win

Code: [Select]
           win-order  jockeyb28  Top Speed  Raw Top Speed     Stamina  Raw Stamina  Raw Stamina2    Run Speed  Intelligence    jockey
Sprinting
0           2.492272   1.502576  97.046938    3299.790689  629.557146  6295.561152   2243.590727  2350.134135     74.775806  1.500859
1           2.553360   1.530962  97.109354    3301.028986  630.645586  6306.395257   2263.548090  2348.274045     75.494071  1.479578
There's a ~50:50 it'll be a smart or dumb sprinter. On average sprinting doesn't seem to affect: intel, top speed, run speed, or jockey values. Something we already knew from reversing but good to reinforce.
Code: [Select]
                        win-order  jockeyb28  Top Speed  Raw Top Speed     Stamina  Raw Stamina  Raw Stamina2    Run Speed    jockey
Sprinting Intelligence
0         100            2.218329   1.522526  97.063150    3300.390065  629.576819  6295.778206   2318.897959  2348.717366  1.486330
1         100            2.273902   1.550388  97.188630    3303.116279  630.591731  6305.808786   2327.726098  2350.826873  1.457364
0         50             2.761346   1.482980  97.031014    3299.201967  629.537821  6295.347958   2169.622163  2351.525719  1.515129
1         50             2.844086   1.510753  97.026882    3298.857527  630.701613  6307.005376   2196.782258  2345.618280  1.502688
If your chocobo is smart it will place on average ~0.5 ranks better

Code: [Select]
                  win-order  jockeyb28  Top Speed  Raw Top Speed     Stamina  Raw Stamina  Raw Stamina2    Run Speed  Intelligence
Sprinting jockey
0         3        1.861621        0.0  96.997706    3298.064985  629.915902  6299.232416   2990.270642  2351.064220     73.967890
          2        1.911518        1.0  96.919908    3295.415713  629.495042  6294.860412   1238.028986  2350.661327     74.980931
          1        1.921970        3.0  97.110606    3302.078788  629.346212  6293.474242   2985.032576  2347.711364     75.340909
1         3        1.928962        0.0  97.229508    3305.409836  630.103825  6300.961749   2928.617486  2337.163934     75.409836
          2        1.989362        1.0  97.239362    3305.585106  629.494681  6295.106383   1279.276596  2350.632979     74.202128
          1        2.000000        3.0  96.989899    3296.818182  631.439394  6314.393939   3066.070707  2349.363636     76.010101
0         0        4.288786        2.0  97.159754    3303.609831  629.473118  6294.694316   1754.290323  2351.125192     74.807988
1         0        4.289474        2.0  96.989474    3296.689474  631.478947  6314.463158   1760.578947  2355.505263     76.315789
Plats are most likely to win, then golds, then silvers

Code: [Select]
                                           win-order  jockeyb28  Top Speed  Raw Top Speed     Stamina  Raw Stamina  Raw Stamina2  Sprinting    Run Speed
jockey Willingness to Sprint Intelligence
2      2003                  100            1.396783        1.0  97.061662    3300.072386  629.705094  6297.083110   2090.383378   0.126005  2344.648794
3      DE03                  100            1.456044        0.0  97.156593    3303.412088  631.035714  6310.373626   2544.604396   0.148352  2353.222527
       2003                  100            1.620787        0.0  96.862360    3293.235955  629.308989  6293.202247   3441.238764   0.109551  2348.660112
1      DE03                  100            1.629921        3.0  97.223097    3305.482940  629.133858  6291.223097   2535.677165   0.133858  2344.900262
2      DE03                  100            1.670241        1.0  96.825737    3292.549598  628.994638  6289.865952    992.037534   0.117962  2349.037534
1      2003                  100            1.680307        3.0  96.913043    3295.460358  630.506394  6305.120205   3448.478261   0.132992  2347.148338
3      DE03                  50             2.061425        0.0  96.972973    3296.948403  630.078624  6300.855037   2568.179361   0.127764  2347.132678
2      2003                  50             2.118110        1.0  97.073491    3300.194226  629.325459  6293.228346   1276.524934   0.144357  2354.763780
1      DE03                  50             2.210938        3.0  97.164062    3303.692708  629.570312  6295.661458   2579.807292   0.098958  2352.401042
       2003                  50             2.226519        3.0  97.082873    3301.055249  629.223757  6292.386740   3431.577348   0.157459  2347.207182
3      2003                  50             2.313187        0.0  97.115385    3302.381868  629.302198  6293.043956   3435.837912   0.104396  2348.664835
2      DE03                  50             2.497312        1.0  96.876344    3293.865591  629.959677  6299.435484    611.454301   0.112903  2354.102151
0      2003                  100            4.078125        2.0  97.026042    3299.354167  629.473958  6294.583333   1782.341146   0.148438  2351.864583
       DE03                  100            4.270718        2.0  97.580110    3316.770718  629.497238  6295.077348   1721.218232   0.118785  2352.734807
                             50             4.347107        2.0  96.955923    3296.446281  629.187328  6291.724518   1721.327824   0.126722  2353.267218
       2003                  50             4.462141        2.0  97.005222    3298.793734  630.715405  6307.065274   1791.785901   0.114883  2349.005222
Golds burn more stamina
Golds are better at short courses, plats are better at long
Silvers are pretty close to plats on shorts and golds on longs
Bronzes are garbage

Sprinting makes them change ranking more in this case too.
Code: [Select]
jockey Willingness to Sprint Sprinting
2      2003                  1           1.637255        1.0  97.647059    3319.509804  628.745098  6287.666667   1705.245098  2351.235294     73.039216
3      DE03                  1           1.773585        0.0  97.745283    3322.113208  628.575472  6286.018868   2553.867925  2340.820755     75.471698
                             0           1.775940        0.0  96.950376    3296.475188  630.842105  6308.430075   2557.556391  2351.472180     73.308271
2      2003                  0           1.780675        1.0  96.976994    3297.102761  629.633436  6296.303681   1675.052147  2349.529141     75.000000
1      DE03                  0           1.918639        3.0  97.192308    3304.588757  629.443787  6294.325444   2557.689349  2349.183432     74.408284
       2003                  0           1.925466        3.0  97.024845    3299.444099  629.243789  6292.580745   3433.610248  2346.166149     76.319876
       DE03                  1           1.943820        3.0  97.202247    3304.550562  628.662921  6286.808989   2558.887640  2344.730337     78.651685
3      2003                  0           1.950233        0.0  97.046656    3299.709176  628.958009  6289.720062   3437.790047  2350.642302     74.650078
2      DE03                  0           2.040971        1.0  96.863429    3293.746586  629.358118  6293.432473    805.647951  2351.781487     74.962064
1      2003                  1           2.045872        3.0  96.816514    3290.504587  633.706422  6336.917431   3480.192661  2353.146789     73.853211
3      2003                  1           2.142857        0.0  96.519481    3282.415584  632.207792  6321.532468   3444.506494  2332.129870     75.324675
2      DE03                  1           2.406977        1.0  96.755814    3289.069767  630.383721  6303.930233    774.058140  2349.918605     75.581395
0      2003                  0           4.268769        2.0  97.064565    3300.714715  630.024024  6300.144144   1786.487988  2349.516517     74.549550
                             1           4.277228        2.0  96.693069    3288.257426  630.554455  6305.247525   1790.811881  2356.504950     78.217822
       DE03                  1           4.303371        2.0  97.325843    3306.258427  632.528090  6324.921348   1726.269663  2354.370787     74.157303
                             0           4.309748        2.0  97.259434    3306.641509  628.896226  6288.987421   1720.573899  2352.809748     75.078616

Taking into account golds are better than plats at short courses on speed ties we get:
Code: [Select]
Class   Wins    Losses  Avg
C       911     89      91.1
B       922     78      92.2
A       919     81      91.9
S       915     85      91.5
total   3667    333     91.67
Which is only very marginally better.

I'm not sure how to add intel or the effect of course on jockey and sprinting

Sorting by intel then run speed gives a pretty good estimate too
Code: [Select]
Class   Wins    Losses  Avg
C       898     102     89.8
B       893     107     89.3
A       903     97      90.3
S       899     101     89.9
total   3593    407     89.83
as does sorting by intel, top speed, then run speed
Code: [Select]
Class   Wins    Losses  Avg
C       918     82      91.8
B       910     90      91.0
A       914     86      91.4
S       909     91      90.9
total   3651    349     91.27

24
I wrote a script to go through the data set predict the winners by removing bronzes, sorting by top speed and jockey, and then printing win/loss stats per class.
Code: [Select]
from operator import attrgetter
def race(chocobos):
    global wins,losses
    #remove bronze/lowest jockey
    chocobos2 = []
    lowest = 0
    findLowestJockey = False
    if findLowestJockey:
        lowest = 5
        for c in chocobos:
            if c.jockey < lowest:
                lowest = c.jockey
    for c in chocobos:
        if c.jockey != lowest:
            chocobos2.append(c)

    # we need to select 2 winners so,
    # if < 2 chocobos don't delete any
    if len(chocobos2) < 2:
        chocobos2 = chocobos

    # Sort by top speed, then jockey
    chocobos2.sort(key=attrgetter('topspeed','jockey'),reverse=True)

    # check if we predicted the correct winners
    success = 0
    if chocobos2[0].place <3:
        success +=1
    if chocobos2[1].place <3:
        success +=1
    if len(chocobos2)> 2:
        if chocobos2[2].place <3:
            success += 1
    if success >1:
        wins +=1
    else:
        losses += 1

class Chocobo:
    def __init__(self,name,ts,stamina,sprinting,jockey,course,order,place):
        self.topspeed = ts
        self.stamina = stamina
        self.sprinting = sprinting
        self.jockey = jockey
        self.name = name
        self.course = course
        self.order = order
        self.place = place
    def __str__(self):
        return "name=%s speed=%s stamina=%s sprinting=%s jockey=%s course=%s order=%s place=%s\n" % (self.name, self.topspeed, self.stamina, self.sprinting, self.jockey, self.course, self.order, self.place)
    def __repr__(self):
        return str(self)

def parse_csv(theFile):
    with open(theFile) as f:
        data = f.read()
    sdata = data.split("win-order,")
    for x in sdata:
        chocobos = []
        if "order-b152" in x:
            lines = x.split("\n")
            chocobos = []
            for i in range(0,len(lines)):
                if i >0 and i <7:
                    sline = lines[i].split(",")
                    name = sline[6]
                    place = sline[0]
                    jockey = int(sline[5])
                    # bronze -> silver -> gold -> plat
                    if jockey == 0: #plat
                        jockey = 3
                    elif jockey == 1: #gold
                        jockey = 2
                    elif jockey == 2: #bronze
                        jockey = 0
                    elif jockey == 3: #silver
                        jockey = 1
                    else:
                        print("not a jockey")
                    speed = sline[7]
                    stamina = sline[9]
                    end_stamina = sline[11]
                    sprinting = sline[12]
                    runspeed = sline[13]
                    intel = sline[14]
                    course = sline[17]
                    order = i-1
                    c = Chocobo(name,int(speed),stamina,int(sprinting),jockey,course,int(order),int(place))
                    chocobos.append(c)
        if len(chocobos) == 6:
            race(chocobos)

def run_rank(rank,thefile):
    global wins,losses,totalwins,totallosses
    wins = 0
    losses = 0
    parse_csv(thefile)
    avg = round(((wins / (wins + losses) * 100)),2)
    print(rank+"\t"+str(wins)+"\t"+str(losses)+"\t"+str(avg))
    totalwins += wins
    totallosses += losses

if __name__ == '__main__':
    global wins,losses,totalwins,totallosses
    win,losses,totalwins,totallosses = 0,0,0,0
    print("Class\tWins\tLosses\tAvg")
    run_rank("C","c1000.csv")
    run_rank("B","b1000.csv")
    run_rank("A","a1000.csv")
    run_rank("S","s1000.csv")
    avg = round(((totalwins / (totalwins + totallosses)) * 100),2)
    print("total\t"+str(totalwins)+"\t"+str(totallosses)+"\t"+str(avg))

Code: [Select]
Class   Wins    Losses  Avg
C       908     92      90.8
B       921     79      92.1
A       919     81      91.9
S       914     86      91.4
total   3662    338     91.55

On cases where it's wrong a lot of them are when there are sprinters.
Digging through the data I haven't made any good correlations.
I'm guessing it has something to do with a combination of sprinting, intelligence, run speed, and jockey color.

25
I ran 1000 races for each class and collected data from each. It should be really helpful in testing anyone's theories without waiting for races to finish.

Order was collected using opencv match template

https://anonfiles.com/X01dzdF9xa/ff7_choco_races_1000_7z

We can get close to the right chocobos, but I'm not sure this is going to be solvable.
 
The point being, with the playstation version I could save state, watch the race, restore state, watch the race again, and get the same end results.
I don't have state saves with PC so I changed the jockey to always be the same and copy all the chocobo bytes, and watch the race.
I then start a new race and overwrite the new chocobo bytes with the old ones and the result is different....
If it's not the jockeys and not the stats assigned to the chocobos then some other randomness must also significantly impact how fast each chocobos runs or what movements they make, and that randomness must get generated when the minigame loads.

Pages: [1] 2