KAZIK/tools/tests/check_fortune_session_ai.py
Игорь Федосеев 5dedaf459f fix: restore fortune AI and add model setup
Restore native inference, card-bound readings and paged dialogue.

Provide manual, checksum-verified model setup instead of Git LFS so
source checkouts work without uploading multi-gigabyte weights. The
game stays offline; complete release archives still bundle the model.
2026-10-31 00:37:36 +03:00

174 lines
7.4 KiB
Python

"""Check that the shipping MASM tarot submission actually requests inference.
Builds a test-only entry point; never edits production sources or binaries.
Does not require model weights: even a missing-model error must be observable.
"""
from pathlib import Path
import struct
import subprocess
import argparse
from check_hall_expansion import ROOT, OUT, prepare
from check_artifacts import toolchain
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--with-model', action='store_true')
args = parser.parse_args()
prepare()
source = (OUT / "module.asm").read_text(encoding="cp1251")
entry = r"""
.data
fortuneAiProbePath db "build\hall-expansion\fortune-ai-probe.bin",0
fortuneAiProbeCount dd 0
fortuneAiProbeAnswerPath db "build\hall-expansion\fortune-ai-native-answer.bin",0
fortuneAiProbeStage dd 0
fortuneAiProbePagesPath db "build\hall-expansion\fortune-ai-pages.bin",0
fortuneAiProbePages dd 0
fortuneAiProbeShot db "build\hall-expansion\fortune-ai-stage-"
fortuneAiProbeDigit db "0.bmp",0
fortuneAiProbeQuestion db QUESTION_BYTES,0
.code
FortuneAiProbeStart PROC
call BeginFortuneDialog3D
invoke WindowEvents3D,0,102h,'1',0
lea esi,fortuneAiProbeQuestion
fortune_ai_probe_input:
movzx eax,BYTE PTR [esi]
test eax,eax
jz fortune_ai_probe_submit
invoke WindowEvents3D,0,102h,eax,0
inc esi
jmp fortune_ai_probe_input
fortune_ai_probe_submit:
call SubmitFortuneQuestion3D
; Pin the actual transmitted draw, before the ready frame is polled.
mov fortuneDrawCards3D,0
mov fortuneDrawCards3D[4],23
mov fortuneDrawCards3D[8],17
invoke CreateFileA,OFFSET fortuneAiProbePath,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET fortuneWorkerState3D,4,OFFSET fortuneAiProbeCount,0
invoke WriteFile,ebx,OFFSET fortuneProcess3D,16,OFFSET fortuneAiProbeCount,0
invoke WriteFile,ebx,OFFSET fortuneRead3D,4,OFFSET fortuneAiProbeCount,0
invoke WriteFile,ebx,OFFSET fortuneWrite3D,4,OFFSET fortuneAiProbeCount,0
invoke FortuneCardSpeech3D,0
invoke WriteFile,ebx,OFFSET fortuneSpeech3D,256,OFFSET fortuneAiProbeCount,0
invoke CloseHandle,ebx
; MODEL_PROBE
call StopFortuneWorker3D
invoke ExitProcess,0
FortuneAiProbeStart ENDP
end FortuneAiProbeStart
"""
question = 'Стоит ли мне начать учиться рисованию?'
entry = entry.replace('QUESTION_BYTES', ','.join(map(str, question.encode('cp1251'))))
if args.with_model:
entry = entry.replace('; MODEL_PROBE', r'''
mov esi,12000
fortune_ai_probe_wait:
call PollFortuneWorker3D
cmp fortuneWorkerState3D,5
je fortune_ai_probe_ready
cmp fortuneWorkerState3D,4
je fortune_ai_probe_fail
invoke Sleep,15
dec esi
jnz fortune_ai_probe_wait
fortune_ai_probe_fail:
call StopFortuneWorker3D
invoke ExitProcess,88
fortune_ai_probe_ready:
invoke CreateFileA,OFFSET fortuneAiProbeAnswerPath,40000000h,0,0,2,80h,0
mov ebx,eax
invoke WriteFile,ebx,OFFSET fortuneAnswer3D,fortuneAnswerLength3D,OFFSET fortuneAiProbeCount,0
invoke CloseHandle,ebx
invoke CreateFileA,OFFSET fortuneAiProbePagesPath,40000000h,0,0,2,80h,0
mov fortuneAiProbePages,eax
; Run the real scene, including each page and explicit Continue input.
fortune_ai_probe_scene:
cmp fortunePhase3D,12
je fortune_ai_probe_done
cmp fortuneReadingShown3D,1
jne fortune_ai_probe_tick
mov fortuneSpeechTime3D,20000
call RenderFortuneDialog3D
cmp fortuneSpeechHeight3D,140
ja fortune_ai_probe_fail
cmp fortuneSpeechWidth3D,204
ja fortune_ai_probe_fail
mov eax,fortuneSection3D
mov fortuneAiProbeStage,eax
add al,'0'
mov fortuneAiProbeDigit,al
invoke ExpansionSnapshot,OFFSET fortuneAiProbeShot
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSection3D,4,OFFSET fortuneAiProbeCount,0
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeechLength3D,4,OFFSET fortuneAiProbeCount,0
invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeech3D,fortuneSpeechLength3D,OFFSET fortuneAiProbeCount,0
mov mouseX3D,20
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
fortune_ai_probe_tick:
add animationClock3D,100
call UpdateFortuneSession3D
jmp fortune_ai_probe_scene
fortune_ai_probe_done:
invoke CloseHandle,fortuneAiProbePages
cmp fortuneRevealed3D,3
jne fortune_ai_probe_fail
cmp fortuneAiProbeStage,3
jne fortune_ai_probe_fail
''')
assert source.count("end ExpansionTestStart") == 1
source = source.replace("end ExpansionTestStart", 'include tools/tests/fortune_ai_test.inc\n' + entry)
source = source.replace(' invoke ExitProcess,0\nFortuneAiProbeStart ENDP',
' call FortuneAiNativeTests\n invoke ExitProcess,0\nFortuneAiProbeStart ENDP')
stem = "fortune-ai-probe"
(OUT / (stem + ".asm")).write_text(source, encoding="cp1251")
assembler, linker = toolchain()
subprocess.run([str(assembler), "/c", "/coff", "/Cp", "/nologo", "/I.",
f"/Fobuild/hall-expansion/{stem}.obj",
f"build/hall-expansion/{stem}.asm"], cwd=ROOT, check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
subprocess.run([str(linker), "/nologo", "/SUBSYSTEM:CONSOLE",
"/ENTRY:FortuneAiProbeStart@0", f"/LIBPATH:{sdk}",
f"/OUT:build/hall-expansion/{stem}.exe",
f"build/hall-expansion/{stem}.obj", "build/hall-expansion/helpers.obj",
"kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib", "ws2_32.lib"],
cwd=ROOT, check=True)
subprocess.run([str(OUT / (stem + ".exe"))], cwd=ROOT, timeout=210 if args.with_model else 30, check=True)
raw = (OUT / "fortune-ai-probe.bin").read_bytes()
state, process, thread, pid, tid, reader, writer = struct.unpack_from("<7I", raw)
speech = raw[28:].split(b"\0", 1)[0].decode("cp1251")
print(f"Native submit: state={state}, process={process}, reader={reader}, writer={writer}")
print("Card speech:", ascii(speech))
assert (state == 1 and process and reader and writer) or (state == 4 and not process), (
"Tarot submission neither starts/queues inference nor reports an AI error; "
"the actual native scene cannot produce a model reading."
)
if args.with_model:
from check_fortune_ai import check_reading
text = (OUT / 'fortune-ai-native-answer.bin').read_bytes().decode('cp1251').replace('\0', '\n')
check_reading(text, [0, 23, 17])
pages = (OUT / 'fortune-ai-pages.bin').read_bytes()
recovered = [[] for _ in range(4)]
offset = 0
while offset < len(pages):
section, size = struct.unpack_from('<II', pages, offset)
offset += 8
page = pages[offset:offset + size].decode('cp1251')
offset += size
assert section < 4 and size <= 255, (section, size)
assert len(page.splitlines()) <= 9, page
assert all(len(line) <= 23 for line in page.splitlines()), page
recovered[section].append(page)
for expected, actual in zip(text.splitlines(), recovered):
assert ' '.join(expected.split()) == ' '.join(' '.join(actual).split()), (expected, actual)
print('PASS: actual native WM_CHAR -> worker -> model -> four paged scene stages')
print("PASS: native submit, partial IPC frames, bounded parser/paging, timeout and cleanup")
if __name__ == "__main__":
main()