feat: implement native darts mini-game, AI observer mode, and fortune session assets

- Darts: native 500-point match against Max with sway/fatigue mouse aim, parabolic throw camera, raster arm animations, and 16 camera-relative flying dart views

- AI Observer: autonomous spectator mode for Hold'em, Blackjack, Durak, and Billiards with profile isolation and playback controls

- Fortune Session: raster sprite packs, mist/orb animations, tarot divination spreads, and pack tools

- Hall Integration: wall dartboard in lounge, observer terminal dispatch, and bot turn hooks

- Packaging & Verification: updated installer streaming hash, synchronized launcher, passing check suites and release smoke checks
This commit is contained in:
Artvasil09 2026-10-24 22:10:25 +03:00
parent ce06659ab2
commit 23dc40ee5c
76 changed files with 5747 additions and 1194 deletions

2
.gitignore vendored
View file

@ -30,6 +30,8 @@ __pycache__/
/build/buckshot/
/build/coin/
/build/dice/
/build/darts/
/build/observer/
/build/hall/
/build/icon_work/
/build/scratch/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -0,0 +1,80 @@
# Darts Artwork
Generated with the built-in image-generation tool. Runtime assets live in
`assets/animations/darts/`; original selected PNGs are in this directory.
All grid cells are extracted at one common scale. No separately generated limb
pieces or stretched forearms are used.
## arm-throw-v3.png
Sports game animation sprites, harmless pub darts. Create a 4x4 equal-grid
transparent sprite atlas, 16 frames, square 1536x1536. FIRST PERSON from the
thrower's eyes looking at their OWN RIGHT forearm and hand aiming a small
regulation sports dart FORWARD INTO THE SCREEN toward a distant dartboard,
NOT horizontally sideways across the picture. Strong perspective foreshortening:
the blue tail flights are closest to viewer; steel point points AWAY FROM
VIEWER, deeper into scene, above and slightly left of flights. Entire forearm
and visible bent elbow, dark shirt rolled ABOVE elbow. Right upper arm extends
toward lower right, elbow is near bottom right, forearm rises toward upper
middle. Natural darts throwing posture seen from own eyes, not another person's
side-on arm. Detailed realistic digitized retro game sprite art, natural skin,
silver knurled barrel, blue nylon shaft and blue cross flights. Row1 four similar
forward aiming poses. Row2 four gradual drawback poses bending elbow toward
viewer. Row3 four gradual FORWARD THROW poses straightening elbow and opening
fingertips; dart disappears after frame10, frame11 and12 are empty hand. Row4
four empty-handed follow-through/recovery poses, whole forearm still visible.
Same anatomy, elbow anchor, scale and lighting across frames. EACH WHOLE arm
stays inside its exact grid cell, 10 percent clear transparent margin all sides,
no overflow into neighboring cells. At most one coherent arm per cell. No scene,
no board, no faces, no borders, no text, no artifacts, no wrist-only framing, no
floating dart separated from fingers. Actual alpha transparent background.
## dart-views-v1.png
Asset sheet for a pub DARTS sports video game: ONLY a small realistic regulation
steel-tip SPORTS DART, no people, hands or other objects. 4 by 4 equal square
grid, 16 different coherent views on 1536x1536 transparent canvas. Same dart
every cell: short sharp silver steel point, cylindrical metallic silver tungsten
barrel with many machined knurled grooves and dark narrow grip rings, slim dark
blue nylon shaft, translucent rich royal-blue four-cross tail flights with
realistic curved edges and a small white stripe. Detailed realistic digitized
game rendering, NOT low-poly, not paper triangles. The sporting dart is about
14cm long and delicate, not a weapon. Rotate camera around the dart: row1 rear
three-quarter views angle 0,6,12,18 degrees; row2 24,30,36,42 degrees; row3
48,54,60,66 degrees; row4 72,78,84,90 degrees. Angle0: tail blue flights closest
to camera, point recedes straight away and is mostly hidden. Angle90: complete
clean SIDE VIEW, steel point to LEFT, blue tail to RIGHT. All intermediate angles
smoothly reveal the left-pointing silver barrel. Horizontal projected axis, no
vertical roll. Each entire dart centered in its own cell, same scale, 15 percent
transparent margin all sides. Rear views are naturally narrow in width, side
views longer, same physical scale. Studio highlights for clear metal detail,
sharp blue tail. NO background, checkerboard, grid lines, captions, text or
shadows outside the isolated dart. Actual alpha transparency.
## lounge-wall-v1.png
A production game environment texture: straight-on FLAT FRONT ELEVATION of the
wide darts-lounge wall inside a richly furnished vintage casino. Square
1024x1024 raster asset, realistic digitized 1990s adventure game art, crisp small
details. NOT a room perspective photo, no sidewalls, no floor or ceiling. Wall
occupies full image. Deep muted emerald painted plaster above waist-high dark
walnut wainscoting, brass trim, cream plaster details, burgundy accents, varied
restrained colors. Central wall area around x50%,y42% must be clean emerald
plaster with space for a separate dartboard, no board drawn. Left around
x26%,y42%: substantial black chalkboard in an aged brass frame with subtle
hand-written darts scores and tally marks. Right around x76%,y42%: illuminated
glass trophy cabinet with silver cups, darts memorabilia, folded burgundy cloth,
a framed old tournament photo. Warm-shaded brass wall lamps around x33% and
x67% at y23%, realistic pools of warm light on plaster, a small analog clock
above centre. Lower quarter: wainscot, two low cabinets and a narrow shelf for
chalk, coasters and a folded towel; leave centre lower wall clear. The composition
should read as an established pub-sports corner of an elegant casino, not an
elevator, not a small wooden box, not sci-fi, not orange all over. All objects in
true front elevation, fixed to wall, coherent scale and consistent lighting.
No people, hands, sports dart closeup, text labels, logos, watermark or frame
outside the image.
## board-v1.png
Deterministically generated exact 20-sector sports geometry, not AI artwork.
The sector order and scoring ring radii are shared with native scoring tests.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -0,0 +1,44 @@
[
{
"file": "room-v1.bin",
"width": 640,
"height": 480,
"frames": 1
},
{
"file": "shuffle-v1.bin",
"width": 256,
"height": 256,
"frames": 64
},
{
"file": "deal-v1.bin",
"width": 256,
"height": 256,
"frames": 32
},
{
"file": "gesture-v1.bin",
"width": 256,
"height": 256,
"frames": 32
},
{
"file": "orb-hands-v1.bin",
"width": 256,
"height": 256,
"frames": 32
},
{
"file": "orb-mist-v1.bin",
"width": 192,
"height": 192,
"frames": 16
},
{
"file": "cards-v1.bin",
"width": 96,
"height": 128,
"frames": 45
}
]

View file

@ -0,0 +1,60 @@
# Fortune Session Production Prompts
Generated with the built-in image generation tool, not an API CLI. Original
outputs were copied into this project. Asset compilation is deterministic and
does not make image-generation calls. These are the production specifications.
## teller-actions-v1.png
Reference: `../hall/fortune-room-v2.png`, top-row faceless hooded teller only.
Transparent production sprite sheet, exactly eight columns by five rows, forty
equal square cells. Front-facing seated upper body, navy embroidered robe,
silver crescents, dark gloves, absolutely no face under the hood. Same silhouette,
size, head and shoulder position in every cell. No table, chair, candles, scenery,
labels or borders. Detailed painted retro pixel art. Row one: eight successive
poses taking and splitting a deck and beginning overhand shuffle. Row two: eight
successive packet-transfer shuffle poses ending with the deck centered. Row
three: eight dealing poses, left hand holds the deck, right hand reaches forward,
releases a card and retracts. Row four: eight card-turning and explanatory wrist
and open-palm gestures. Row five: eight poses lifting and circling both hands
around empty space for a separately rendered crystal ball. Articulate fingers,
wrists, forearms and sleeves, no camera changes or torso bouncing. Render the
whole sheet together for consistent character identity and palette.
## seated-room-v1.png
Full-screen 4:3 retro pixel-art background. First-person seated across an intimate
round tarot table in a cramped cluttered midnight-blue occult cellar. Empty
carved high-backed chair centered around 43 percent width, curtains behind it,
no person. Dark blue velvet tabletop covers the lower 42 percent, silver edge
trim and blank central area for separate cards. Teller will be composited with
head at 43 percent width, 17 percent height and hands near 54 percent height.
Left cabinets crowded with books, scrolls, herbs, skull and trinkets; right shelves
with glass bottles, blue crystal and a hanging blue lantern. Deep shadow with
cool candlelight, muted burgundy fabric and aged silver/brass accents. Believable
untidiness, draped curtains above and at the sides. No table cards, deck, ball,
lettering, numbers, UI or screen border. Keep the upper-right speech area clear.
## tarot-symbols-v1.png
Symbolic tarot atlas, exactly six columns by four rows, 24 equal portrait cells,
no gaps, letters or labels. Objects rather than human figures; detailed retro
pixel art, thin ivory borders, navy, silver, burgundy, gold and cyan. Row-major
symbols: traveller's satchel/staff/cliff; wand/tools; pillars/moon; crown/wheat;
stone throne; ceremonial hat/crossed keys; entwined roses/heart; chariot; lion/
infinity; lantern/mountain; ornate wheel; scales/sword; inverted watch/tree;
scythe/rose/hourglass; pouring cups; horned stone mask/chains; lightning/tower;
star/water; moon/two towers; sun/sunflowers; trumpet/banner; globe/laurel. Last
two cells: symmetrical silver crescent and stars on midnight-blue card backs.
Keep each recognizable illustration completely within its consistent frame.
## crystal-mist-v1.png
Transparent VFX sprite sheet, exactly four columns by four rows, sixteen equal
square cells. Identical crystal ball on a small antique silver stand, front view,
fixed silhouette, scale and bottom-center anchor. No hands, person or table.
Pale-blue smoke spirals clockwise through one full cycle across sixteen frames.
Subtle blue sparks and lighting inside the glass, no text, faces, numbers or
symbols. Keep stand stationary; vary the internal smoke, not object placement.
Detailed painted retro pixel art, dark glass and luminous cyan-blue mist, clean
transparent corners, no matte or background.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "C1714FEA01C6AA594A1895F56CCC287E5D1F35CDC829F5C2658FF05A4CBB6ECF";
const string ExpectedPokerSha256 = "C3D857FDBF7597C976B16C5021D7F07951628E54F86FF2B7BB53F8A83FDE11CA";
[STAThread]
static void Main()

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
@ -27,7 +27,7 @@ namespace PokerGameSetup
sealed class InstallerForm : Form
{
const string Marker = "<<POKER_GAME_GUI_PAYLOAD_ZIP>>";
const string ExpectedPayloadSha256 = "718663039D0CCB6EE83C62D23C8F8F7A6D4C596135E75F757C101C568E8473E9";
const string ExpectedPayloadSha256 = "1A2AC9A543AAF354EAD5BFA4716A8677143E27D994D55120AAF4ACF8296AAA6B";
readonly Color bg = Color.FromArgb(15, 12, 10);
readonly Color panel = Color.FromArgb(28, 22, 18);
readonly Color gold = Color.FromArgb(236, 186, 75);
@ -247,19 +247,44 @@ namespace PokerGameSetup
Directory.CreateDirectory(Path.Combine(target, "runtime"));
var exe = Assembly.GetExecutingAssembly().Location;
var bytes = File.ReadAllBytes(exe);
var marker = System.Text.Encoding.UTF8.GetBytes(Marker);
var index = FindMarker(bytes, marker);
if (index < 0) throw new InvalidOperationException("В установщике не найден архив игры.");
var payloadBytes = bytes.Skip(index + marker.Length).ToArray();
VerifyPayloadIntegrity(payloadBytes);
SetProgress(0, "Поиск архива игры...");
long payloadStart = -1;
using (var fs = File.OpenRead(exe))
{
payloadStart = FindMarkerInStream(fs, marker);
}
if (payloadStart < 0) throw new InvalidOperationException("В установщике не найден архив игры.");
var tempZip = Path.Combine(Path.GetTempPath(), "assembler_casino_payload_" + Guid.NewGuid().ToString("N") + ".zip");
File.WriteAllBytes(tempZip, payloadBytes);
try
{
SetProgress(5, "Проверка целостности архива...");
using (var fs = File.OpenRead(exe))
{
fs.Position = payloadStart;
using (var outFs = File.Create(tempZip, 65536))
using (var sha = SHA256.Create())
{
var buf = new byte[65536];
int r;
while ((r = fs.Read(buf, 0, buf.Length)) > 0)
{
sha.TransformBlock(buf, 0, r, null, 0);
outFs.Write(buf, 0, r);
}
sha.TransformFinalBlock(buf, 0, 0);
var actual = BitConverter.ToString(sha.Hash).Replace("-", "");
if (!string.IsNullOrWhiteSpace(ExpectedPayloadSha256) &&
ExpectedPayloadSha256 != "TO_BE_FILLED_BY_RELEASE_BUILD" &&
!actual.Equals(ExpectedPayloadSha256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Архив игры внутри установщика был изменен. Установка остановлена.");
}
}
}
using (var zip = ZipFile.OpenRead(tempZip))
{
var files = zip.Entries.Where(e => !string.IsNullOrEmpty(e.Name)).ToList();
@ -285,23 +310,10 @@ namespace PokerGameSetup
}
finally
{
try { File.Delete(tempZip); } catch { }
try { if (File.Exists(tempZip)) File.Delete(tempZip); } catch { }
}
}
void VerifyPayloadIntegrity(byte[] payloadBytes)
{
if (string.IsNullOrWhiteSpace(ExpectedPayloadSha256) || ExpectedPayloadSha256 == "TO_BE_FILLED_BY_RELEASE_BUILD")
return;
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(payloadBytes);
var actual = BitConverter.ToString(hash).Replace("-", "");
if (!actual.Equals(ExpectedPayloadSha256, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Архив игры внутри установщика был изменен. Установка остановлена.");
}
}
void CreateDesktopShortcut(string target)
{
@ -347,12 +359,48 @@ namespace PokerGameSetup
catch { }
}
long FindMarkerInStream(Stream stream, byte[] marker)
{
var buffer = new byte[1024 * 1024];
long streamPos = 0;
int bytesRead;
int overlap = marker.Length - 1;
int carry = 0;
while ((bytesRead = stream.Read(buffer, carry, buffer.Length - carry)) > 0)
{
int total = carry + bytesRead;
int idx = FindMarker(buffer, marker, total);
if (idx >= 0)
{
return streamPos + idx + marker.Length;
}
if (total >= overlap)
{
Buffer.BlockCopy(buffer, total - overlap, buffer, 0, overlap);
carry = overlap;
streamPos += (total - overlap);
}
else
{
carry = total;
}
}
return -1;
}
int FindMarker(byte[] bytes, byte[] marker)
{
for (int i = 0; i <= bytes.Length - marker.Length; i++)
return FindMarker(bytes, marker, bytes.Length);
}
int FindMarker(byte[] bytes, byte[] marker, int length)
{
for (int i = 0; i <= length - marker.Length; i++)
{
if (bytes[i] != marker[0]) continue;
bool ok = true;
for (int j = 0; j < marker.Length; j++)
for (int j = 1; j < marker.Length; j++)
{
if (bytes[i + j] != marker[j]) { ok = false; break; }
}

Binary file not shown.

View file

@ -171,6 +171,8 @@ include src_graphics\native_dice_data.inc
include src_graphics\scratch_data.inc
include src_graphics\hell_tower_data.inc
include src_graphics\charms_data.inc
include src_graphics\observer_data.inc
include src_graphics\darts_data.inc
.data
ALIGN 4
@ -4856,7 +4858,7 @@ check_target_terminal:
check_target_observer:
cmp eax, INTERACT_OBSERVER
jne target_unported
call BeginUnavailable3D
call BeginObserver3D
ret
target_unported:
@ -5194,6 +5196,8 @@ include src_graphics\unavailable.inc
include src_graphics\fortune_dialog.inc
include src_graphics\hell_tower.inc
include src_graphics\charms.inc
include src_graphics\observer.inc
include src_graphics\darts.inc
; --- Main Loop of the 3D Casino Engine ---
StartHellTower3D PROC uses ebx esi edi
@ -5245,6 +5249,15 @@ dispatch_frame:
jz @F
call ToggleFullscreen3D
@@:
cmp obsActive3D,0
jne engine_observer_frame
cmp engineMode,MODE_OBSERVER
jne engine_observer_skip
engine_observer_frame:
call UpdateMouseLockState
call ObserverFrame3D
jmp present_frame
engine_observer_skip:
invoke Pressed3D, 1Bh
test eax, eax
jz engine_tick
@ -5343,6 +5356,12 @@ check_walk_mode:
call RenderMinimap
jmp present_frame
check_slots_mode:
cmp engineMode,MODE_DARTS
jne check_darts_done
call HandleDarts3D
call RenderDarts3D
jmp present_frame
check_darts_done:
cmp engineMode,MODE_FORTUNE
jne @F
call HandleFortuneDialog3D
@ -5620,6 +5639,7 @@ engine_shutdown:
call SaveProgress3D
invoke PlayCasinoSound3D, 0, 0, 0
call FreeRussianAssets3D
call FreeDarts3D
call FreeStartupAssets3D
call FreeCardTableAssets3D
call FreeBuckshotAssets3D

Binary file not shown.

Binary file not shown.

71
docs/DARTS.md Normal file
View file

@ -0,0 +1,71 @@
# Darts
The board is on the first-floor wall near the billiards area. It opens a native
MASM darts match against Max. No Python, browser, external engine or network is
required by the game.
## Match
- Both players start at 500. First to reach exactly zero wins.
- Three darts per visit. Visits alternate between the player and Max.
- Any scoring ring can finish. This is a custom 500-point straight-out format,
not tournament 501 double-out.
- A negative remainder is a bust: the entire visit is cancelled and play passes
to the other player. A remainder of one is valid.
- The casino bankroll and saved profile are not changed by this game.
## Controls
- Move the mouse to aim; the outlined cross marks the current hit point.
- Hold the left mouse button to draw back, release to throw.
- The hand and cross have the same smooth, mixed-phase sway. Holding the aim
longer than 1.2 seconds gradually increases fatigue, capped after 4 seconds.
Precise mouse motion is retained independently of the hall's 320px UI grid.
There is no extra
random scatter after releasing the button.
- The target sector appears beside the turn indicator (S, D or T).
- Tab opens/closes the live score sheet and pauses the current action.
- R or the lower-right button starts a new match. Escape returns to the hall.
## Visuals
The throwing line is 2700 board millimetres from the target. The initial lens
is adjusted so the board is only about 7 percent smaller on screen than the
previous 2370mm/900px view. Its original 512px texture and scoring geometry are
unchanged. Max watches the same moving aim point for a short release window;
he does not reroll a hit or bypass the sway.
The independent
scene camera follows one immutable parabolic trajectory, passes beside the
dart, returns behind it, pauses at the hit, then returns to the throwing line.
The small fading trail samples the same trajectory.
Whole-arm raster animation includes the elbow, draw-back, release and empty
follow-through. A single rigid canvas transform aligns the held dart with the
target; individual limb pieces are never stretched or assembled.
The flying dart uses sixteen detailed camera-relative raster views. Its needle
anchor is registered per view, so the tip meets the scored board coordinate.
It is not drawn from flat-colored geometric triangles.
The lounge has actual ray-projected front/side/back walls, floor and ceiling.
Chairs and the lamp table occupy fixed world planes, rather than turning to
face the camera. The wall chalkboard is rewritten with real scores and recent
visit history, including when seen from the moving camera.
## Assets And Checks
`tools/generators/pack_darts.py` is an offline asset packer. It builds the exact
20-sector board and packs the generated arm, dart views and lounge artwork.
Artwork provenance and prompts are in `assets/sprites/darts/prompts-v1.md`.
`C:/Python314/python.exe tools/tests/check_darts.py` compiles the actual MASM
scene into a native fixture and checks 67 scoring vectors, 20 complete bot
matches, busts, per-player score isolation, immutable hits, real mouse-release
events, fine high-resolution mouse input, sway/fatigue, board footprint, Tab
behavior, camera motion and profile isolation. It renders native
640x480 framebuffers, a contact sheet and a GIF under `build/darts/`.
`--parity` compares tested procedure instruction/relocation signatures to the
full shipping assembly object. The hall fixture additionally covers interaction
dispatch and the unchanged stairs, doors, bar and fortune room.

View file

@ -1,5 +1,17 @@
# Offline Fortune Teller
## Currently Shelved (2026-10-22)
The seated visual session no longer starts the worker or generates readings.
The native scene deals and reveals cards, then explicitly says that explanation
is not implemented. The crystal-ball scene is visual only, without academic
grade selection or collection of student identity details. Existing inference
sources and local model files are retained for future work, not required by the
current game. The older AI archive is not an updated visual-session release.
See `FORTUNE_SESSION.md` for the current runtime and verification.
The following documents the previous experimental neural integration.
The approved C++ exception is limited to the neural worker. MASM still owns the
room, interaction, question input, card selection, dialogue rendering, process
lifetime and all gameplay. No OpenAI API, Ollama, Python runtime, listening port,

58
docs/FORTUNE_SESSION.md Normal file
View file

@ -0,0 +1,58 @@
# Seated Fortune Session
The room interaction opens a fixed first-person view across the table. No player
position, camera or bankroll state is modified. Escape or Exit returns to walking.
All runtime input, random card selection, state transitions and rendering are MASM
in `src_graphics/fortune_dialog.inc`; Python only compiles offline image assets.
## Tarot
Enter a question (up to 240 CP1251 characters), then Ask or Enter. The question
stays visible under the table for the entire reading and cannot change midway.
Three distinct major arcana are selected in assembly, each with an orientation.
The 22 symbolic illustrations have separately packed 180-degree reversed faces.
Sequence: shuffle 3.2 s; deal cards individually 1 s each; turn a card over 0.8 s;
show its name, orientation and honest unavailable explanation 3.6 s; repeat for
the next two cards; show the overall-answer placeholder; finish. Continue can
advance a completed speech, never skip a deal or a flip. Again clears the question
only after completion; Selection can return to the choice of scenes at any time.
Speech is composited beside the teller, not an OS window. Its border grows with
the visible text. Characters reveal every 28 ms with a short upward pop. Rendering
does not advance timers: the session updates from the game clock, with bounded
steps after stalls. No neural worker or algorithmic reading generator is called.
## Crystal Ball
The second selection raises a crystal ball and animates gloved hands with swirling
blue mist. It ends with an explicit unavailable-result line. There is no real
student grading, inference of merit, identity form or automated academic decision.
Again replays the visual sequence. This limitation is deliberate, not a hidden
random grade presented as a judgement.
## Assets
Generated source art and production prompts: `assets/sprites/fortune/`.
Offline packer: `tools/generators/pack_fortune_session.py`.
Runtime packs: `assets/animations/fortune/` (ANM1 format).
Forty authored poses are arranged on one shared anchored 8x5 sheet. Shared-canvas
in-betweens yield 64 shuffle, 32 deal, 32 gesture and 32 orb-hand frames. Sixteen
mist frames, 44 oriented card faces, a back and a 640x480 room complete the scene.
Art loads only when seated and all seven packs are freed on exit/shutdown. The
seated view is independent of the walking room's existing asset packs and map.
## Verification
Run `C:/Python314/python.exe tools/tests/check_hall_expansion.py` after packing.
The fresh native x86 harness exercises real WM_CHAR input, length/backspace,
empty/double submission, all stage transitions, card uniqueness, speech growth,
question persistence, crystal-ball stages, exit during animation, asset cleanup
and unchanged player position/bank. It writes actual framebuffer BMP/PNG captures
and a 64-frame shuffle GIF under `build/hall-expansion/`. Python additionally checks
native-frame differences, question pixels, pack integrity and reversed artwork.
`--parity` checks these tested procedures against the shipping game object.
The old multi-gigabyte neural ZIP is not rebuilt by this feature. Distributions
must include the new `assets/animations/fortune/` directory with the updated game.

49
docs/OBSERVER.md Normal file
View file

@ -0,0 +1,49 @@
# Native AI Observer
The observer lounge opens a game selector rather than the old unavailable modal.
All gameplay, decisions, rendering and input remain MASM32. No neural model or
network connection is used.
## Games
- Heads-up Hold'em: the existing bot decision is shared by both seats.
- Blackjack: an autonomous first seat plays against the normal dealer.
- Durak: both throwing-in and transfer variants use the same legal-move bot.
- Billiards: both pyramid and eight-ball seats use the existing shot planner.
These are real rounds using the normal shuffled decks, settlement and billiard
physics. The observer does not generate scripted winners. Both poker and Durak
hands are visible to the spectator, but each bot still sees only its own cards
and public information. Blackjack retains the dealer's normal hidden hole card.
## Controls
Select a game with the mouse, keys 1-6, or arrows and Enter. During a match:
- Space or the pause button pauses/resumes decisions and animation.
- Tab or the speed button cycles 1x, 2x and 4x.
- N or restart starts a fresh round without adding an abandoned round to scores.
- Escape or Games returns to the selector. Escape again returns to the hall.
Completed rounds remain visible for three simulated seconds before another
round starts. Each round uses a fresh virtual stack. Scores reset when choosing
a different watching session. Human gameplay commands are discarded while watching.
## Profile Isolation
The observer snapshots the player's balance, RNG state, tournament flag, bets,
game variants and blackjack/billiard opponent settings. These are restored before
leaving. Native save, NPC-contract progress and loss-protection charms have an
observer barrier. Watching does not spend personal funds, charms, grant balance
achievements, progress contracts or write the profile.
## Verification
`C:/Python314/python.exe tools/tests/check_observer.py` compiles fresh native game
and spectator procedures against existing rendering helpers. It runs 80 complete
card rounds, checks both billiard seats, pause and profile restoration, compares
runtime files before/after, and captures actual native framebuffers.
After rebuilding the full game, `--parity` checks that the shipping procedures
match the native-tested object. `test_billiards_module.ps1` additionally exercises
ordinary human/AI billiards with observer mode disabled.

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -40,6 +40,10 @@ DrawTableHeader3D PROC hdrLabel:DWORD,hdrPot:DWORD
DrawTableHeader3D ENDP
DrawStakeControls3D PROC uses ebx enabled:DWORD
cmp obsActive3D,0
je @F
ret
@@:
mov ebx, 006D7470h
cmp enabled, 0
je @F
@ -84,6 +88,8 @@ poker_render_stake_enabled:
invoke DrawStakeControls3D, eax
mov eax, pkStreet3D
invoke DrawString8x8, 180, 6, pkStreetNames3D[eax*4], 00C4B8A0h
cmp obsActive3D,0
jne poker_observer_chips
mov eax, pkRaise3D
mov esi, OFFSET tableRaiseText3D
cmp pkState3D, PK_IDLE
@ -99,6 +105,7 @@ poker_render_blind:
pop eax
invoke FormatNumber, eax, OFFSET hudNumBuffer
invoke DrawString8x8, 12, 187, OFFSET hudNumBuffer, 00F6F4DDh
poker_observer_chips:
invoke DrawNativeChips3D, 278, 196, pkPot3D, 00B14A49h
cmp pkChipTimer3D, 320
jae @F
@ -144,6 +151,8 @@ poker_render_chip_path:
xor ebx, ebx
poker_render_hole:
mov hidden, 0
cmp obsActive3D,0
jne poker_render_hole_state
cmp ebx, 2
jb poker_render_hole_state
mov hidden, 1
@ -237,7 +246,13 @@ poker_render_status:
cmp pkState3D, PK_RESULT
jne poker_render_ai_status
mov eax, pkOutcome3D
cmp obsActive3D,0
je poker_personal_result
invoke DrawString8x8,108,103,obsResultNames3D[eax*4],00F4DF9Eh
jmp poker_result_class
poker_personal_result:
invoke DrawString8x8, 108, 103, pkResults3D[eax*4], 00F4DF9Eh
poker_result_class:
cmp pkResult3D, 1
jne poker_render_buttons
invoke NativePokerClass3D, pkScore3D
@ -315,9 +330,12 @@ bj_render_stake_enabled:
inc eax
@@:
invoke DrawStakeControls3D, eax
cmp obsActive3D,0
jne bj_observer_cards
invoke DrawString8x8, 8, 36, OFFSET textBet3D, 00DDCFB1h
invoke FormatNumber, cbBet3D, OFFSET hudNumBuffer
invoke DrawString8x8, 80, 36, OFFSET hudNumBuffer, 00F6F4DDh
bj_observer_cards:
cmp cbState3D, CB_IDLE
je bj_render_buttons
xor edi, edi

View file

@ -7,6 +7,8 @@
; Output: eax = 1 if saved (bet refunded to bankroll), 0 if not saved
; ------------------------------------------------------------------------------
TriggerCharmLossProtection PROC uses ebx ecx edx, betAmount:DWORD
cmp obsActive3D,0
jne protection_none
; 1. Check Devil's Contract (50% chance to refund full bet)
cmp charmDevilUses, 0
jle check_clover

View file

@ -5,6 +5,11 @@ cityRayY3D dd SCREEN_W dup(?)
; Exterior-only materials do not consume or overwrite the hall's texture slots.
ResolveCitySurface3D PROC material:DWORD
mov eax,material
cmp eax,160
jne @F
call ResolveDartsSurface3D
ret
@@:
cmp eax,132
jb @F
cmp eax,159

890
src_graphics/darts.inc Normal file
View file

@ -0,0 +1,890 @@
; One immutable hit point drives scoring, dart geometry and the follow camera.
.code
LoadDarts3D PROC
invoke LoadAnimPack3D,OFFSET dtBoard3D,OFFSET dtBoardPath
test eax,eax
jz dt_load_fail
cmp dtBoard3D.imageWidth,512
jne dt_load_fail
cmp dtBoard3D.height,512
jne dt_load_fail
cmp dtBoard3D.frames,1
jne dt_load_fail
invoke LoadAnimPack3D,OFFSET dtHands3D,OFFSET dtHandsPath
test eax,eax
jz dt_load_fail
cmp dtHands3D.imageWidth,256
jne dt_load_fail
cmp dtHands3D.height,256
jne dt_load_fail
cmp dtHands3D.frames,16
jne dt_load_fail
invoke LoadAnimPack3D,OFFSET dtDart3D,OFFSET dtDartPath
test eax,eax
jz dt_load_fail
cmp dtDart3D.imageWidth,256
jne dt_load_fail
cmp dtDart3D.height,256
jne dt_load_fail
cmp dtDart3D.frames,16
jne dt_load_fail
invoke LoadAnimPack3D,OFFSET dtWall3D,OFFSET dtWallPath
test eax,eax
jz dt_load_fail
cmp dtWall3D.imageWidth,512
jne dt_load_fail
cmp dtWall3D.height,512
jne dt_load_fail
cmp dtWall3D.frames,1
jne dt_load_fail
invoke LoadAnimPack3D,OFFSET dtChair3D,OFFSET dtChairPath
test eax,eax
jz dt_load_fail
invoke LoadAnimPack3D,OFFSET dtTable3D,OFFSET dtTablePath
test eax,eax
jz dt_load_fail
invoke LoadAnimPack3D,OFFSET dtRoom3D,OFFSET dtRoomPath
test eax,eax
jz dt_load_fail
cmp dtRoom3D.imageWidth,256
jne dt_load_fail
cmp dtRoom3D.height,256
jne dt_load_fail
cmp dtRoom3D.frames,12
jne dt_load_fail
mov eax,1
ret
dt_load_fail:
call FreeDarts3D
xor eax,eax
ret
LoadDarts3D ENDP
FreeDarts3D PROC
invoke FreeAnimPack3D,OFFSET dtBoard3D
invoke FreeAnimPack3D,OFFSET dtHands3D
invoke FreeAnimPack3D,OFFSET dtDart3D
invoke FreeAnimPack3D,OFFSET dtWall3D
invoke FreeAnimPack3D,OFFSET dtChair3D
invoke FreeAnimPack3D,OFFSET dtTable3D
invoke FreeAnimPack3D,OFFSET dtRoom3D
ret
FreeDarts3D ENDP
ResetDarts3D PROC
mov dtState3D,DT_AIM
mov dtTimer3D,0
mov dtRemaining3D,500
mov dtVisitStart3D,500
mov dtScores3D,500
mov dtScores3D[4],500
mov dtActor3D,0
mov dtWinner3D,0
mov dtBotTimer3D,0
mov dtBotPrepared3D,0
mov dtScoreOpen3D,0
mov dtHistoryRows3D,1
mov dtHistory3D,500
mov dtHistory3D[4],500
mov dtWallDirty3D,1
mov dtCount3D,0
mov dtThrows3D,0
mov dtVisit3D,1
mov dtLast3D,0
mov dtBust3D,0
mov dtHolding3D,0
mov dtMouseReleased3D,0
mov dtHoldTime3D,0
mov dtCamX3D,0
mov dtCamY3D,0
mov dtCamZ3D,0
mov dtLens3D,DT_FOCAL
mov dtLookX3D,0
mov dtLookY3D,0
ret
ResetDarts3D ENDP
BeginDarts3D PROC
call LoadDarts3D
xor eax,1
mov dtError3D,eax
call ResetDarts3D
mov engineMode,MODE_DARTS
mov paused3D,0
mov mouseX3D,160
mov mouseY3D,107
mov dtCursorX3D,320*256
mov dtCursorY3D,214*256
call ClearKeys3D
mov mouseClick3D,0
ret
BeginDarts3D ENDP
DartsAim3D PROC uses ebx
local amplitude:DWORD,phase:DWORD,secondary:DWORD
; Smooth mixed phases and gradual hold fatigue; never scatter after release.
mov amplitude,10
cmp dtHolding3D,0
je dt_aim_phase
mov eax,dtHoldTime3D
sub eax,1200
jle dt_aim_phase
xor edx,edx
mov ecx,350
div ecx
cmp eax,8
jbe @F
mov eax,8
@@:
add amplitude,eax
dt_aim_phase:
mov eax,dtClock3D
xor edx,edx
mov ecx,3
div ecx
and eax,511
mov phase,eax
mov ebx,tableSin[eax*4]
imul ebx,amplitude
sar ebx,16
mov eax,dtClock3D
xor edx,edx
mov ecx,5
div ecx
add eax,91
and eax,511
mov secondary,eax
mov eax,tableSin[eax*4]
imul eax,3
sar eax,16
add ebx,eax
mov eax,dtCursorX3D
sub eax,320*256
imul eax,DT_WALL
cdq
mov ecx,DT_FOCAL*256
idiv ecx
cmp dtActor3D,0
je @F
mov eax,dtBotAimX3D
@@:
add eax,ebx
cmp eax,-290
jge @F
mov eax,-290
@@:
cmp eax,290
jle @F
mov eax,290
@@:
mov dtAimX3D,eax
mov eax,phase
mov ebx,tableCos[eax*4]
mov ecx,amplitude
sub ecx,2
imul ebx,ecx
sar ebx,16
mov eax,secondary
sub eax,58
and eax,511
mov eax,tableSin[eax*4]
imul eax,3
sar eax,16
add ebx,eax
mov eax,dtCursorY3D
mov edx,214*256
sub edx,eax
imul eax,edx,DT_WALL
cdq
mov ecx,DT_FOCAL*256
idiv ecx
cmp dtActor3D,0
je @F
mov eax,dtBotAimY3D
@@:
add eax,ebx
cmp eax,-245
jge @F
mov eax,-245
@@:
cmp eax,245
jle @F
mov eax,245
@@:
mov dtAimY3D,eax
ret
DartsAim3D ENDP
; Select a target from the visible score, then commit imperfect aim before release.
DartsBotAim3D PROC uses ebx esi
local ring:DWORD,target:DWORD
mov target,20
mov ring,103
mov eax,dtRemaining3D
cmp eax,20
ja dt_bot_double
mov target,eax
mov ring,53
jmp dt_bot_sector
dt_bot_double:
cmp eax,40
ja dt_bot_special
test eax,1
jnz dt_bot_special
shr eax,1
mov target,eax
mov ring,166
jmp dt_bot_sector
dt_bot_special:
cmp eax,50
jne @F
mov dtBotDesired3D,50
mov dtBotAimX3D,0
mov dtBotAimY3D,0
jmp dt_bot_scatter
@@:
cmp eax,25
jne @F
mov dtBotDesired3D,25
mov dtBotAimX3D,0
mov dtBotAimY3D,11
jmp dt_bot_scatter
@@:
cmp eax,60
ja dt_bot_sector
xor edx,edx
mov ecx,3
div ecx
test edx,edx
jnz dt_bot_single20
mov target,eax
jmp dt_bot_sector
dt_bot_single20:
mov ring,53
dt_bot_sector:
mov eax,target
cmp ring,103
jne @F
imul eax,3
@@:
cmp ring,166
jne @F
shl eax,1
@@:
mov dtBotDesired3D,eax
xor ebx,ebx
@@:
mov eax,dtSector3D[ebx*4]
cmp eax,target
je dt_bot_direction
inc ebx
cmp ebx,20
jb @B
xor ebx,ebx
dt_bot_direction:
imul eax,ebx,512
xor edx,edx
mov ecx,20
div ecx
mov esi,eax
mov eax,tableSin[esi*4]
imul eax,ring
sar eax,16
mov dtBotAimX3D,eax
mov eax,tableCos[esi*4]
imul eax,ring
sar eax,16
mov dtBotAimY3D,eax
dt_bot_scatter:
invoke RandomRange3D,17
sub eax,8
add dtBotAimX3D,eax
invoke RandomRange3D,17
sub eax,8
add dtBotAimY3D,eax
mov dtBotPrepared3D,1
ret
DartsBotAim3D ENDP
ThrowDarts3D PROC
cmp dtState3D,DT_AIM
jne dt_throw_done
cmp dtError3D,0
jne dt_throw_done
mov eax,dtAimX3D
mov dtHitX3D,eax
mov eax,dtAimY3D
mov dtHitY3D,eax
mov dtState3D,DT_RELEASE
mov dtTimer3D,0
mov dtHolding3D,0
mov dtMouseReleased3D,0
inc dtThrows3D
dt_throw_done:
ret
ThrowDarts3D ENDP
; Board units are millimetres: 6.35/15.9 bull, 99..107 triple, 162..170 double.
DartsScore3D PROC uses ebx x:SDWORD,y:SDWORD
local angle:DWORD,oldControl:WORD,truncate:WORD
cmp x,-170
jl dt_score_miss
cmp x,170
jg dt_score_miss
cmp y,-170
jl dt_score_miss
cmp y,170
jg dt_score_miss
mov eax,x
imul eax,eax
mov ebx,y
imul ebx,ebx
add ebx,eax
cmp ebx,170*170
ja dt_score_miss
cmp ebx,40
jbe dt_score_bull
cmp ebx,253
jbe dt_score_outer_bull
fnstcw oldControl
mov ax,oldControl
or ax,0C00h
mov truncate,ax
fldcw truncate
fild x
fild y
fpatan
fmul dtAngleFactor3D
fistp angle
fldcw oldControl
mov eax,angle
add eax,65536+1638
and eax,65535
imul eax,20
shr eax,16
mov eax,dtSector3D[eax*4]
mov edx,1
cmp ebx,162*162
jae dt_score_double
cmp ebx,99*99
jb dt_score_done
cmp ebx,107*107
ja dt_score_done
mov edx,3
imul eax,3
ret
dt_score_double:
mov edx,2
shl eax,1
ret
dt_score_bull:
mov eax,50
mov edx,2
ret
dt_score_outer_bull:
mov eax,25
mov edx,1
ret
dt_score_miss:
xor eax,eax
xor edx,edx
dt_score_done:
ret
DartsScore3D ENDP
SettleDarts3D PROC uses ebx
cmp dtState3D,DT_FLIGHT
jne dt_settle_done
invoke DartsScore3D,dtHitX3D,dtHitY3D
mov dtLast3D,eax
mov dtMultiplier3D,edx
mov ebx,dtCount3D
cmp ebx,3
jae dt_settle_done
mov dtMarkScore3D[ebx*4],eax
mov ecx,dtHitX3D
mov dtMarkX3D[ebx*4],ecx
mov ecx,dtHitY3D
mov dtMarkY3D[ebx*4],ecx
inc dtCount3D
mov ecx,dtRemaining3D
sub ecx,eax
js dt_bust
dt_score_accept:
mov dtRemaining3D,ecx
jmp dt_begin_impact
dt_bust:
mov eax,dtVisitStart3D
mov dtRemaining3D,eax
mov dtBust3D,1
dt_begin_impact:
mov ebx,dtActor3D
mov eax,dtRemaining3D
mov dtScores3D[ebx*4],eax
mov dtWallDirty3D,1
test eax,eax
jnz @F
mov dtWinner3D,ebx
@@:
cmp dtRemaining3D,0
je dt_record_visit
cmp dtCount3D,3
je dt_record_visit
cmp dtBust3D,0
je dt_no_record
dt_record_visit:
call DartsRecordScore3D
dt_no_record:
mov dtState3D,DT_IMPACT
mov dtTimer3D,0
invoke PlayCasinoSound3D,OFFSET soundClick,0,1
dt_settle_done:
ret
SettleDarts3D ENDP
DartsRecordScore3D PROC uses ebx esi edi
mov ebx,dtHistoryRows3D
cmp ebx,32
jb @F
lea esi,dtHistory3D[8]
lea edi,dtHistory3D
mov ecx,62
cld
rep movsd
mov ebx,31
@@:
mov eax,dtScores3D
mov dtHistory3D[ebx*8],eax
mov eax,dtScores3D[4]
mov dtHistory3D[ebx*8+4],eax
inc ebx
mov dtHistoryRows3D,ebx
mov dtWallDirty3D,1
ret
DartsRecordScore3D ENDP
HandleDarts3D PROC
invoke Pressed3D,9
test eax,eax
jz @F
xor dtScoreOpen3D,1
mov dtHolding3D,0
mov dtMouseReleased3D,0
@@:
cmp dtScoreOpen3D,0
jne dt_handle_done
invoke Pressed3D,52h
test eax,eax
jz @F
call ResetDarts3D
@@:
cmp mouseClick3D,0
je dt_handle_game
cmp mouseY3D,218
jb dt_handle_game
cmp mouseX3D,230
jb dt_handle_game
call ResetDarts3D
mov mouseClick3D,0
dt_handle_game:
cmp dtError3D,0
jne dt_handle_done
mov eax,deltaTime3D
add dtClock3D,eax
cmp dtState3D,DT_AIM
jne dt_animate
cmp dtActor3D,0
je dt_player_aim
cmp dtBotPrepared3D,0
jne @F
call DartsBotAim3D
@@:
call DartsAim3D
mov eax,deltaTime3D
add dtBotTimer3D,eax
cmp dtBotTimer3D,900
jb dt_handle_done
mov dtHolding3D,1
mov eax,dtBotTimer3D
sub eax,900
cmp eax,280
jbe @F
mov eax,280
@@:
mov dtHoldTime3D,eax
cmp dtBotTimer3D,1220
jb dt_handle_done
invoke DartsScore3D,dtAimX3D,dtAimY3D
cmp eax,dtBotDesired3D
je dt_bot_commit
cmp dtBotTimer3D,1600
jb dt_handle_done
dt_bot_commit:
call ThrowDarts3D
jmp dt_handle_done
dt_player_aim:
cmp mouseClick3D,0
je @F
mov dtHolding3D,1
mov dtHoldTime3D,0
@@:
cmp dtHolding3D,0
je dt_player_hover
mov eax,deltaTime3D
add dtHoldTime3D,eax
cmp dtHoldTime3D,4000
jbe @F
mov dtHoldTime3D,4000
@@:
dt_player_hover:
call DartsAim3D
cmp dtHolding3D,0
jne dt_player_release
mov dtMouseReleased3D,0
jmp dt_handle_done
dt_player_release:
cmp dtMouseReleased3D,0
je dt_handle_done
call ThrowDarts3D
jmp dt_handle_done
dt_animate:
mov eax,deltaTime3D
add dtTimer3D,eax
cmp dtState3D,DT_RELEASE
jne dt_animate_flight
cmp dtTimer3D,200
jb dt_handle_done
sub dtTimer3D,200
mov dtState3D,DT_FLIGHT
jmp dt_handle_done
dt_animate_flight:
cmp dtState3D,DT_FLIGHT
jne dt_animate_impact
cmp dtTimer3D,DT_THROW_MS
jb dt_handle_done
call SettleDarts3D
jmp dt_handle_done
dt_animate_impact:
cmp dtState3D,DT_IMPACT
jne dt_animate_return
cmp dtTimer3D,1300
jb dt_handle_done
mov dtState3D,DT_WIN
cmp dtRemaining3D,0
je dt_handle_done
mov dtState3D,DT_RETURN
mov dtTimer3D,0
jmp dt_handle_done
dt_animate_return:
cmp dtState3D,DT_RETURN
jne dt_handle_done
cmp dtTimer3D,750
jb dt_handle_done
cmp dtBust3D,0
jne dt_next_visit
cmp dtCount3D,3
jb dt_return_aim
dt_next_visit:
mov dtBust3D,0
mov dtCount3D,0
inc dtVisit3D
xor dtActor3D,1
mov eax,dtActor3D
mov eax,dtScores3D[eax*4]
mov dtRemaining3D,eax
mov dtVisitStart3D,eax
dt_return_aim:
mov dtState3D,DT_AIM
mov dtTimer3D,0
mov dtMouseReleased3D,0
mov dtHolding3D,0
mov dtHoldTime3D,0
mov dtBotPrepared3D,0
mov dtBotTimer3D,0
dt_handle_done:
mov mouseClick3D,0
ret
HandleDarts3D ENDP
DartsPath3D PROC uses ebx esi timeMs:DWORD
local progress:DWORD,pathX:DWORD,pathY:DWORD,pathZ:DWORD
mov eax,timeMs
cmp eax,DT_THROW_MS
jbe @F
mov eax,DT_THROW_MS
@@:
shl eax,16
xor edx,edx
mov ecx,DT_THROW_MS
div ecx
mov progress,eax
mov ebx,eax
imul eax,DT_WALL
sar eax,16
mov pathZ,eax
mov eax,dtHitX3D
sub eax,70
imul eax,ebx
sar eax,16
add eax,70
mov pathX,eax
mov eax,65536
sub eax,ebx
imul ebx
shrd eax,edx,16
imul eax,280
sar eax,16
mov esi,eax
mov eax,dtHitY3D
add eax,35
imul eax,ebx
sar eax,16
sub eax,35
add eax,esi
mov edx,eax
mov eax,pathX
mov ecx,pathZ
ret
DartsPath3D ENDP
; Real orbit: behind -> alongside the dart -> behind the impact, with one camera basis.
DartsCamera3D PROC uses ebx esi
local progress:DWORD,weight:DWORD,timeMs:DWORD,factor:DWORD
local length2:DWORD,span:DWORD,targetZ:DWORD,deltaX:DWORD,deltaZ:DWORD
mov timeMs,0
cmp dtState3D,DT_FLIGHT
jne @F
mov eax,dtTimer3D
mov timeMs,eax
jmp dt_camera_path
@@:
cmp dtState3D,DT_IMPACT
je dt_camera_end
cmp dtState3D,DT_WIN
je dt_camera_end
cmp dtState3D,DT_RETURN
jne dt_camera_path
dt_camera_end:
mov timeMs,DT_THROW_MS
dt_camera_path:
invoke DartsPath3D,timeMs
mov dtShotX3D,eax
mov dtShotY3D,edx
mov dtShotZ3D,ecx
mov eax,timeMs
cmp eax,DT_THROW_MS
jbe @F
mov eax,DT_THROW_MS
@@:
shl eax,16
xor edx,edx
mov ecx,DT_THROW_MS
div ecx
mov progress,eax
mov ebx,eax
shr eax,8
mov eax,tableSin[eax*4]
imul eax
shrd eax,edx,16
mov weight,eax
imul eax,260
sar eax,16
mov ecx,420
sub ecx,eax
mov eax,dtShotZ3D
sub eax,ecx
jns @F
xor eax,eax
@@:
mov dtCamZ3D,eax
mov eax,weight
imul eax,300
sar eax,16
mov ecx,dtShotX3D
sub ecx,70
sub ecx,eax
mov dtCamX3D,ecx
mov eax,weight
imul eax,80
sar eax,16
add eax,dtShotY3D
add eax,35
mov dtCamY3D,eax
mov eax,dtHitX3D
imul eax,ebx
sar eax,17
mov esi,eax
mov ecx,dtShotX3D
sub ecx,eax
imul ecx,weight
sar ecx,16
add ecx,esi
mov dtLookX3D,ecx
mov eax,dtHitY3D
imul eax,ebx
sar eax,17
mov esi,eax
mov ecx,dtShotY3D
sub ecx,eax
imul ecx,weight
sar ecx,16
add ecx,esi
mov dtLookY3D,ecx
mov eax,dtShotZ3D
sub eax,DT_WALL
imul eax,weight
sar eax,16
add eax,DT_WALL
mov targetZ,eax
cmp dtState3D,DT_RETURN
jne dt_camera_basis
mov eax,dtTimer3D
cmp eax,750
jbe @F
mov eax,750
@@:
mov ecx,750
sub ecx,eax
imul eax,ecx,65536
xor edx,edx
mov ecx,750
div ecx
mov ebx,eax
mov eax,dtCamX3D
imul eax,ebx
sar eax,16
mov dtCamX3D,eax
mov eax,dtCamY3D
imul eax,ebx
sar eax,16
mov dtCamY3D,eax
mov eax,dtCamZ3D
imul eax,ebx
sar eax,16
mov dtCamZ3D,eax
mov eax,dtLookX3D
imul eax,ebx
sar eax,16
mov dtLookX3D,eax
mov eax,dtLookY3D
imul eax,ebx
sar eax,16
mov dtLookY3D,eax
dt_camera_basis:
mov eax,dtCamZ3D
imul eax,550
xor edx,edx
mov ecx,DT_WALL-420
div ecx
mov ecx,DT_FOCAL
sub ecx,eax
mov eax,weight
imul eax,160
sar eax,16
sub ecx,eax
cmp ecx,350
jge @F
mov ecx,350
@@:
mov dtLens3D,ecx
mov eax,dtLookX3D
sub eax,dtCamX3D
mov deltaX,eax
imul eax,eax
mov ebx,targetZ
sub ebx,dtCamZ3D
mov deltaZ,ebx
imul ebx,ebx
add eax,ebx
mov length2,eax
fild length2
fsqrt
fistp span
cmp span,1
jge @F
mov span,1
@@:
mov eax,deltaX
shl eax,16
cdq
idiv span
mov dtYawSin3D,eax
mov eax,deltaZ
shl eax,16
cdq
idiv span
mov dtYawCos3D,eax
mov eax,dtLookY3D
sub eax,dtCamY3D
imul eax,dtLens3D
cdq
idiv span
add eax,214
mov dtScreenY3D,eax
mov dtScreenX3D,320
ret
DartsCamera3D ENDP
DartsProject3D PROC uses ebx x:SDWORD,y:SDWORD,z:SDWORD
local deltaX:DWORD,deltaZ:DWORD,depth:DWORD,side:DWORD
mov eax,x
sub eax,dtCamX3D
mov deltaX,eax
mov ebx,z
sub ebx,dtCamZ3D
mov deltaZ,ebx
imul eax,dtYawCos3D
imul ebx,dtYawSin3D
sub eax,ebx
sar eax,16
mov side,eax
mov eax,deltaX
imul eax,dtYawSin3D
mov ebx,deltaZ
imul ebx,dtYawCos3D
add eax,ebx
sar eax,16
mov depth,eax
cmp eax,16
jg @F
mov eax,16
@@:
mov ecx,eax
mov eax,side
imul eax,dtLens3D
cdq
idiv ecx
add eax,320
push eax
mov eax,y
sub eax,dtCamY3D
imul eax,dtLens3D
cdq
idiv ecx
mov edx,dtScreenY3D
sub edx,eax
pop eax
mov ecx,depth
ret
DartsProject3D ENDP
ResolveDartsSurface3D PROC
cmp dtBoard3D.pixels,0
jne @F
invoke LoadAnimPack3D,OFFSET dtBoard3D,OFFSET dtBoardPath
@@:
mov eax,dtBoard3D.pixels
mov ecx,512
mov edx,512
ret
ResolveDartsSurface3D ENDP
DrawHallDarts3D PROC
cmp hallLevel3D,0
jne @F
invoke DrawCityVertical3D,0,FP_1*3098/100,FP_1*275/10,FP_1*285/10,FP_1*115/100,FP_1*215/100,160
@@:
ret
DrawHallDarts3D ENDP
include src_graphics\darts_render.inc

View file

@ -0,0 +1,3 @@
; Generated per-view needle anchors in the 256px source canvas.
dtDartTipX3D dd 94,70,41,24,28,21,11,3,15,9,8,11,17,10,9,7
dtDartTipY3D dd 142,139,139,139,135,135,134,133,132,131,130,130,116,118,123,128

145
src_graphics/darts_data.inc Normal file
View file

@ -0,0 +1,145 @@
; All game and camera logic is native MASM; offline tooling only packs artwork.
MODE_DARTS equ 27
INTERACT_DARTS equ 47
DT_AIM equ 0
DT_RELEASE equ 1
DT_FLIGHT equ 2
DT_IMPACT equ 3
DT_RETURN equ 4
DT_WIN equ 5
DT_WALL equ 2700
DT_FOCAL equ 950
DT_THROW_MS equ 1200
BeginDarts3D PROTO
HandleDarts3D PROTO
RenderDarts3D PROTO
LoadDarts3D PROTO
FreeDarts3D PROTO
ResetDarts3D PROTO
DartsAim3D PROTO
ThrowDarts3D PROTO
DartsCamera3D PROTO
DartsPath3D PROTO :DWORD
PrepareDartsRays3D PROTO
DrawDartsTrail3D PROTO
DrawDartsLine3D PROTO :SDWORD,:SDWORD,:SDWORD,:SDWORD,:DWORD
DartsBotAim3D PROTO
DartsRecordScore3D PROTO
UpdateDartsWall3D PROTO
DartsWallText3D PROTO :DWORD,:DWORD,:DWORD
DrawDartsScoreboard3D PROTO
DrawDartsProps3D PROTO
DrawDartsArm3D PROTO :DWORD,:SDWORD,:SDWORD
DartsScore3D PROTO :SDWORD,:SDWORD
SettleDarts3D PROTO
DrawDartsRoom3D PROTO
DrawDartsModel3D PROTO :SDWORD,:SDWORD,:SDWORD
DartsProject3D PROTO :SDWORD,:SDWORD,:SDWORD
DrawHallDarts3D PROTO
ResolveDartsSurface3D PROTO
.data
dtBoard3D AnimPack3D <>
dtHands3D AnimPack3D <>
dtDart3D AnimPack3D <>
dtWall3D AnimPack3D <>
dtChair3D AnimPack3D <>
dtTable3D AnimPack3D <>
dtRoom3D AnimPack3D <>
dtBoardPath db "assets\animations\darts\board-v1.bin",0
dtHandsPath db "assets\animations\darts\arm-throw-v3.bin",0
dtDartPath db "assets\animations\darts\dart-views-v1.bin",0
dtWallPath db "assets\animations\darts\lounge-wall-v1.bin",0
dtChairPath db "assets\animations\hall\lounge-chair-v5.bin",0
dtTablePath db "assets\animations\hall\lamp-table-v5.bin",0
dtRoomPath db "assets\animations\hall\room-materials-hd.bin",0
dtState3D dd DT_AIM
dtTimer3D dd 0
dtClock3D dd 0
dtRemaining3D dd 500
dtVisitStart3D dd 500
dtScores3D dd 500,500
dtActor3D dd 0
dtWinner3D dd 0
dtBotTimer3D dd 0
dtBotPrepared3D dd 0
dtBotAimX3D dd 0
dtBotAimY3D dd 0
dtBotDesired3D dd 60
dtCursorX3D dd 320*256
dtCursorY3D dd 214*256
dtScoreOpen3D dd 0
dtHistoryRows3D dd 1
dtHistory3D dd 64 dup(0)
dtWallDirty3D dd 1
dtVisit3D dd 1
dtCount3D dd 0
dtThrows3D dd 0
dtLast3D dd 0
dtMultiplier3D dd 0
dtBust3D dd 0
dtError3D dd 0
dtHolding3D dd 0
dtHoldTime3D dd 0
dtMouseReleased3D dd 0
dtAimX3D dd 0
dtAimY3D dd 0
dtHitX3D dd 0
dtHitY3D dd 0
dtMarkX3D dd 3 dup(0)
dtMarkY3D dd 3 dup(0)
dtMarkScore3D dd 3 dup(0)
dtShotX3D dd 0
dtShotY3D dd 0
dtShotZ3D dd 0
dtCamX3D dd 0
dtCamY3D dd 0
dtCamZ3D dd 0
dtLens3D dd DT_FOCAL
dtYawSin3D dd 0
dtYawCos3D dd 65536
dtLookX3D dd 0
dtLookY3D dd 0
dtScreenX3D dd 320
dtScreenY3D dd 214
include src_graphics\darts_art.inc
dtSector3D dd 20,1,18,4,13,6,10,15,2,17,3,19,7,16,8,11,14,9,12,5
dtAngleFactor3D REAL8 10430.37835047045
dtTitle db 196,192,208,210,209,32,53,48,48,0
dtYou db 194,219,0
dtBot db 204,192,202,209,0
dtYourTurn db 194,192,216,32,213,206,196,0
dtBotTurn db 213,206,196,32,204,192,202,209,192,0
dtYouWin db 194,219,32,207,206,193,197,196,200,203,200,0
dtBotWin db 204,192,202,209,32,207,206,193,197,196,200,203,0
dtScoreTitle db 209,215,197,210,32,204,192,210,215,192,0
; Fixed wall-axis furniture planes: axis, fixed coordinate, range, bottom/top, pack.
dtProps3D dd 0,2150,-1800,-1000,-1730,-380,OFFSET dtChair3D
dd 1,1950,1050,1850,-1730,-380,OFFSET dtChair3D
dd 1,1950,100,900,-1730,-380,OFFSET dtChair3D
dd 1,1780,850,1200,-1730,-680,OFFSET dtTable3D
dtThrow db 193,208,206,209,206,202,0
dtLeft db 206,209,210,192,203,206,209,220,0
dtVisit db 207,206,196,213,206,196,0
dtWin db 195,206,210,206,194,206,33,32,208,206,194,205,206,32,205,206,203,220,0
dtBust db 207,197,208,197,193,206,208,0
dtMiss db 204,200,204,206,0
dtNext db 196,192,203,220,216,197,0
dtAgain db 209,205,206,194,192,0
dtExit db 194,32,213,206,203,203,0
dtPrompt db 69,58,32,196,192,208,210,209,0
dtLoading db 205,197,32,211,196,192,203,206,209,220,32,199,192,195,208,211,199,200,210,220,32,196,192,208,210,209,0
dtHold db 203,202,204,0
dtBull db 66,85,76,76,0
dtDouble db 68,0
dtTriple db 84,0
dtSingle db 83,0
dtTarget db 214,197,203,220,0
.data?
dtRayX3D dd 640 dup(?)
dtRayZ3D dd 640 dup(?)
dtRayDepth3D dd 640 dup(?)
dtRayU3D dd 640 dup(?)
dtRayFront3D dd 640 dup(?)
dtRayHitX3D dd 640 dup(?)
.code

View file

@ -0,0 +1,330 @@
; A single rigid sprite transform aligns the dart axis with the live target.
DrawDartsArm3D PROC uses ebx esi edi pose:DWORD,targetX:SDWORD,targetY:SDWORD
local originX:DWORD,originY:DWORD,left:DWORD,right:DWORD,bottom:DWORD,row:DWORD,column:DWORD
local deltaX:DWORD,deltaY:DWORD,sourceX:DWORD,framePtr:DWORD
mov eax,pose
shl eax,18
add eax,dtHands3D.pixels
mov framePtr,eax
mov eax,targetX
sub eax,10
mov originX,eax
mov left,eax
add eax,363
mov right,eax
mov eax,targetY
add eax,49
mov originY,eax
add eax,182
cmp eax,400
jle @F
mov eax,400
@@:
mov bottom,eax
mov eax,originY
sub eax,182
cmp eax,32
jge @F
mov eax,32
@@:
mov row,eax
cmp left,0
jge @F
mov left,0
@@:
cmp right,640
jle dt_arm_row
mov right,640
dt_arm_row:
mov eax,row
cmp eax,bottom
jge dt_arm_done
sub eax,originY
mov deltaY,eax
mov eax,left
mov column,eax
dt_arm_column:
mov eax,column
cmp eax,right
jge dt_arm_next_row
sub eax,originX
mov deltaX,eax
sub eax,deltaY
imul eax,46341
sar eax,16
cmp eax,256
jae dt_arm_skip
mov sourceX,eax
mov eax,deltaX
add eax,deltaY
imul eax,46341
sar eax,16
cmp eax,256
jae dt_arm_skip
shl eax,8
add eax,sourceX
mov esi,framePtr
mov eax,[esi+eax*4]
test eax,80000000h
jz dt_arm_skip
and eax,00FFFFFFh
mov edx,row
imul edx,640
add edx,column
mov screenBuffer[edx*4],eax
dt_arm_skip:
inc column
jmp dt_arm_column
dt_arm_next_row:
inc row
jmp dt_arm_row
dt_arm_done:
ret
DrawDartsArm3D ENDP
; Furniture is attached to fixed world planes, never camera-facing billboards.
DrawDartsProps3D PROC uses ebx esi edi
local item:DWORD,column:DWORD,depth:DWORD,sourceU:DWORD,packPtr:DWORD
local firstY:DWORD,lastY:DWORD,row:DWORD,sourceY:DWORD,stepY:DWORD,span:DWORD
mov item,0
dt_prop_item:
imul eax,item,28
lea esi,dtProps3D[eax]
mov eax,[esi+24]
mov packPtr,eax
mov column,0
dt_prop_column:
mov ebx,column
cmp dword ptr [esi],0
jne dt_prop_side
mov ecx,dtRayZ3D[ebx*4]
mov eax,[esi+4]
sub eax,dtCamZ3D
jmp dt_prop_distance
dt_prop_side:
mov ecx,dtRayX3D[ebx*4]
mov eax,[esi+4]
sub eax,dtCamX3D
dt_prop_distance:
test ecx,ecx
jz dt_prop_next_column
shl eax,16
cdq
idiv ecx
cmp eax,40
jl dt_prop_next_column
cmp eax,dtRayDepth3D[ebx*4]
jge dt_prop_next_column
mov depth,eax
mov eax,dtRayX3D[ebx*4]
mov edi,dtCamX3D
cmp dword ptr [esi],0
je @F
mov eax,dtRayZ3D[ebx*4]
mov edi,dtCamZ3D
@@:
imul depth
shrd eax,edx,16
add eax,edi
sub eax,[esi+8]
js dt_prop_next_column
mov ecx,[esi+12]
sub ecx,[esi+8]
cmp eax,ecx
jge dt_prop_next_column
mov edi,packPtr
imul eax,[edi].AnimPack3D.imageWidth
cdq
idiv ecx
mov sourceU,eax
mov eax,[esi+20]
sub eax,dtCamY3D
imul eax,dtLens3D
cdq
idiv depth
mov ecx,dtScreenY3D
sub ecx,eax
mov firstY,ecx
mov eax,[esi+16]
sub eax,dtCamY3D
imul eax,dtLens3D
cdq
idiv depth
mov ecx,dtScreenY3D
sub ecx,eax
mov lastY,ecx
sub ecx,firstY
jle dt_prop_next_column
mov span,ecx
mov eax,[edi].AnimPack3D.height
shl eax,16
xor edx,edx
div span
mov stepY,eax
mov eax,firstY
cmp eax,32
jge @F
mov eax,32
@@:
mov row,eax
sub eax,firstY
imul eax,stepY
mov sourceY,eax
cmp lastY,400
jle dt_prop_row
mov lastY,400
dt_prop_row:
mov eax,row
cmp eax,lastY
jge dt_prop_next_column
mov eax,sourceY
shr eax,16
cmp eax,[edi].AnimPack3D.height
jae dt_prop_next_column
imul eax,[edi].AnimPack3D.imageWidth
add eax,sourceU
mov edx,[edi].AnimPack3D.pixels
mov eax,[edx+eax*4]
test eax,80000000h
jz @F
mov edx,row
imul edx,640
add edx,column
and eax,00FFFFFFh
mov screenBuffer[edx*4],eax
@@:
mov eax,stepY
add sourceY,eax
inc row
jmp dt_prop_row
dt_prop_next_column:
inc column
cmp column,640
jb dt_prop_column
inc item
cmp item,4
jb dt_prop_item
ret
DrawDartsProps3D ENDP
; Write chalk glyphs directly into the wall texture, so the score follows perspective.
DartsWallText3D PROC uses ebx esi edi px:DWORD,py:DWORD,textPtr:DWORD
local row:DWORD,column:DWORD,pen:DWORD,glyph:DWORD
mov esi,textPtr
mov eax,px
mov pen,eax
dt_chalk_char:
movzx eax,byte ptr [esi]
test eax,eax
jz dt_chalk_done
lea eax,font8x8[eax*8]
mov glyph,eax
mov row,0
dt_chalk_row:
mov eax,glyph
mov edx,row
movzx ebx,byte ptr [eax+edx]
mov column,0
dt_chalk_pixel:
test ebx,80h
jz @F
mov eax,row
add eax,py
shl eax,9
add eax,pen
add eax,column
mov edi,dtWall3D.pixels
mov dword ptr [edi+eax*4],0FFD8D9C2h
@@:
shl ebx,1
inc column
cmp column,8
jb dt_chalk_pixel
inc row
cmp row,8
jb dt_chalk_row
add pen,8
inc esi
jmp dt_chalk_char
dt_chalk_done:
ret
DartsWallText3D ENDP
UpdateDartsWall3D PROC uses ebx esi edi
cmp dtWallDirty3D,0
je dt_wall_score_done
cmp dtWall3D.pixels,0
je dt_wall_score_done
mov ebx,104
dt_chalk_clear:
mov eax,ebx
shl eax,9
add eax,32
mov edi,dtWall3D.pixels
lea edi,[edi+eax*4]
mov ecx,102
mov eax,0FF202623h
rep stosd
inc ebx
cmp ebx,282
jb dt_chalk_clear
invoke DartsWallText3D,40,108,OFFSET dtYou
invoke DartsWallText3D,86,108,OFFSET dtBot
invoke FormatNumber,dtScores3D,OFFSET hudNumBuffer
invoke DartsWallText3D,40,126,OFFSET hudNumBuffer
invoke FormatNumber,dtScores3D[4],OFFSET hudNumBuffer
invoke DartsWallText3D,86,126,OFFSET hudNumBuffer
mov ebx,dtHistoryRows3D
sub ebx,7
jns @F
xor ebx,ebx
@@:
mov esi,150
dt_chalk_history:
cmp ebx,dtHistoryRows3D
jae dt_chalk_clean
invoke FormatNumber,dtHistory3D[ebx*8],OFFSET hudNumBuffer
invoke DartsWallText3D,40,esi,OFFSET hudNumBuffer
invoke FormatNumber,dtHistory3D[ebx*8+4],OFFSET hudNumBuffer
invoke DartsWallText3D,86,esi,OFFSET hudNumBuffer
add esi,17
inc ebx
jmp dt_chalk_history
dt_chalk_clean:
mov dtWallDirty3D,0
dt_wall_score_done:
ret
UpdateDartsWall3D ENDP
DrawDartsScoreboard3D PROC uses ebx esi edi
invoke DrawBoxBorder,54,23,212,174,001A2420h,00B0A16Eh,2
invoke DrawString8x8,70,32,OFFSET dtScoreTitle,00E9DEBAh
invoke DrawString8x8,118,49,OFFSET dtYou,0090CDB9h
invoke DrawString8x8,198,49,OFFSET dtBot,00E8D7ADh
mov ebx,dtHistoryRows3D
sub ebx,10
jns @F
xor ebx,ebx
@@:
mov esi,65
dt_scoreboard_row:
cmp ebx,dtHistoryRows3D
jae dt_scoreboard_current
lea eax,[ebx+1]
invoke FormatNumber,eax,OFFSET hudNumBuffer
invoke DrawString8x8,70,esi,OFFSET hudNumBuffer,008A9C91h
invoke FormatNumber,dtHistory3D[ebx*8],OFFSET hudNumBuffer
invoke DrawString8x8,118,esi,OFFSET hudNumBuffer,00E2E2CFh
invoke FormatNumber,dtHistory3D[ebx*8+4],OFFSET hudNumBuffer
invoke DrawString8x8,198,esi,OFFSET hudNumBuffer,00E2E2CFh
add esi,10
inc ebx
jmp dt_scoreboard_row
dt_scoreboard_current:
invoke DrawBox,66,170,188,1,00758772h
invoke FormatNumber,dtScores3D,OFFSET hudNumBuffer
invoke DrawString8x8,118,180,OFFSET hudNumBuffer,00FFFFFFh
invoke FormatNumber,dtScores3D[4],OFFSET hudNumBuffer
invoke DrawString8x8,198,180,OFFSET hudNumBuffer,00FFFFFFh
ret
DrawDartsScoreboard3D ENDP

View file

@ -0,0 +1,657 @@
include src_graphics\darts_environment.inc
; One-pixel, alpha-blended trajectory, clipped to the 3D playfield.
DrawDartsLine3D PROC uses ebx esi edi x0:SDWORD,y0:SDWORD,x1:SDWORD,y1:SDWORD,opacity:DWORD
local stepX:DWORD,stepY:DWORD,steps:DWORD,px:DWORD,py:DWORD,paint:DWORD
mov eax,x1
sub eax,x0
cdq
xor eax,edx
sub eax,edx
mov ebx,eax
mov eax,y1
sub eax,y0
cdq
xor eax,edx
sub eax,edx
cmp eax,ebx
jbe @F
mov ebx,eax
@@:
cmp ebx,2048
ja dt_line_done
test ebx,ebx
jnz @F
inc ebx
@@:
mov steps,ebx
mov eax,x1
sub eax,x0
shl eax,16
cdq
idiv ebx
mov stepX,eax
mov eax,y1
sub eax,y0
shl eax,16
cdq
idiv ebx
mov stepY,eax
mov eax,x0
shl eax,16
mov px,eax
mov eax,y0
shl eax,16
mov py,eax
inc steps
dt_line_pixel:
mov eax,px
sar eax,16
cmp eax,640
jae dt_line_skip
mov edx,py
sar edx,16
cmp edx,32
jl dt_line_skip
cmp edx,400
jge dt_line_skip
imul edx,640
add edx,eax
lea edi,screenBuffer[edx*4]
mov esi,[edi]
movzx eax,byte ptr [edi]
mov ecx,219
sub ecx,eax
imul ecx,opacity
sar ecx,8
add eax,ecx
mov paint,eax
mov eax,esi
shr eax,8
and eax,255
mov ecx,210
sub ecx,eax
imul ecx,opacity
sar ecx,8
add eax,ecx
shl eax,8
or paint,eax
mov eax,esi
shr eax,16
and eax,255
mov ecx,180
sub ecx,eax
imul ecx,opacity
sar ecx,8
add eax,ecx
shl eax,16
or eax,paint
mov [edi],eax
dt_line_skip:
mov eax,stepX
add px,eax
mov eax,stepY
add py,eax
dec steps
jnz dt_line_pixel
dt_line_done:
ret
DrawDartsLine3D ENDP
DrawDartsTrail3D PROC uses ebx esi edi
local timeMs:DWORD,lastX:DWORD,lastY:DWORD,valid:DWORD,fade:DWORD,alpha:DWORD
mov eax,dtTimer3D
mov fade,256
cmp dtState3D,DT_FLIGHT
je dt_trail_start
cmp dtState3D,DT_IMPACT
jne dt_trail_done
cmp eax,300
jae dt_trail_done
imul eax,256
xor edx,edx
mov ecx,300
div ecx
mov ecx,256
sub ecx,eax
mov fade,ecx
mov eax,DT_THROW_MS
dt_trail_start:
mov timeMs,eax
mov valid,0
mov ebx,12
dt_trail_segment:
imul eax,ebx,20
mov ecx,timeMs
sub ecx,eax
js dt_trail_next
invoke DartsPath3D,ecx
invoke DartsProject3D,eax,edx,ecx
cmp ecx,60
jl dt_trail_invalid
mov esi,eax
mov edi,edx
cmp valid,0
je dt_trail_remember
mov eax,13
sub eax,ebx
imul eax,9
imul eax,fade
shr eax,8
mov alpha,eax
invoke DrawDartsLine3D,lastX,lastY,esi,edi,alpha
dt_trail_remember:
mov lastX,esi
mov lastY,edi
mov valid,1
jmp dt_trail_next
dt_trail_invalid:
mov valid,0
dt_trail_next:
dec ebx
jns dt_trail_segment
dt_trail_done:
ret
DrawDartsTrail3D ENDP
; Independent scene camera. Hall player coordinates are never borrowed or modified.
PrepareDartsRays3D PROC uses ebx esi edi
local cameraRay:DWORD,wallType:DWORD,wallT:DWORD,sideT:DWORD
xor ebx,ebx
dt_ray_column:
mov eax,ebx
sub eax,320
shl eax,16
cdq
idiv dtLens3D
mov cameraRay,eax
imul dtYawCos3D
shrd eax,edx,16
add eax,dtYawSin3D
mov dtRayX3D[ebx*4],eax
mov eax,cameraRay
imul dtYawSin3D
shrd eax,edx,16
neg eax
add eax,dtYawCos3D
mov dtRayZ3D[ebx*4],eax
mov wallT,7FFFFFFFh
mov wallType,0
test eax,eax
jz dt_ray_side
mov ecx,eax
mov eax,DT_WALL
test ecx,ecx
jns @F
mov eax,-2000
mov wallType,2
@@:
sub eax,dtCamZ3D
shl eax,16
cdq
idiv ecx
test eax,eax
jle dt_ray_side
mov wallT,eax
dt_ray_side:
mov ecx,dtRayX3D[ebx*4]
test ecx,ecx
jz dt_ray_hit
mov eax,2600
test ecx,ecx
jns @F
mov eax,-2600
@@:
sub eax,dtCamX3D
shl eax,16
cdq
idiv ecx
test eax,eax
jle dt_ray_hit
cmp eax,wallT
jge dt_ray_hit
mov wallT,eax
mov wallType,1
dt_ray_hit:
mov eax,wallT
cmp eax,1
jge @F
mov eax,1
@@:
mov dtRayDepth3D[ebx*4],eax
mov esi,eax
mov eax,dtRayX3D[ebx*4]
imul esi
shrd eax,edx,16
add eax,dtCamX3D
mov dtRayHitX3D[ebx*4],eax
cmp wallType,1
jne @F
mov eax,dtRayZ3D[ebx*4]
imul esi
shrd eax,edx,16
add eax,dtCamZ3D
@@:
imul eax,256
cdq
mov ecx,500
idiv ecx
and eax,255
mov dtRayU3D[ebx*4],eax
mov eax,wallType
mov dtRayFront3D[ebx*4],eax
inc ebx
cmp ebx,640
jb dt_ray_column
ret
PrepareDartsRays3D ENDP
; True inverse projection onto front/side/back walls, floor and ceiling.
DrawDartsRoom3D PROC uses ebx esi edi
local row:DWORD,rayY:DWORD,worldY:DWORD,depth:DWORD,sourceU:DWORD,sourceV:DWORD
call PrepareDartsRays3D
mov row,32
dt_room_row:
mov eax,dtScreenY3D
sub eax,row
shl eax,16
cdq
idiv dtLens3D
mov rayY,eax
mov eax,row
imul eax,640*4
lea edi,screenBuffer[eax]
xor ebx,ebx
dt_room_pixel:
mov eax,rayY
imul dtRayDepth3D[ebx*4]
shrd eax,edx,16
add eax,dtCamY3D
mov worldY,eax
cmp eax,-1730
jl dt_room_floor
cmp eax,1300
jg dt_room_ceiling
cmp dtRayFront3D[ebx*4],0
jne dt_room_wall
mov eax,dtRayHitX3D[ebx*4]
add eax,256
cmp eax,512
jae dt_room_wall
mov sourceU,eax
mov eax,256
sub eax,worldY
cmp eax,512
jae dt_room_wall
shl eax,9
add eax,sourceU
mov esi,dtBoard3D.pixels
mov eax,[esi+eax*4]
test eax,80000000h
jnz dt_room_store
dt_room_wall:
mov eax,570
sub eax,worldY
imul eax,512
cdq
mov ecx,1800
idiv ecx
test eax,eax
jns @F
xor eax,eax
@@:
cmp eax,511
jbe @F
mov eax,511
@@:
shl eax,9
mov sourceV,eax
mov eax,dtRayU3D[ebx*4]
and eax,15
add eax,190
cmp dtRayFront3D[ebx*4],0
jne dt_room_wall_sample
mov ecx,dtRayHitX3D[ebx*4]
add ecx,850
cmp ecx,1700
jae dt_room_wall_sample
imul eax,ecx,512
cdq
mov ecx,1700
idiv ecx
dt_room_wall_sample:
add eax,sourceV
mov esi,dtWall3D.pixels
mov eax,[esi+eax*4]
jmp dt_room_store
dt_room_floor:
mov eax,-1730
jmp dt_room_horizontal
dt_room_ceiling:
mov eax,1300
dt_room_horizontal:
sub eax,dtCamY3D
shl eax,16
cdq
idiv rayY
mov depth,eax
mov eax,dtRayX3D[ebx*4]
imul depth
shrd eax,edx,16
add eax,dtCamX3D
imul eax,256
cdq
mov ecx,500
idiv ecx
and eax,255
mov sourceU,eax
mov eax,dtRayZ3D[ebx*4]
imul depth
shrd eax,edx,16
add eax,dtCamZ3D
imul eax,256
cdq
mov ecx,500
idiv ecx
and eax,255
shl eax,8
add eax,sourceU
mov esi,dtRoom3D.pixels
add esi,5*256*256*4
mov eax,[esi+eax*4]
cmp rayY,0
jle dt_room_store
mov eax,00444943h
dt_room_store:
and eax,00FFFFFFh
stosd
inc ebx
cmp ebx,640
jb dt_room_pixel
inc row
cmp row,400
jb dt_room_row
ret
DrawDartsRoom3D ENDP
; Camera-relative raster views retain the generated metal and flight detail.
DrawDartsModel3D PROC uses ebx esi edi x:SDWORD,y:SDWORD,z:SDWORD
local deltaX:DWORD,deltaZ:DWORD,angle:DWORD,pose:DWORD
local centerX:DWORD,centerY:DWORD,depth:DWORD,span:DWORD,worldX:DWORD,worldY:DWORD
mov eax,x
sub eax,dtCamX3D
mov deltaX,eax
mov eax,z
sub eax,dtCamZ3D
mov deltaZ,eax
fild deltaX
fild deltaZ
fpatan
fabs
fmul dtAngleFactor3D
fistp angle
imul eax,angle,60
add eax,32768
shr eax,16
cmp eax,15
jbe @F
mov eax,15
@@:
mov pose,eax
mov eax,x
sub eax,70
imul eax,64
cdq
mov ecx,DT_WALL
idiv ecx
mov edx,x
sub edx,eax
mov worldX,edx
mov eax,y
sub eax,280
cmp z,DT_WALL-8
jae @F
mov eax,dtTimer3D
imul eax,560
xor edx,edx
mov ecx,DT_THROW_MS
div ecx
mov edx,y
add edx,280
sub edx,eax
mov eax,edx
@@:
imul eax,64
cdq
mov ecx,DT_WALL
idiv ecx
mov edx,y
sub edx,eax
mov worldY,edx
mov eax,z
sub eax,64
invoke DartsProject3D,worldX,worldY,eax
cmp ecx,80
jl dt_model_done
mov centerX,eax
mov centerY,edx
mov depth,ecx
imul eax,dtLens3D,160
xor edx,edx
div depth
cmp eax,240
jbe @F
mov eax,240
@@:
cmp eax,12
jae @F
mov eax,12
@@:
mov span,eax
invoke DartsProject3D,x,y,z
mov centerX,eax
mov centerY,edx
mov ebx,pose
mov eax,dtDartTipX3D[ebx*4]
imul eax,span
shr eax,8
sub centerX,eax
mov eax,dtDartTipY3D[ebx*4]
imul eax,span
shr eax,8
sub centerY,eax
invoke DrawAnimSprite3D,OFFSET dtDart3D,pose,centerX,centerY,span,span
dt_model_done:
ret
DrawDartsModel3D ENDP
RenderDarts3D PROC uses ebx esi edi
local bx0:DWORD,by0:DWORD,boardSpan:DWORD,handX:DWORD,handY:DWORD,pose:DWORD
cmp dtError3D,0
jne dt_render_error
call DartsCamera3D
call UpdateDartsWall3D
call DrawDartsRoom3D
call DrawDartsProps3D
call DrawDartsTrail3D
xor ebx,ebx
dt_render_marks:
cmp ebx,dtCount3D
jae dt_render_flight
invoke DrawDartsModel3D,dtMarkX3D[ebx*4],dtMarkY3D[ebx*4],DT_WALL-4
inc ebx
jmp dt_render_marks
dt_render_flight:
cmp dtState3D,DT_FLIGHT
jne dt_render_hand
invoke DartsProject3D,dtShotX3D,dtShotY3D,dtShotZ3D
cmp ecx,140
jl dt_render_hand
invoke DrawDartsModel3D,dtShotX3D,dtShotY3D,dtShotZ3D
dt_render_hand:
mov pose,0
cmp dtState3D,DT_AIM
je dt_render_aim_hand
cmp dtState3D,DT_RELEASE
je dt_render_release_hand
cmp dtState3D,DT_FLIGHT
jne dt_render_hud
cmp dtTimer3D,250
jae dt_render_hud
mov eax,dtTimer3D
xor edx,edx
mov ecx,50
div ecx
add eax,12
cmp eax,15
jbe dt_render_pose
mov eax,15
jmp dt_render_pose
dt_render_release_hand:
mov eax,dtTimer3D
imul eax,3
xor edx,edx
mov ecx,200
div ecx
add eax,8
cmp eax,10
jne dt_render_pose
mov eax,11
jmp dt_render_pose
dt_render_aim_hand:
mov eax,0
cmp dtHolding3D,0
je dt_render_pose
mov eax,dtHoldTime3D
imul eax,4
xor edx,edx
mov ecx,280
div ecx
cmp eax,3
jbe @F
mov eax,3
@@:
add eax,4
dt_render_pose:
mov pose,eax
invoke DartsProject3D,dtAimX3D,dtAimY3D,DT_WALL
mov bx0,eax
mov by0,edx
add eax,16
mov handX,eax
mov eax,edx
sub eax,110
mov handY,eax
invoke DrawDartsArm3D,pose,bx0,by0
cmp dtState3D,DT_AIM
jne dt_render_hud
mov eax,bx0
sub eax,7
mov edx,by0
sub edx,2
invoke DrawBuckshotPanel3D,eax,edx,15,5,00101112h
mov eax,by0
sub eax,7
mov edx,bx0
sub edx,2
invoke DrawBuckshotPanel3D,edx,eax,5,15,00101112h
mov eax,bx0
sub eax,6
invoke DrawBuckshotPanel3D,eax,by0,13,2,00F6E6ADh
mov eax,by0
sub eax,6
invoke DrawBuckshotPanel3D,bx0,eax,2,13,00F6E6ADh
dt_render_hud:
invoke DrawBox,0,0,320,16,00151D20h
invoke DrawString8x8,8,4,OFFSET dtTitle,00E8D7ADh
invoke DrawString8x8,112,4,OFFSET dtYou,0090CDB9h
invoke FormatNumber,dtScores3D,OFFSET hudNumBuffer
invoke DrawString8x8,140,4,OFFSET hudNumBuffer,00FFFFFFh
invoke DrawString8x8,204,4,OFFSET dtBot,00E8D7ADh
invoke FormatNumber,dtScores3D[4],OFFSET hudNumBuffer
invoke DrawString8x8,248,4,OFFSET hudNumBuffer,00FFFFFFh
invoke DrawBox,0,200,320,40,00151D20h
mov ebx,OFFSET dtYourTurn
cmp dtActor3D,0
je @F
mov ebx,OFFSET dtBotTurn
@@:
cmp dtState3D,DT_WIN
jne dt_hud_actor
mov ebx,OFFSET dtYouWin
cmp dtWinner3D,0
je dt_hud_actor
mov ebx,OFFSET dtBotWin
dt_hud_actor:
invoke DrawString8x8,8,206,ebx,00E8D7ADh
xor ebx,ebx
dt_render_scores:
imul esi,ebx,52
add esi,8
invoke DrawBoxBorder,esi,220,46,16,00293436h,00728689h,1
cmp ebx,dtCount3D
jae @F
invoke FormatNumber,dtMarkScore3D[ebx*4],OFFSET hudNumBuffer
add esi,7
invoke DrawString8x8,esi,224,OFFSET hudNumBuffer,00FFFFFFh
@@:
inc ebx
cmp ebx,3
jb dt_render_scores
invoke DrawBoxBorder,230,220,82,16,00293436h,00728689h,1
invoke DrawString8x8,248,224,OFFSET dtAgain,00FFFFFFh
cmp dtState3D,DT_AIM
jne @F
invoke DartsScore3D,dtAimX3D,dtAimY3D
mov esi,OFFSET dtSingle
cmp edx,3
jne dt_preview_double
mov esi,OFFSET dtTriple
jmp dt_preview_sector
dt_preview_double:
cmp edx,2
jne dt_preview_sector
mov esi,OFFSET dtDouble
dt_preview_sector:
test edx,edx
jz dt_preview_value
mov ecx,edx
xor edx,edx
div ecx
dt_preview_value:
mov edi,eax
invoke DrawString8x8,176,206,OFFSET dtTarget,00AABDB6h
invoke DrawString8x8,224,206,esi,00F6E6ADh
invoke FormatNumber,edi,OFFSET hudNumBuffer
invoke DrawString8x8,236,206,OFFSET hudNumBuffer,00FFFFFFh
jmp dt_render_done
@@:
cmp dtState3D,DT_RELEASE
je dt_render_done
cmp dtState3D,DT_FLIGHT
je dt_render_done
cmp dtState3D,DT_RETURN
je dt_render_done
mov ebx,OFFSET dtWin
cmp dtRemaining3D,0
je dt_render_done
mov ebx,OFFSET dtBust
cmp dtBust3D,0
jne dt_render_banner
mov ebx,OFFSET dtMiss
cmp dtLast3D,0
je dt_render_banner
mov ebx,OFFSET dtThrow
dt_render_banner:
invoke DrawString8x8,192,206,ebx,00E8D7ADh
dt_render_done:
cmp dtScoreOpen3D,0
je @F
call DrawDartsScoreboard3D
@@:
ret
dt_render_error:
invoke DrawBox,0,0,320,240,00151D20h
invoke DrawString8x8,12,104,OFFSET dtLoading,00E8D7ADh
ret
RenderDarts3D ENDP

View file

@ -85,6 +85,11 @@ dk_render_ai_card:
shr edx, 1
add eax, edx
mov posX, eax
mov hidden,1
cmp obsActive3D,0
je @F
mov hidden,0
@@:
cmp dkState3D, DK_DEAL
jne @F
mov eax, ebx
@ -94,10 +99,10 @@ dk_render_ai_card:
mov edx, dkTimer3D
sub edx, eax
js dk_render_next_ai
invoke DrawSlidingCard3D, posX, 48, durakAIHand[ebx*4], 1, edx
invoke DrawSlidingCard3D, posX, 48, durakAIHand[ebx*4], hidden, edx
jmp dk_render_next_ai
@@:
invoke DrawNativeCard3D, posX, 48, durakAIHand[ebx*4], 1, 34
invoke DrawNativeCard3D, posX, 48, durakAIHand[ebx*4], hidden, 34
dk_render_next_ai:
inc ebx
jmp dk_render_ai_card

View file

@ -300,10 +300,13 @@ eight_finish_done:
EightFinish3D ENDP
EightHand3D PROC uses ebx esi
cmp obsActive3D,0
jne eight_hand_bot
cmp poolPlayer3D,1
jne eight_hand_human
cmp poolAI3D,0
je eight_hand_human
eight_hand_bot:
mov esi,55*FP_1
eight_hand_row:
mov ebx,40*FP_1

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,7 @@
MODE_FORTUNE equ 25
FORTUNE_SUBMODE_MENU equ 0
FORTUNE_SUBMODE_TAROT equ 1
FORTUNE_SUBMODE_ORB equ 2
BeginFortuneDialog3D PROTO
HandleFortuneDialog3D PROTO
RenderFortuneDialog3D PROTO
@ -7,61 +10,116 @@ StopFortuneWorker3D PROTO
PollFortuneWorker3D PROTO
SubmitFortuneQuestion3D PROTO
FortuneWrap3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
InitFortuneMindState3D PROTO
GenerateTarotReadingMind3D PROTO
GenerateStudentGradeMind3D PROTO
DetectTopic3D PROTO :DWORD
FortuneStrLen PROTO :DWORD
FortuneStrCat PROTO :DWORD,:DWORD
SubmitStudentGrade3D PROTO
DrawFortuneCardBox3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
CreatePipe PROTO :DWORD,:DWORD,:DWORD,:DWORD
SetHandleInformation PROTO :DWORD,:DWORD,:DWORD
CreateProcessA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
PeekNamedPipe PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
TerminateProcess PROTO :DWORD,:DWORD
GetExitCodeProcess PROTO :DWORD,:DWORD
include src_graphics\fortune_mind_data.inc
UpdateFortuneSession3D PROTO
SetFortuneSpeech3D PROTO :DWORD
FortuneCardSpeech3D PROTO :DWORD
DrawFortuneSpeech3D PROTO
DrawFortuneSessionCard3D PROTO :DWORD
.data
fortuneWorkerPath3D db "ai\fortune-engine.exe",0
fortuneWorkerCommand3D db "ai\fortune-engine.exe",0
fortuneNullName3D db "NUL",0
fortunePipeSecurity3D dd 12,0,1
fortuneStartup3D dd 17 dup(0)
fortuneSessionPacks3D LABEL BYTE
fortuneSessionRoom3D AnimPack3D <>
fortuneSessionShuffle3D AnimPack3D <>
fortuneSessionDeal3D AnimPack3D <>
fortuneSessionGesture3D AnimPack3D <>
fortuneSessionOrbHands3D AnimPack3D <>
fortuneSessionMist3D AnimPack3D <>
fortuneSessionCards3D AnimPack3D <>
fortuneSessionPath0 db "assets\animations\fortune\room-v1.bin",0
fortuneSessionPath1 db "assets\animations\fortune\shuffle-v1.bin",0
fortuneSessionPath2 db "assets\animations\fortune\deal-v1.bin",0
fortuneSessionPath3 db "assets\animations\fortune\gesture-v1.bin",0
fortuneSessionPath4 db "assets\animations\fortune\orb-hands-v1.bin",0
fortuneSessionPath5 db "assets\animations\fortune\orb-mist-v1.bin",0
fortuneSessionPath6 db "assets\animations\fortune\cards-v1.bin",0
fortuneSessionPaths3D dd OFFSET fortuneSessionPath0,OFFSET fortuneSessionPath1,OFFSET fortuneSessionPath2,OFFSET fortuneSessionPath3
dd OFFSET fortuneSessionPath4,OFFSET fortuneSessionPath5,OFFSET fortuneSessionPath6
fortuneSubMode3D dd 0
fortunePhase3D dd 0
fortunePhaseTime3D dd 0
fortuneSessionClock3D dd 0
fortuneSpeechTime3D dd 0
fortuneSpeechLength3D dd 0
fortuneSpeechVisible3D dd 0
fortuneSpeechWidth3D dd 0
fortuneSpeechHeight3D dd 0
fortuneQuestionLength3D dd 0
fortuneQuestion3D db 241 dup(0)
fortuneDrawCards3D dd 3 dup(0)
fortuneCardsDrawn3D dd 0
fortuneDealt3D dd 0
fortuneRevealed3D dd 0
fortuneWorkerState3D dd 0
fortuneProcess3D dd 4 dup(0)
fortuneRead3D dd 0
fortuneWrite3D dd 0
fortuneChildRead3D dd 0
fortuneChildWrite3D dd 0
fortuneNull3D dd 0
fortuneWorkerState3D dd 0 ; 0=off, 1=loading, 2=ready, 3=generating, 4=error
fortuneWorkerTick3D dd 0
fortuneReceiveLength3D dd -1
fortuneReceiveHeader3D dd 3 dup(0)
fortuneRequest3D dd 31514346h,0,0,0,0
fortuneIo3D dd 0
fortuneAvailable3D dd 0
fortuneQuestionLength3D dd 0
fortuneQuestion3D db 241 dup(0)
fortuneAnswer3D db 8193 dup(0)
fortuneBackground3D dd SCREEN_SIZE dup(0)
fortuneSpeech3D db 256 dup(0)
fortuneLine3D db 73 dup(0)
fortuneGlyph3D db 0,0
fortuneCaret3D db "_",0
fortuneCaretX3D dd 24
fortuneCaretY3D dd 402
fortunePage3D dd 0
fortuneMore3D dd 0
fortuneShort3D dd 0
fortuneLine3D db 73 dup(0)
fortuneCaretX3D dd 24
fortuneCaretY3D dd 366
fortuneCaret3D db "_",0
fortuneDurations3D dd 0,3200,1000,1000,1000,800,3600,800,3600,800,3600,3600,0
fortuneRoleTexts3D dd OFFSET fortuneFirst3D,OFFSET fortuneSecond3D,OFFSET fortuneThird3D
fortuneTitle3D db 195,192,196,192,203,202,192,0
fortuneLoading3D db 207,240,238,225,243,230,228,224,254,32,234,224,240,242,251,46,46,46,0
fortuneThinking3D db 202,224,240,242,251,32,238,242,226,229,247,224,254,242,46,46,46,0
fortuneReady3D db 202,224,234,238,233,32,226,238,239,240,238,241,32,242,229,225,255,32,239,240,232,226,184,235,63,0
fortuneError3D db 205,229,32,243,228,224,235,238,241,252,32,231,224,239,243,241,242,232,242,252,32,237,229,233,240,238,241,229,242,252,46,32
db 207,240,238,226,229,240,252,32,234,238,236,239,235,229,234,242,32,232,227,240,251,46,0
fortuneAsk3D db 209,207,208,206,209,200,210,220,0
fortuneExit3D db 194,219,201,210,200,0
fortuneDisclaimer3D db 208,224,241,234,235,224,228,32,228,235,255,32,240,224,231,236,251,248,235,229,237,232,255,44,32
db 237,229,32,228,238,241,242,238,226,229,240,237,238,229,32,239,240,229,228,241,234,224,231,224,237,232,229,46,0
fortunePartial3D db 206,242,226,229,242,32,241,238,234,240,224,249,184,237,0
fortuneLeft3D db "<",0
fortuneRight3D db ">",0
fortuneAsk3D db 209,207,208,206,209,200,210,220,0
fortuneAgain3D db 197,217,168,32,208,192,199,0
fortuneMenu3D db 194,219,193,206,208,0
fortuneTarot3D db 195,192,196,192,205,200,197,0
fortuneFate3D db 208,197,216,197,205,200,197,32,209,211,196,220,193,219,0
fortuneWelcome3D db 215,242,238,32,242,229,225,255,32,239,240,232,226,229,235,238,63,0
fortuneReady3D db 202,224,234,238,233,32,226,238,239,240,238,241,10,242,229,225,255,32,239,240,232,226,184,235
db 63,0
fortuneShuffle3D db 210,192,209,206,194,192,205,200,197,0
fortuneDeal3D db 208,192,209,202,203,192,196,0
fortuneReveal3D db 202,192,208,210,219,32,206,210,202,208,219,194,192,222,210,209,223,0
fortuneSummary3D db 194,241,229,32,242,240,232,32,234,224,240,242,251,32,238,242,234,240,251,242,251,46,10,206
db 225,249,232,233,32,238,242,226,229,242,32,237,224,32,226,238,239,240,238,241,10,239,238,234
db 224,32,237,229,32,240,229,224,235,232,231,238,226,224,237,46,0
fortuneUnavailable3D db 207,238,234,224,32,237,229,32,236,238,227,243,32,238,225,250,255,241,237,232,242,252,10,231
db 237,224,247,229,237,232,229,32,253,242,238,233,32,234,224,240,242,251,46,0
fortuneOrbText3D db 216,224,240,32,236,238,235,247,232,242,46,10,208,229,248,229,237,232,229,32,239,238,234,224
db 32,237,229,32,240,229,224,235,232,231,238,226,224,237,238,46,0
fortuneOrbStatus3D db 213,208,211,209,210,192,203,220,205,219,201,32,216,192,208,0
fortuneMissing3D db 205,229,32,243,228,224,235,238,241,252,32,231,224,227,240,243,231,232,242,252,32,232,231,238,225,240,224,230,229,237,232,255,46,0
fortuneContinue3D db 196,192,203,220,216,197,0
fortuneQuestionLabel3D db 210,194,206,201,32,194,206,207,208,206,209,0
fortuneFirst3D db 207,197,208,194,192,223,32,202,192,208,210,192,0
fortuneSecond3D db 194,210,206,208,192,223,32,202,192,208,210,192,0
fortuneThird3D db 210,208,197,210,220,223,32,202,192,208,210,192,0
fortuneUpright3D db 207,240,255,236,238,229,32,239,238,235,238,230,229,237,232,229,0
fortuneReversed3D db 207,229,240,229,226,184,240,237,243,242,224,255,32,234,224,240,242,224,0
fortuneCardName0 db 216,243,242,0
fortuneCardName1 db 204,224,227,0
fortuneCardName2 db 194,229,240,245,238,226,237,224,255,32,230,240,232,246,224,0
fortuneCardName3 db 200,236,239,229,240,224,242,240,232,246,224,0
fortuneCardName4 db 200,236,239,229,240,224,242,238,240,0
fortuneCardName5 db 200,229,240,238,244,224,237,242,0
fortuneCardName6 db 194,235,254,225,235,184,237,237,251,229,0
fortuneCardName7 db 202,238,235,229,241,237,232,246,224,0
fortuneCardName8 db 209,232,235,224,0
fortuneCardName9 db 206,242,248,229,235,252,237,232,234,0
fortuneCardName10 db 202,238,235,229,241,238,32,244,238,240,242,243,237,251,0
fortuneCardName11 db 209,239,240,224,226,229,228,235,232,226,238,241,242,252,0
fortuneCardName12 db 207,238,226,229,248,229,237,237,251,233,0
fortuneCardName13 db 209,236,229,240,242,252,0
fortuneCardName14 db 211,236,229,240,229,237,237,238,241,242,252,0
fortuneCardName15 db 196,252,255,226,238,235,0
fortuneCardName16 db 193,224,248,237,255,0
fortuneCardName17 db 199,226,229,231,228,224,0
fortuneCardName18 db 203,243,237,224,0
fortuneCardName19 db 209,238,235,237,246,229,0
fortuneCardName20 db 209,243,228,0
fortuneCardName21 db 204,232,240,0
fortuneCardNames3D dd OFFSET fortuneCardName0,OFFSET fortuneCardName1,OFFSET fortuneCardName2,OFFSET fortuneCardName3
dd OFFSET fortuneCardName4,OFFSET fortuneCardName5,OFFSET fortuneCardName6,OFFSET fortuneCardName7
dd OFFSET fortuneCardName8,OFFSET fortuneCardName9,OFFSET fortuneCardName10,OFFSET fortuneCardName11
dd OFFSET fortuneCardName12,OFFSET fortuneCardName13,OFFSET fortuneCardName14,OFFSET fortuneCardName15
dd OFFSET fortuneCardName16,OFFSET fortuneCardName17,OFFSET fortuneCardName18,OFFSET fortuneCardName19
dd OFFSET fortuneCardName20,OFFSET fortuneCardName21
.data?
fortuneBackground3D dd SCREEN_SIZE dup(?)
.data

View file

@ -781,6 +781,7 @@ CheckHallInteractions3D PROC
; A point on the serving edge remains in front even when standing close.
invoke HallTarget3D, playerX,23*FP_1+FP_HALF,INTERACT_BAR,OFFSET hallBarPrompt3D
hall_check_other:
invoke HallTarget3D,FP_1*305/10,28*FP_1,INTERACT_DARTS,OFFSET dtPrompt
invoke HallTarget3D,16*FP_1,FP_1*73/10,INTERACT_CABARET_SEAT,OFFSET cabaretSeat3D
invoke HallTarget3D,scrStation3D.posX,scrStation3D.posY,INTERACT_SCRATCH,OFFSET scratchHall3D
invoke HallTarget3D,FP_1*285/10,FP_1*105/10,INTERACT_BASEMENT,OFFSET hallBunkerPrompt3D
@ -797,6 +798,12 @@ hall_check_bunker:
CheckHallInteractions3D ENDP
TriggerHallInteraction3D PROC
cmp interactTargetId,INTERACT_DARTS
jne hall_not_darts
call BeginDarts3D
mov eax,1
ret
hall_not_darts:
call TryFortuneInteraction3D
test eax,eax
jnz hall_trigger_handled

View file

@ -1,4 +1,8 @@
DrawBJPartySelector3D PROC uses ebx esi edi
cmp obsActive3D,0
je @F
ret
@@:
xor edi,edi
cmp cbState3D,CB_IDLE
je @F

View file

@ -573,33 +573,39 @@ dk_find_done:
DurakFindMove3D ENDP
DurakAITurn3D PROC
cmp dkAttacker3D, 1
je dk_ai_attack
invoke DurakFindMove3D, 1, DK_TRANSFER
cmp eax, 0FFFFFFFFh
je @F
invoke DurakPlay3D, 1, eax, DK_TRANSFER
ret
@@:
invoke DurakFindMove3D, 1, DK_DEFEND
cmp eax, 0FFFFFFFFh
je dk_ai_take
invoke DurakPlay3D, 1, eax, DK_DEFEND
ret
dk_ai_take:
invoke DurakTake3D, 1
ret
dk_ai_attack:
invoke DurakFindMove3D, 1, DK_ATTACK
cmp eax, 0FFFFFFFFh
je dk_ai_finish
invoke DurakPlay3D, 1, eax, DK_ATTACK
ret
dk_ai_finish:
invoke DurakFinish3D, 1
invoke DurakBotTurn3D,1
ret
DurakAITurn3D ENDP
DurakBotTurn3D PROC actor:DWORD
mov eax,actor
cmp dkAttacker3D,eax
je dk_ai_attack
invoke DurakFindMove3D, actor, DK_TRANSFER
cmp eax, 0FFFFFFFFh
je @F
invoke DurakPlay3D, actor, eax, DK_TRANSFER
ret
@@:
invoke DurakFindMove3D, actor, DK_DEFEND
cmp eax, 0FFFFFFFFh
je dk_ai_take
invoke DurakPlay3D, actor, eax, DK_DEFEND
ret
dk_ai_take:
invoke DurakTake3D, actor
ret
dk_ai_attack:
invoke DurakFindMove3D, actor, DK_ATTACK
cmp eax, 0FFFFFFFFh
je dk_ai_finish
invoke DurakPlay3D, actor, eax, DK_ATTACK
ret
dk_ai_finish:
invoke DurakFinish3D, actor
ret
DurakBotTurn3D ENDP
DurakMouse3D PROC uses ebx
cmp mouseClick3D, 0
je dk_mouse_done

View file

@ -382,19 +382,27 @@ poker_showdown_done:
PokerShowdown3D ENDP
; The AI sees only its two cards and the already exposed board. No player cards/future deck.
PokerAIDecide3D PROC uses ebx esi edi
PokerAIDecide3D PROC
invoke PokerBotAct3D,1
ret
PokerAIDecide3D ENDP
PokerBotAct3D PROC uses ebx esi edi actor:DWORD
local strength:DWORD,owed:DWORD
mov eax, pkCurrent3D
sub eax, pkPaid3D[4]
mov ecx,actor
sub eax, pkPaid3D[ecx*4]
mov owed, eax
cmp pkBoardCount3D, 3
jae poker_ai_postflop
mov eax, pkHole3D[8]
mov ebx,actor
shl ebx,3
mov eax, pkHole3D[ebx]
xor edx, edx
mov ecx, 13
div ecx
mov esi, edx
mov eax, pkHole3D[12]
mov eax, pkHole3D[ebx+4]
xor edx, edx
div ecx
mov strength, 0
@ -416,7 +424,8 @@ poker_ai_pair:
jmp poker_ai_odds
poker_ai_postflop:
lea edi, pkEvalHand3D
lea esi, pkHole3D[8]
mov eax,actor
lea esi, pkHole3D[eax*8]
movsd
movsd
lea esi, pkBoard3D
@ -452,7 +461,12 @@ poker_ai_consider_raise:
je poker_ai_call
cmp pkRaisedCount3D, 2
jae poker_ai_call
cmp bankroll, 0
mov eax,bankroll
cmp actor,1
je @F
mov eax,pkStack3D
@@:
cmp eax, 0
je poker_ai_call
cmp strength, 3
jae poker_ai_strong_raise
@ -464,11 +478,11 @@ poker_ai_strong_raise:
jae poker_ai_call
poker_ai_raise:
mov pkAiMessage3D, 3
invoke PokerRaise3D, 1, pkMinRaise3D
invoke PokerRaise3D, actor, pkMinRaise3D
ret
poker_ai_fold:
mov pkAiMessage3D, 4
invoke PokerFold3D, 1
invoke PokerFold3D, actor
ret
poker_ai_call:
mov pkAiMessage3D, 1
@ -476,9 +490,9 @@ poker_ai_call:
je @F
mov pkAiMessage3D, 2
@@:
invoke PokerCall3D, 1
invoke PokerCall3D, actor
ret
PokerAIDecide3D ENDP
PokerBotAct3D ENDP
HandleNativePoker3D PROC uses ebx
call HandleCardExit3D

View file

@ -190,6 +190,8 @@ npc_accept_done:
NpcAcceptContract3D ENDP
NpcRecordRound3D PROC uses ebx esi edi game:DWORD,won:DWORD
cmp obsActive3D,0
jne npc_event_done
call NpcEnsureJournal3D
test eax,eax
jz npc_event_done

494
src_graphics/observer.inc Normal file
View file

@ -0,0 +1,494 @@
; Spectator control supplies decisions to the real native games, never simulated results.
.code
BeginObserver3D PROC uses esi edi
lea esi,screenBuffer
lea edi,obsBackground3D
mov ecx,SCREEN_SIZE
rep movsd
mov engineMode,MODE_OBSERVER
mov obsError3D,0
mov paused3D,0
call ClearKeys3D
mov mouseClick3D,0
ret
BeginObserver3D ENDP
StartObserver3D PROC uses ebx esi
cmp obsActive3D,0
jne obs_start_done
cmp obsSelection3D,OBS_GAME_COUNT
jae obs_start_done
xor esi,esi
obs_save_profile:
mov ebx,obsProfilePointers3D[esi*4]
mov eax,[ebx]
mov obsSavedProfile3D[esi*4],eax
inc esi
cmp esi,LENGTHOF obsProfilePointers3D
jb obs_save_profile
mov obsActive3D,1
mov obsPaused3D,0
mov obsRound3D,0
mov obsWins3D[0],0
mov obsWins3D[4],0
mov obsWins3D[8],0
mov tournamentMode,0
mov cardExitActive3D,0
mov obsError3D,0
mov eax,obsSelection3D
mov eax,obsModes3D[eax*4]
mov engineMode,eax
cmp eax,MODE_POOL
je obs_init_pool
cmp eax,MODE_DURAK
je obs_init_durak
cmp eax,MODE_BLACKJACK
je obs_init_blackjack
call InitNativePoker3D
jmp obs_check_assets
obs_init_durak:
call InitNativeDurak3D
mov eax,obsSelection3D
sub eax,2
mov dkVariant3D,eax
jmp obs_check_assets
obs_init_blackjack:
call InitNativeBlackjack3D
mov cbBotCount3D,0
obs_check_assets:
cmp cardTablePixels3D,0
jne obs_start_round
call StopObserver3D
mov obsError3D,1
ret
obs_init_pool:
mov poolAI3D,1
mov eax,obsSelection3D
sub eax,4
invoke StartBilliards3D,eax
obs_start_round:
call ObserverNewRound3D
obs_start_done:
ret
StartObserver3D ENDP
StopObserver3D PROC uses esi ebx
cmp obsActive3D,0
je obs_stop_menu
; The save barrier remains active until every personal value has been restored.
xor esi,esi
obs_restore_profile:
mov ebx,obsProfilePointers3D[esi*4]
mov eax,obsSavedProfile3D[esi*4]
mov [ebx],eax
inc esi
cmp esi,LENGTHOF obsProfilePointers3D
jb obs_restore_profile
mov obsActive3D,0
obs_stop_menu:
mov engineMode,MODE_OBSERVER
mov paused3D,0
mov cardExitActive3D,0
call ClearKeys3D
mov mouseClick3D,0
invoke PlayCasinoSound3D,0,0,0
ret
StopObserver3D ENDP
ObserverNewRound3D PROC
mov obsTimer3D,0
mov obsResultTimer3D,0
mov obsOutcome3D,0
inc obsRound3D
mov bankroll,10000
cmp engineMode,MODE_POOL
jne @F
call ResetPool3D
ret
@@:
cmp engineMode,MODE_DURAK
jne @F
mov dkBet3D,100
mov dkState3D,DK_IDLE
call StartNativeDurak3D
ret
@@:
cmp engineMode,MODE_BLACKJACK
jne @F
mov cbBet3D,100
mov cbState3D,CB_IDLE
call StartNativeBlackjack3D
ret
@@:
mov pkBlind3D,50
mov pkState3D,PK_IDLE
call StartNativePoker3D
ret
ObserverNewRound3D ENDP
ObserverOutcome3D PROC
cmp engineMode,MODE_POOL
jne @F
cmp poolState3D,POOL_RESULT
jne obs_no_outcome
mov eax,poolWinner3D
inc eax
ret
@@:
cmp engineMode,MODE_DURAK
jne @F
cmp dkState3D,DK_RESULT
jne obs_no_outcome
mov eax,dkOutcome3D
inc eax
ret
@@:
cmp engineMode,MODE_BLACKJACK
jne @F
cmp cbState3D,CB_RESULT
jne obs_no_outcome
mov eax,cbOutcome3D
cmp eax,3
jne obs_number_outcome
mov eax,1
ret
@@:
cmp pkState3D,PK_RESULT
jne obs_no_outcome
mov eax,pkOutcome3D
obs_number_outcome:
inc eax
ret
obs_no_outcome:
xor eax,eax
ret
ObserverOutcome3D ENDP
ObserverBlackjack3D PROC
cmp cbState3D,CB_PLAYER
jne obs_bj_update
cmp obsTimer3D,700
jb obs_bj_update
mov obsTimer3D,0
; Only our hand and the dealer's exposed up-card enter the decision.
cmp cbPlayerScore3D,17
jae obs_bj_stand
cmp cbPlayerScore3D,12
jb obs_bj_hit
mov eax,cbDealer3D[0]
xor edx,edx
mov ecx,13
div ecx
cmp edx,5
jae obs_bj_hit
obs_bj_stand:
invoke FinishBJPlayer3D,-1
jmp obs_bj_update
obs_bj_hit:
call NativeBJHit3D
obs_bj_update:
call HandleNativeBlackjack3D
ret
ObserverBlackjack3D ENDP
ObserverDurak3D PROC
cmp dkState3D,DK_TURN
jne obs_durak_update
cmp dkActor3D,0
jne obs_durak_update
cmp obsTimer3D,700
jb obs_durak_update
mov obsTimer3D,0
invoke DurakBotTurn3D,0
obs_durak_update:
call HandleNativeDurak3D
ret
ObserverDurak3D ENDP
ObserverTick3D PROC uses ebx
cmp obsActive3D,0
je obs_tick_done
call ObserverOutcome3D
test eax,eax
jz obs_tick_game
cmp obsOutcome3D,0
jne obs_result_wait
mov obsOutcome3D,eax
inc obsWins3D[eax*4-4]
mov obsResultTimer3D,0
obs_result_wait:
mov eax,deltaTime3D
add obsResultTimer3D,eax
cmp obsResultTimer3D,3000
jb obs_tick_done
call ObserverNewRound3D
ret
obs_tick_game:
mov eax,deltaTime3D
add obsTimer3D,eax
cmp engineMode,MODE_POOL
jne @F
call HandlePool3D
ret
@@:
cmp engineMode,MODE_DURAK
jne @F
call ObserverDurak3D
ret
@@:
cmp engineMode,MODE_BLACKJACK
jne @F
call ObserverBlackjack3D
ret
@@:
cmp pkState3D,PK_PLAYER
jne obs_poker_update
cmp obsTimer3D,700
jb obs_poker_update
mov obsTimer3D,0
invoke PokerBotAct3D,0
obs_poker_update:
call HandleNativePoker3D
obs_tick_done:
ret
ObserverTick3D ENDP
ObserverFrame3D PROC uses ebx esi
invoke Pressed3D,1Bh
test eax,eax
jz obs_frame_input
cmp obsActive3D,0
je obs_leave_hall
call StopObserver3D
jmp obs_frame_draw
obs_leave_hall:
mov engineMode,MODE_WALK
call ClearKeys3D
mov mouseClick3D,0
jmp obs_frame_draw
obs_frame_input:
cmp obsActive3D,0
jne obs_watch_input
xor esi,esi
obs_menu_digit:
lea eax,[esi+31h]
invoke Pressed3D,eax
test eax,eax
jnz obs_menu_pick
inc esi
cmp esi,OBS_GAME_COUNT
jb obs_menu_digit
invoke Pressed3D,26h
test eax,eax
jz @F
cmp obsSelection3D,0
jne obs_menu_up
mov obsSelection3D,OBS_GAME_COUNT
obs_menu_up:
dec obsSelection3D
@@:
invoke Pressed3D,28h
test eax,eax
jz @F
inc obsSelection3D
cmp obsSelection3D,OBS_GAME_COUNT
jb @F
mov obsSelection3D,0
@@:
invoke Pressed3D,0Dh
test eax,eax
jnz obs_menu_start
cmp mouseClick3D,0
je obs_frame_draw
cmp mouseX3D,16
jb obs_frame_draw
cmp mouseX3D,304
jae obs_frame_draw
mov eax,mouseY3D
sub eax,52
js obs_frame_draw
xor edx,edx
mov ecx,23
div ecx
cmp eax,OBS_GAME_COUNT
jae obs_menu_close_click
cmp edx,20
jae obs_frame_draw
mov esi,eax
obs_menu_pick:
mov obsSelection3D,esi
obs_menu_start:
call StartObserver3D
jmp obs_frame_draw
obs_menu_close_click:
cmp mouseY3D,212
jb obs_frame_draw
jmp obs_leave_hall
obs_watch_input:
invoke Pressed3D,20h
test eax,eax
jnz obs_watch_pause
invoke Pressed3D,9
test eax,eax
jnz obs_watch_speed
invoke Pressed3D,4Eh
test eax,eax
jnz obs_watch_restart
cmp mouseClick3D,0
je obs_watch_tick
cmp mouseY3D,222
jb obs_watch_tick
cmp mouseY3D,239
jae obs_watch_tick
mov eax,mouseX3D
cmp eax,74
jb obs_watch_pause
cmp eax,134
jb obs_watch_speed
cmp eax,226
jb obs_watch_restart
call StopObserver3D
jmp obs_frame_draw
obs_watch_pause:
xor obsPaused3D,1
jmp obs_watch_tick
obs_watch_speed:
shl obsSpeed3D,1
cmp obsSpeed3D,4
jbe obs_watch_tick
mov obsSpeed3D,1
jmp obs_watch_tick
obs_watch_restart:
call ObserverNewRound3D
obs_watch_tick:
; Human game commands never reach the autonomous seats.
call ClearKeys3D
mov mouseClick3D,0
cmp obsPaused3D,0
jne obs_frame_draw
mov ebx,deltaTime3D
mov eax,ebx
imul eax,obsSpeed3D
mov deltaTime3D,eax
add animationClock3D,eax
call ObserverTick3D
mov deltaTime3D,ebx
obs_frame_draw:
call RenderObserver3D
mov mouseClick3D,0
ret
ObserverFrame3D ENDP
RenderObserver3D PROC uses ebx esi edi
cmp obsActive3D,0
jne obs_render_game
lea esi,obsBackground3D
lea edi,screenBuffer
mov ecx,SCREEN_SIZE
rep movsd
invoke DrawBox,0,0,320,240,00151D20h
invoke DrawString8x8,16,18,OFFSET obsTitle,00E8D7ADh
xor ebx,ebx
obs_render_row:
imul esi,ebx,23
add esi,52
mov edi,00728689h
cmp ebx,obsSelection3D
jne @F
mov edi,00E8D7ADh
@@:
invoke DrawBoxBorder,16,esi,288,20,00293436h,edi,1
add esi,6
lea eax,[ebx+1]
invoke FormatNumber,eax,OFFSET hudNumBuffer
invoke DrawString8x8,24,esi,OFFSET hudNumBuffer,edi
invoke DrawString8x8,48,esi,obsNames3D[ebx*4],edi
inc ebx
cmp ebx,OBS_GAME_COUNT
jb obs_render_row
invoke DrawBoxBorder,16,212,288,20,00293436h,00728689h,1
invoke DrawString8x8,136,218,OFFSET obsClose,00E8D7ADh
cmp obsError3D,0
je obs_render_done
invoke DrawString8x8,16,198,OFFSET obsFailed,00DB8B83h
ret
obs_render_game:
cmp engineMode,MODE_POOL
jne @F
call RenderPool3D
jmp obs_render_hud
@@:
cmp engineMode,MODE_DURAK
jne @F
call RenderNativeDurak3D
jmp obs_render_hud
@@:
cmp engineMode,MODE_BLACKJACK
jne @F
call RenderNativeBlackjack3D
jmp obs_render_hud
@@:
call RenderNativePoker3D
obs_render_hud:
invoke DrawBox,0,0,320,32,00151D20h
mov ebx,obsSelection3D
invoke DrawString8x8,8,5,obsNames3D[ebx*4],00E8D7ADh
invoke DrawString8x8,8,20,OFFSET obsFirst,0090CDB2h
invoke FormatNumber,obsWins3D[0],OFFSET hudNumBuffer
invoke DrawString8x8,48,20,OFFSET hudNumBuffer,0090CDB2h
invoke DrawString8x8,88,20,OFFSET obsSecond,00DEA0A0h
invoke FormatNumber,obsWins3D[4],OFFSET hudNumBuffer
invoke DrawString8x8,128,20,OFFSET hudNumBuffer,00DEA0A0h
invoke DrawString8x8,190,20,OFFSET obsRound,00E8D7ADh
invoke FormatNumber,obsRound3D,OFFSET hudNumBuffer
invoke DrawString8x8,254,20,OFFSET hudNumBuffer,00FFFFFFh
invoke DrawBox,0,215,320,25,00151D20h
cmp obsOutcome3D,0
je obs_render_turn
mov ebx,OFFSET obsWin1
cmp obsOutcome3D,1
je @F
mov ebx,OFFSET obsWin2
cmp obsOutcome3D,2
je @F
mov ebx,OFFSET obsDraw
@@:
invoke DrawString8x8,200,5,ebx,00E8D7ADh
jmp obs_render_controls
obs_render_turn:
mov eax,pkTurn3D
cmp engineMode,MODE_POKER
je obs_render_actor
mov eax,dkActor3D
cmp engineMode,MODE_DURAK
je obs_render_actor
mov eax,poolPlayer3D
cmp engineMode,MODE_POOL
je obs_render_actor
xor eax,eax
cmp cbState3D,CB_DEALER
jne obs_render_actor
inc eax
obs_render_actor:
and eax,1
invoke DrawString8x8,232,5,obsTurnNames3D[eax*4],00A7C7C5h
obs_render_controls:
mov ebx,OFFSET obsPause
cmp obsPaused3D,0
je @F
mov ebx,OFFSET obsPlay
@@:
invoke DrawBoxBorder,4,222,66,16,00293436h,00728689h,1
invoke DrawString8x8,14,226,ebx,00FFFFFFh
mov eax,obsSpeed3D
shr eax,1
mov ebx,obsSpeedNames3D[eax*4]
invoke DrawBoxBorder,78,222,48,16,00293436h,00728689h,1
invoke DrawString8x8,94,226,ebx,00FFFFFFh
invoke DrawBoxBorder,138,222,80,16,00293436h,00728689h,1
invoke DrawString8x8,154,226,OFFSET obsRestart,00FFFFFFh
invoke DrawBoxBorder,230,222,86,16,00293436h,00728689h,1
invoke DrawString8x8,254,226,OFFSET obsGames,00FFFFFFh
obs_render_done:
ret
RenderObserver3D ENDP

View file

@ -0,0 +1,64 @@
; Native observer uses the existing games; no alternate rules engine.
MODE_OBSERVER equ 26
OBS_GAME_COUNT equ 6
BeginObserver3D PROTO
ObserverFrame3D PROTO
StartObserver3D PROTO
StopObserver3D PROTO
ObserverNewRound3D PROTO
ObserverTick3D PROTO
ObserverOutcome3D PROTO
RenderObserver3D PROTO
ObserverBlackjack3D PROTO
ObserverDurak3D PROTO
PokerBotAct3D PROTO :DWORD
DurakBotTurn3D PROTO :DWORD
.data
obsActive3D dd 0
obsSelection3D dd 0
obsPaused3D dd 0
obsSpeed3D dd 1
obsTimer3D dd 0
obsResultTimer3D dd 0
obsOutcome3D dd 0
obsRound3D dd 0
obsWins3D dd 0,0,0
obsError3D dd 0
obsModes3D dd MODE_POKER,MODE_BLACKJACK,MODE_DURAK,MODE_DURAK,MODE_POOL,MODE_POOL
obsNames3D dd OFFSET obsPoker,OFFSET obsBlackjack,OFFSET obsDurak,OFFSET obsTransfer,OFFSET obsPyramid,OFFSET obsEight
obsProfilePointers3D dd OFFSET bankroll,OFFSET rngSeed3D,OFFSET tournamentMode
dd OFFSET pkBlind3D,OFFSET cbBet3D,OFFSET dkBet3D,OFFSET dkVariant3D
dd OFFSET poolVariant3D,OFFSET poolAI3D,OFFSET cbBotCount3D
obsSavedProfile3D dd 10 dup(0)
obsSpeedNames3D dd OFFSET obsSpeed1,OFFSET obsSpeed2,OFFSET obsSpeed4
obsSpeed1 db "1X",0
obsSpeed2 db "2X",0
obsSpeed4 db "4X",0
obsSeparator db ":",0
obsResultNames3D dd OFFSET obsWin1,OFFSET obsWin2,OFFSET obsDraw
obsTurnNames3D dd OFFSET obsTurn1,OFFSET obsTurn2
obsTurn1 db 213,206,196,32,200,200,32,49,0
obsTurn2 db 213,206,196,32,200,200,32,50,0
obsTitle db 205,192,193,203,222,196,197,205,200,197,0
obsPoker db 207,206,202,197,208,0
obsBlackjack db 50,49,0
obsDurak db 196,211,208,192,202,58,32,207,206,196,202,200,196,205,206,201,0
obsTransfer db 196,211,208,192,202,58,32,207,197,208,197,194,206,196,205,206,201,0
obsPyramid db 193,200,203,220,223,208,196,58,32,207,200,208,192,204,200,196,192,0
obsEight db 193,200,203,220,223,208,196,58,32,194,206,209,220,204,197,208,202,192,0
obsFirst db 200,200,32,49,0
obsSecond db 200,200,32,50,0
obsDraw db 205,200,215,220,223,0
obsWin1 db 207,206,193,197,196,192,32,200,200,32,49,0
obsWin2 db 207,206,193,197,196,192,32,200,200,32,50,0
obsPause db 207,192,211,199,192,0
obsPlay db 62,0
obsRestart db 199,192,205,206,194,206,0
obsGames db 200,195,208,219,0
obsRound db 207,192,208,210,200,223,0
obsClose db 194,32,213,206,203,203,0
obsFailed db 205,197,32,211,196,192,203,206,209,220,32,206,210,202,208,219,210,220,32,209,210,206,203,0
.data?
obsBackground3D dd SCREEN_SIZE dup(?)
.code

View file

@ -13,6 +13,13 @@ WindowEvents3D PROC uses ebx esi edi hwnd:DWORD, uMsg:DWORD, wp:DWORD, lp:DWORD
invoke FortuneChar3D, wp
xor eax, eax
ret
@@:
cmp uMsg,202h
jne @F
cmp engineMode,MODE_DARTS
jne event_default
mov dtMouseReleased3D,1
jmp event_mouse_down
@@:
cmp uMsg, 201h
je event_mouse_down
@ -24,6 +31,8 @@ WindowEvents3D PROC uses ebx esi edi hwnd:DWORD, uMsg:DWORD, wp:DWORD, lp:DWORD
@@:
cmp uMsg, 200h
jne @F
cmp engineMode,MODE_DARTS
je event_mouse_down
cmp engineMode, MODE_POOL
jne event_default
test wp, 1
@ -61,6 +70,15 @@ event_mouse_down:
sub eax, viewLeft3D
cmp eax, viewWidth3D
jae event_done
cmp engineMode,MODE_DARTS
jne event_coarse_x
push eax
imul eax,640*256
xor edx,edx
div viewWidth3D
mov dtCursorX3D,eax
pop eax
event_coarse_x:
imul eax, 320
xor edx, edx
div viewWidth3D
@ -69,6 +87,15 @@ event_mouse_down:
sub eax, viewTop3D
cmp eax, viewHeight3D
jae event_done
cmp engineMode,MODE_DARTS
jne event_coarse_y
push eax
imul eax,480*256
xor edx,edx
div viewHeight3D
mov dtCursorY3D,eax
pop eax
event_coarse_y:
imul eax, 240
xor edx, edx
div viewHeight3D
@ -87,6 +114,13 @@ event_mouse_down:
pyr_event_pick:
mov pyrPickClick3D,1
pyr_event_pick_done:
cmp uMsg,202h
je event_done
cmp uMsg,200h
jne @F
cmp engineMode,MODE_DARTS
je event_done
@@:
mov mouseClick3D, 1
jmp event_done
event_key_down:
@ -110,6 +144,8 @@ event_fullscreen:
call ToggleFullscreen3D
jmp event_done
event_focus_lost:
mov dtHolding3D,0
mov dtMouseReleased3D,0
call ClearKeys3D
jmp event_done
event_erase:
@ -572,6 +608,8 @@ progress_loaded:
LoadProgress3D ENDP
SaveProgress3D PROC uses ebx
cmp obsActive3D,0
jne progress_saved
IFDEF CASINO_ACTION_TEST
ret
ENDIF

View file

@ -537,7 +537,11 @@ pyr_actor:
ret
@@:
cmp poolPlayer3D,1
jne pyr_human
je pyr_actor_ai_check
cmp obsActive3D,0
jne pyr_actor_ai_check
jmp pyr_human
pyr_actor_ai_check:
cmp poolAI3D,0
je pyr_human
cmp poolTime3D,1000

View file

@ -115,6 +115,7 @@ RenderHallDressing3D PROC uses ebx esi edi
cmp hallLevel3D,0
jne hall_dressing_done
call RenderTerminalCabinets3D
call DrawHallDarts3D
lea esi,hallWallArt3D
mov edi,WORLD_WALL_ART_COUNT
hall_wall_painting:

203
tools/build_setup.py Normal file
View file

@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Build and package the complete modern Assembler Casino into PokerGameSetup.exe."""
import hashlib
import os
import re
import subprocess
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MARKER = b"<<POKER_GAME_GUI_PAYLOAD_ZIP>>"
def get_sha256(data_or_path):
if isinstance(data_or_path, (str, Path)):
h = hashlib.sha256()
with open(data_or_path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest().upper()
return hashlib.sha256(data_or_path).hexdigest().upper()
def collect_payload_files():
entries = []
# Executables
for src, dst in [
(ROOT / "dist/Assembler Casino.exe", "Assembler Casino.exe"),
(ROOT / "dist/Uninstall Assembler Casino.exe", "Uninstall Assembler Casino.exe"),
(ROOT / "bin/poker.exe", "bin/poker.exe"),
(ROOT / "poker.exe", "poker.exe"),
]:
if not src.exists():
raise FileNotFoundError(f"Required binary not found: {src}")
entries.append((src, dst))
# Assets (excluding raw sprites used only for asset generation)
assets_dir = ROOT / "assets"
for r, dirs, files in os.walk(assets_dir):
if "sprites" in Path(r).parts:
continue
for f in sorted(files):
fp = Path(r) / f
rel = fp.relative_to(ROOT).as_posix()
entries.append((fp, rel))
# Docs
docs_dir = ROOT / "docs"
for r, dirs, files in os.walk(docs_dir):
for f in sorted(files):
if f.endswith((".txt", ".md")):
fp = Path(r) / f
rel = fp.relative_to(ROOT).as_posix()
entries.append((fp, rel))
# Tools
tool_files = [
"tools/previews/preview_dbd_hook.bat",
"tools/previews/preview_hakari.bat",
"tools/previews/preview_intro.bat",
"tools/previews/preview_casino3d.bat",
"tools/tests/achievement_popup.bat",
"tools/tests/release_smoke_check.ps1",
]
for rel in tool_files:
fp = ROOT / rel
if fp.exists():
entries.append((fp, rel))
# Backwards compatibility paths for tests / older scripts
if (ROOT / "tools/previews/preview_dbd_hook.bat").exists():
entries.append((ROOT / "tools/previews/preview_dbd_hook.bat", "tools/preview_dbd_hook.bat"))
if (ROOT / "tools/tests/release_smoke_check.ps1").exists():
entries.append((ROOT / "tools/tests/release_smoke_check.ps1", "tools/release_smoke_check.ps1"))
# Runtime configuration and state
for rel in ["runtime/casino3d-progress.dat", "runtime/display.cfg"]:
fp = ROOT / rel
if fp.exists():
entries.append((fp, rel))
# AI worker & metadata (without the 2.5 GB model per docs/FORTUNE_AI.md)
ai_dir = ROOT / "ai"
if ai_dir.exists():
for r, dirs, files in os.walk(ai_dir):
if "models" in Path(r).parts:
continue
for f in sorted(files):
fp = Path(r) / f
rel = fp.relative_to(ROOT).as_posix()
entries.append((fp, rel))
# Root documentation
for rel in ["AGENTS.md", "README.md"]:
fp = ROOT / rel
if fp.exists():
entries.append((fp, rel))
return entries
def build_payload_zip(zip_path, entries):
print(f"Creating payload archive: {zip_path} ({len(entries)} files)...")
zip_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
for src, dst in entries:
zf.write(src, dst)
size_mb = zip_path.stat().st_size / (1024 * 1024)
print(f"Payload archive created: {size_mb:.2f} MB")
def compile_setup_base(source_path, base_exe_path):
csc = Path(os.environ.get("WINDIR", r"C:\Windows")) / r"Microsoft.NET\Framework\v4.0.30319\csc.exe"
if not csc.exists():
raise RuntimeError(f"csc.exe not found at {csc}")
icon = ROOT / "assets/icons/casino.ico"
cmd = [
str(csc),
"/nologo",
"/target:winexe",
"/platform:x86",
"/optimize+",
"/reference:System.Windows.Forms.dll",
"/reference:System.Drawing.dll",
"/reference:System.IO.Compression.dll",
"/reference:System.IO.Compression.FileSystem.dll",
"/reference:System.Core.dll",
f"/win32icon:{icon}",
f"/out:{base_exe_path}",
str(source_path),
]
print(f"Compiling installer base: {' '.join(cmd)}")
res = subprocess.run(cmd, capture_output=True, text=True)
if res.returncode != 0:
raise RuntimeError(f"Compilation failed:\n{res.stdout}\n{res.stderr}")
print(f"Compiled: {base_exe_path} ({base_exe_path.stat().st_size} bytes)")
def main():
print("=== Building Assembler Casino PokerGameSetup ===")
# 1. Collect files
entries = collect_payload_files()
payload_zip = ROOT / "build/payload_release_final.zip"
build_payload_zip(payload_zip, entries)
# 2. Hash payload
payload_hash = get_sha256(payload_zip)
print(f"Payload SHA-256: {payload_hash}")
# 3. Update PokerGameSetupGui.cs with new payload hash
setup_cs = ROOT / "build/PokerGameSetupGui.cs"
cs_content = setup_cs.read_text(encoding="utf-8")
hash_pattern = r'const string ExpectedPayloadSha256 = "([A-Fa-f0-9]{64}|TO_BE_FILLED_BY_RELEASE_BUILD)";'
if not re.search(hash_pattern, cs_content):
raise ValueError("Could not find ExpectedPayloadSha256 constant in PokerGameSetupGui.cs")
new_cs_content = re.sub(
hash_pattern,
f'const string ExpectedPayloadSha256 = "{payload_hash}";',
cs_content,
count=1
)
setup_cs.write_text(new_cs_content, encoding="utf-8")
print("Updated ExpectedPayloadSha256 in PokerGameSetupGui.cs")
# 4. Compile base setup executable
base_exe = ROOT / "build/PokerGameSetup_base.exe"
compile_setup_base(setup_cs, base_exe)
# 5. Assemble final PokerGameSetup.exe
setup_exe = ROOT / "PokerGameSetup.exe"
print(f"Assembling {setup_exe}...")
with open(setup_exe, "wb") as out_f:
with open(base_exe, "rb") as in_base:
out_f.write(in_base.read())
out_f.write(MARKER)
with open(payload_zip, "rb") as in_zip:
while chunk := in_zip.read(65536):
out_f.write(chunk)
total_mb = setup_exe.stat().st_size / (1024 * 1024)
print(f"=== Successfully built {setup_exe} ({total_mb:.2f} MB) ===")
# 6. Verify built setup
with open(setup_exe, "rb") as f:
head = f.read(4 * 1024 * 1024)
idx = head.find(MARKER)
assert idx != -1, "Marker not found in generated setup!"
f.seek(idx + len(MARKER))
h = hashlib.sha256()
while chunk := f.read(65536):
h.update(chunk)
actual_hash = h.hexdigest().upper()
assert actual_hash == payload_hash, f"Hash mismatch: {actual_hash} != {payload_hash}"
print(f"[VERIFIED] Marker and SHA-256 match perfectly ({actual_hash})")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,21 @@
diff --git a/casino3d.inc b/casino3d.inc
--- a/casino3d.inc
+++ b/casino3d.inc
@@ -174,1 +174,2 @@
include src_graphics\observer_data.inc
+include src_graphics\darts_data.inc
@@ -5198,1 +5199,2 @@
include src_graphics\observer.inc
+include src_graphics\darts.inc
@@ -5357,1 +5359,9 @@
check_slots_mode:
+ cmp engineMode,MODE_DARTS
+ jne check_darts_done
+ call HandleDarts3D
+ call RenderDarts3D
+ jmp present_frame
+check_darts_done:
cmp engineMode,MODE_FORTUNE
@@ -5642,1 +5652,2 @@
call FreeRussianAssets3D
+ call FreeDarts3D

View file

@ -0,0 +1,15 @@
diff --git a/src_graphics/hall_scene.inc b/src_graphics/hall_scene.inc
--- a/src_graphics/hall_scene.inc
+++ b/src_graphics/hall_scene.inc
@@ -783,1 +783,2 @@
hall_check_other:
+ invoke HallTarget3D,FP_1*305/10,28*FP_1,INTERACT_DARTS,OFFSET dtPrompt
@@ -799,1 +800,9 @@
TriggerHallInteraction3D PROC
+ cmp interactTargetId,INTERACT_DARTS
+ jne hall_not_darts
+ call BeginDarts3D
+ mov eax,1
+ ret
+hall_not_darts:
call TryFortuneInteraction3D

View file

@ -0,0 +1,29 @@
diff --git a/casino3d.inc b/casino3d.inc
--- a/casino3d.inc
+++ b/casino3d.inc
@@ -173,1 +173,2 @@
include src_graphics\charms_data.inc
+include src_graphics\observer_data.inc
@@ -4856,5 +4857,5 @@
check_target_observer:
cmp eax, INTERACT_OBSERVER
jne target_unported
- call BeginUnavailable3D
+ call BeginObserver3D
ret
@@ -5196,1 +5197,2 @@
include src_graphics\charms.inc
+include src_graphics\observer.inc
@@ -5245,2 +5247,11 @@
call ToggleFullscreen3D
@@:
+ cmp obsActive3D,0
+ jne engine_observer_frame
+ cmp engineMode,MODE_OBSERVER
+ jne engine_observer_skip
+engine_observer_frame:
+ call UpdateMouseLockState
+ call ObserverFrame3D
+ jmp present_frame
+engine_observer_skip:
invoke Pressed3D, 1Bh

View file

@ -0,0 +1,103 @@
"""Exact clock-board geometry plus fixed-grid full-arm animation packing."""
from pathlib import Path
import math
import random
import struct
from PIL import Image, ImageDraw, ImageFont
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT / "assets/sprites/darts"
OUT = ROOT / "assets/animations/darts"
SECTORS = [20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5]
def pack(name, frames):
width, height = frames[0].size
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
(OUT / name).write_bytes(struct.pack("<4s4I", b"ANM1", width, height, len(frames), len(payload)) + payload)
def board():
rng = random.Random(301)
image = Image.new("RGBA", (512, 512))
pixels = image.load()
for y in range(512):
for x in range(512):
dx, dy = x-256, 256-y
radius = math.hypot(dx, dy)
angle = math.atan2(dx, dy)
sector = int(((angle + math.pi/20) % math.tau) / (math.tau/20))
red = sector % 2 == 0
color = (30, 36, 39)
if radius < 170:
color = (35, 38, 37) if red else (214, 206, 178)
if 99 <= radius <= 107 or 162 <= radius:
color = (174, 37, 47) if red else (28, 112, 87)
if radius <= 15.9:
color = (28, 112, 87)
if radius <= 6.35:
color = (187, 37, 47)
elif radius < 210:
color = (20, 24, 25)
elif radius < 240:
color = (72, 29, 35)
elif radius < 248:
color = (31, 37, 41)
else:
continue
grain = rng.randrange(-11, 12) if radius < 170 else rng.randrange(-3, 4)
pixels[x, y] = tuple(max(0, min(255, c+grain)) for c in color) + (255,)
draw = ImageDraw.Draw(image)
wire = (171, 178, 173, 255)
for r in (6.35, 15.9, 99, 107, 162, 170):
draw.ellipse((256-r, 256-r, 256+r, 256+r), outline=wire, width=1)
for index in range(20):
theta = (index*18-9)*math.pi/180
draw.line([(256+math.sin(theta)*16, 256-math.cos(theta)*16),
(256+math.sin(theta)*170, 256-math.cos(theta)*170)], fill=wire)
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", 23)
for index, value in enumerate(SECTORS):
theta = index*math.tau/20
draw.text((256+math.sin(theta)*190, 256-math.cos(theta)*190), str(value),
font=font, fill=(227, 230, 220), anchor="mm", stroke_width=1)
draw.ellipse((12, 12, 500, 500), outline=(104, 114, 120), width=3)
image.save(ART / "board-v1.png")
return image
def main():
ART.mkdir(parents=True, exist_ok=True)
OUT.mkdir(parents=True, exist_ok=True)
pack("board-v1.bin", [board()])
wall = Image.open(ART / "lounge-wall-v1.png").convert("RGBA")
pack("lounge-wall-v1.bin", [wall.resize((512,512), Image.Resampling.LANCZOS)])
anchors = []
for name in ("arm-throw-v3", "dart-views-v1"):
source = ART / (name + ".png")
sheet = Image.open(source).convert("RGBA")
assert sheet.width == sheet.height
cell = sheet.width / 4
frames = [sheet.crop(tuple(round(v*cell) for v in (x, y, x+1, y+1)))
.resize((256, 256), Image.Resampling.LANCZOS) for y in range(4) for x in range(4)]
assert all(f.getchannel("A").getextrema()[0] == 0 for f in frames)
assert len({f.tobytes() for f in frames}) == 16
pack(name + ".bin", frames)
if name == "dart-views-v1":
for frame in frames:
metal = [(x,y) for y in range(256) for x in range(256)
if (lambda p: p[3] > 220 and min(p[:3]) > 50
and max(p[:3])-min(p[:3]) < 60)(frame.getpixel((x,y)))]
tip_x = min(x for x,y in metal)
ys = sorted(y for x,y in metal if x <= tip_x+2)
anchors.append((tip_x, ys[len(ys)//2]))
(ROOT / "src_graphics/darts_art.inc").write_text(
"; Generated per-view needle anchors in the 256px source canvas.\n"
+ "dtDartTipX3D dd " + ",".join(str(p[0]) for p in anchors) + "\n"
+ "dtDartTipY3D dd " + ",".join(str(p[1]) for p in anchors) + "\n", encoding="ascii")
print("Packed exact 20-sector board; whole-arm animation uses one fixed canvas and scale.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,63 @@
"""Offline sprite-sheet compiler; runtime animation stays in MASM."""
from pathlib import Path
import json
import struct
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT / "assets/sprites/fortune"
OUT = ROOT / "assets/animations/fortune"
def grid(name, columns, rows, size):
source = Image.open(ART / name).convert("RGBA")
return [source.crop((round(x*source.width/columns), round(y*source.height/rows),
round((x+1)*source.width/columns), round((y+1)*source.height/rows)))
.resize(size, Image.Resampling.LANCZOS)
for y in range(rows) for x in range(columns)]
def tween(frames, loop=False):
# One shared canvas/scale, never fit moving hands independently per frame.
output = []
for i, frame in enumerate(frames):
following = frames[(i+1) % len(frames)] if loop or i+1 < len(frames) else frame
output.extend(Image.blend(frame, following, step/4) for step in range(4))
return output
def pack(name, frames):
width, height = frames[0].size
assert len(frames) <= 64
assert all(frame.size == (width, height) for frame in frames)
payload = b"".join(frame.tobytes("raw", "BGRA") for frame in frames)
(OUT / (name + ".bin")).write_bytes(
struct.pack("<4s4I", b"ANM1", width, height, len(frames), len(payload)) + payload)
return {"file": name + ".bin", "width": width, "height": height, "frames": len(frames)}
def main():
OUT.mkdir(parents=True, exist_ok=True)
body = grid("teller-actions-v1.png", 8, 5, (256, 256))
cards = grid("tarot-symbols-v1.png", 6, 4, (96, 128))
# Orientations are real upside-down artwork, not merely a different label.
faces = cards[:22] + [c.transpose(Image.Transpose.ROTATE_180) for c in cards[:22]] + [cards[22]]
room = Image.open(ART / "seated-room-v1.png").convert("RGBA").resize((640, 480), Image.Resampling.LANCZOS)
manifest = [pack("room-v1", [room]),
pack("shuffle-v1", tween(body[:16], loop=True)),
pack("deal-v1", tween(body[16:24])),
pack("gesture-v1", tween(body[24:32], loop=True)),
pack("orb-hands-v1", tween(body[32:40], loop=True)),
pack("orb-mist-v1", grid("crystal-mist-v1.png", 4, 4, (192, 192))),
pack("cards-v1", faces)]
(ART / "packed-v1.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="ascii")
preview = room.copy()
preview.alpha_composite(body[0].resize((280, 250)), (134, 32))
for index in range(3):
preview.alpha_composite(cards[22].resize((72, 96)), (198+index*100, 302))
preview.save(ROOT / "build/hall-expansion/fortune-session-art.png")
print("Packed 160 hand poses, 16 mist frames, 44 oriented faces, back and seated room.")
if __name__ == "__main__":
main()

View file

@ -33,6 +33,8 @@ InitPaths3D PROTO
ExitProcess PROTO :DWORD
WriteActionSnapshot3D PROTO :DWORD
include src_graphics/billiards_data.inc
.data
obsActive3D dd 0
.code
include src_graphics/billiards.inc
include tools/tests/pyramid_test.inc

175
tools/tests/check_darts.py Normal file
View file

@ -0,0 +1,175 @@
"""Fresh MASM dart scoring, mouse aiming, full-arm animation and 3D follow-camera proofs."""
from pathlib import Path
import argparse
import hashlib
import re
import struct
import subprocess
from PIL import Image
from check_card_exit_build import Coff
from check_terminals import signature
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/darts"
def read(path):
raw = (ROOT / path).read_bytes()
try:
return raw.decode("utf8").replace("\r\n", "\n")
except UnicodeDecodeError:
return raw.decode("cp1251").replace("\r\n", "\n")
def proc(text, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", text, re.M | re.S).group()
def expand(path):
return re.sub(r"^include ([^\n]+)$", lambda m: expand(m[1].strip().replace("\\", "/")),
read(path), flags=re.M)
def prepare():
OUT.mkdir(parents=True, exist_ok=True)
code = expand("src_graphics/darts.inc")
code += "\n" + proc(read("src_graphics/platform_code.inc"), "WindowEvents3D")
for name in ("ResolveCitySurface3D", "DrawCityVertical3D"):
code += "\n" + proc(read("src_graphics/city_bins.inc"), name)
code += "\n" + proc(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D")
code += "\nPlayCasinoSound3D PROC arg0:DWORD,arg1:DWORD,arg2:DWORD\n ret\nPlayCasinoSound3D ENDP\n"
code += "\n" + read("tools/tests/darts_test.inc")
code += "\n" + read("tools/tests/darts_ui.inc")
data = expand("src_graphics/darts_data.inc")
stages = [(0, t) for t in range(0, 320, 40)] + [(1, t) for t in range(0, 200, 40)]
stages += [(2, t) for t in range(0, 1200, 40)]
stages += [(3, t) for t in range(0, 1300, 80)] + [(4, t) for t in range(0, 751, 50)]
code += "\nDartsTestFrames PROC\n call ResetDarts3D\n mov dtAimX3D,0\n mov dtAimY3D,103\n mov dtHitX3D,0\n mov dtHitY3D,103\n"
for index, (state, time) in enumerate(stages):
data += f'\n.data\ndtFrame{index} db "build\\darts\\frame-{index:03}.bmp",0\n'
if state == 3 and stages[index-1][0] != 3:
code += " mov dtState3D,DT_FLIGHT\n call SettleDarts3D\n"
code += f" mov dtState3D,{state}\n mov dtTimer3D,{time}\n mov dtClock3D,{time}\n"
if state == 0:
code += f" mov dtHolding3D,1\n mov dtHoldTime3D,{time}\n"
code += f" call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtFrame{index}\n"
data += '\n.data\ndtScoreFrame db "build\\darts\\scoreboard.bmp",0\n'
code += " mov dtScoreOpen3D,1\n call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtScoreFrame\n"
code += " call ResetDarts3D\n mov dtHolding3D,1\n mov dtCursorX3D,320*256\n mov dtCursorY3D,214*256-103*DT_FOCAL*256/DT_WALL\n"
for index in range(80):
data += f'\n.data\ndtSwayFrame{index} db "build\\darts\\sway-{index:03}.bmp",0\n'
code += f" mov dtClock3D,{index*40}\n mov dtHoldTime3D,{index*40}\n call DartsAim3D\n call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtSwayFrame{index}\n"
code += " ret\nDartsTestFrames ENDP\n"
code = code.replace(":SDWORD", ":DWORD")
data += "\n.data\nactionBmpHeader db 42h,4Dh\n dd 1228854\n dw 0,0\n dd 54,40,640,-480\n dw 1,32\n dd 0,1228800,0,0,0,0\n"
own_funcs = set(re.findall(r"^(\w+) PROC", code, re.M))
own_data = set(re.findall(r"^(\w+)\s+(?:db|dw|dd|REAL4|REAL8|AnimPack3D|LABEL)\b", data + "\n" + code, re.M))
source = "\n".join(read(str(p.relative_to(ROOT))) for p in
[ROOT / "poker.asm", ROOT / "casino3d.inc", ROOT / "casino3d_data.inc",
*sorted(p for p in (ROOT / "src_graphics").glob("*.inc") if p.name != "renderer.inc")])
structures = [re.search(rf"^{name} STRUCT\b.*?^{name} ENDS", source, re.M | re.S).group()
for name in ("AnimPack3D", "BJBot3D")]
types = {name: kind for name, kind in re.findall(r"^(\w+)\s+(AnimPack3D|BJBot3D)\b", source, re.M)}
types.update({name: {"db": "BYTE", "dw": "WORD", "dd": "DWORD"}[kind.lower()]
for name, kind in re.findall(r"^(\w+)\s+(db|dw|dd)\b", source, re.M | re.I)})
types.update({name: kind.upper() for name, kind in
re.findall(r"^(\w+)\s+(REAL4|REAL8)\b", source, re.M | re.I)})
coff = Coff(ROOT / "build/graphics/poker.obj")
helper = bytearray(coff.data)
symbol_at = struct.unpack_from("<I", helper, 8)[0]
externs, protos = [], {}
for index, name in coff.symbols.items():
pos = symbol_at + index * 18
_, section, kind, storage, _ = struct.unpack_from("<IhHBB", helper, pos + 8)
if section > 0 and re.fullmatch(r"[A-Za-z_]\w*", name) and storage in (2, 3):
secname = helper[20+(section-1)*40:28+(section-1)*40].rstrip(b"\0")
if secname in (b".data", b".bss", b".const"):
helper[pos+16] = 3 if name in own_data else 2
if name not in own_data:
externs.append(f"EXTERN {name}:{types.get(name, 'DWORD')}")
match = re.fullmatch(r"_(\w+)@(\d+)", name)
if match and kind == 0x20 and section > 0:
helper[pos+16] = 3 if match[1] in own_funcs else 2
if match[1] not in own_funcs:
protos[match[1]] = int(match[2]) // 4
for name, params in re.findall(r"^(\w+)\s+PROTO([^\n]*)", source, re.M):
protos.setdefault(name, params.count(":DWORD"))
for name in own_funcs:
protos[name] = len(re.findall(r":S?DWORD", proc(code, name).splitlines()[0]))
constants = dict(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)", source, re.M))
used = set(re.findall(r"^(\w+) equ", data, re.M))
ordered = []
def constant(name):
if name not in constants or name in used:
return
used.add(name)
for dep in re.findall(r"\b[A-Za-z_]\w*", constants[name]):
constant(dep)
ordered.append(f"{name} equ {constants[name]}")
for name in re.findall(r"\b[A-Za-z_]\w*", code + data):
constant(name)
header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures + ordered + externs)
header += "\noption language:stdcall\n" + "\n".join(
name + " PROTO" + (" " + ",".join([":DWORD"]*count) if count else "") for name, count in protos.items())
data = re.sub(r"^\w+ PROTO[^\n]*\n", "", data, flags=re.M)
(OUT / "helpers.obj").write_bytes(helper)
(OUT / "module.asm").write_text(header + "\n" + data + "\n.code\n" + code + "\nend DartsTestStart\n", encoding="cp1251")
return {name for name in own_funcs if not name.startswith(("DartsTest", "DartsUi"))} - {
"PlayCasinoSound3D", "WriteActionSnapshot3D"}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity", action="store_true")
parser.add_argument("--interactive", action="store_true")
args = parser.parse_args()
functions = prepare()
if args.parity:
tested, shipping = Coff(OUT / "module.obj"), Coff(ROOT / "build/graphics/poker.obj")
for name in functions:
symbol = next(key for key in tested.functions if key.startswith("_" + name + "@"))
assert signature(tested, symbol, tail_branches=True) == signature(shipping, symbol, tail_branches=True), name
print("PASS: dart scene, scoring, input and camera match the shipping object")
return
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
"/Fobuild/darts/module.obj", "build/darts/module.asm"], cwd=ROOT, check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
target = "ui" if args.interactive else "module"
entry = "DartsUiStart" if args.interactive else "DartsTestStart"
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:WINDOWS", f"/ENTRY:{entry}@0",
f"/LIBPATH:{sdk}", f"/LIBPATH:{masm/'lib'}", f"/OUT:build/darts/{target}.exe",
"build/darts/module.obj", "build/darts/helpers.obj", "kernel32.lib", "user32.lib",
"gdi32.lib", "winmm.lib", "ws2_32.lib"], cwd=ROOT, check=True)
def profile():
return {str(p): hashlib.sha256(p.read_bytes()).digest() for p in (ROOT / "runtime").rglob("*")
if p.is_file()}
before = profile()
run = subprocess.run([str(OUT / (target + ".exe"))], cwd=ROOT, timeout=900 if args.interactive else 180)
assert run.returncode == 0, f"Native darts assertion: {run.returncode}"
assert profile() == before, "Darts changed persistent profile files"
if args.interactive:
print("PASS: interactive native scene closed without changing the profile")
return
frames = [Image.open(p).convert("RGB") for p in sorted(OUT.glob("frame-*.bmp"))]
assert len(frames) >= 60 and len({f.tobytes() for f in frames}) >= 40
assert all(len(f.getcolors(640*480)) > 80 for f in frames)
frames[0].save(OUT / "throw.gif", save_all=True, append_images=frames[1:], duration=40, loop=0)
sheet = Image.new("RGB", (640*3, 480*2))
for slot, index in enumerate((0, 10, 22, 36, 48, len(frames)-1)):
sheet.paste(frames[index], ((slot % 3)*640, (slot // 3)*480))
sheet.save(OUT / "contact.png")
sway = [Image.open(p).convert("RGB") for p in sorted(OUT.glob("sway-*.bmp"))]
assert len(sway) == 80 and len({f.tobytes() for f in sway}) > 24
sway[0].save(OUT / "sway.gif", save_all=True, append_images=sway[1:], duration=40, loop=0)
triples, = struct.unpack("<I", (OUT / "aim-metrics.bin").read_bytes())
print(f"PASS: stationary centre-of-T20 aim scored triples in {triples}/100 native samples")
print("PASS: 67 scoring vectors, 20 complete bot matches, bust/turn isolation, fine mouse/release, sway/fatigue, board footprint, Tab, orbit camera and native moving frames")
if __name__ == "__main__":
main()

View file

@ -24,6 +24,11 @@ def proc(source, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP",source,re.M|re.S).group()
def expand(path):
return re.sub(r"^include ([^\n]+)$", lambda m: expand(m[1].strip().replace("\\", "/")),
read(path), flags=re.M)
def prepare():
OUT.mkdir(exist_ok=True)
code = read("src_graphics/hall_expansion.inc") + "\n" + read("src_graphics/cabaret.inc")
@ -32,6 +37,10 @@ def prepare():
code += "\n" + read("src_graphics/fortune_room.inc")
code += "\n" + read("src_graphics/fortune_dialog.inc")
code += "\n" + proc(read("src_graphics/unavailable.inc"),"BeginUnavailable3D")
code += "\n" + read("src_graphics/observer.inc")
code += "\n" + expand("src_graphics/darts.inc").replace(":SDWORD", ":DWORD")
code += "\n" + proc(read("src_graphics/native_poker.inc"),"PokerBotAct3D")
code += "\n" + proc(read("src_graphics/native_durak.inc"),"DurakBotTurn3D")
code += "\n" + proc(read("src_graphics/world_dressing.inc"),"RenderHallDressing3D")
for path, names in (
("src_graphics/hall_scene.inc",["RenderCeilingAndFloor","RenderSprites","InitHall3D",
@ -53,8 +62,13 @@ def prepare():
npc = npc[npc.index("npcGuestArt3D "):npc.index("npcLifeClock3D ")]
data = "\n".join(read(path) for path in ("src_graphics/hall_expansion_data.inc",
"src_graphics/cabaret_data.inc","src_graphics/hall_layout.inc",
"src_graphics/fortune_room_data.inc","src_graphics/fortune_dialog_data.inc")) + "\n.data\n" + npc
own_data = set(re.findall(r"^(\w+) (?:dd|db|AnimPack3D|CityProp3D)\b",data,re.M))
"src_graphics/fortune_room_data.inc","src_graphics/fortune_dialog_data.inc",
"src_graphics/observer_data.inc")) + "\n.data\n" + npc
darts = read("src_graphics/darts_data.inc").replace(
"include src_graphics\\darts_art.inc", read("src_graphics/darts_art.inc"))
data += "\n" + re.sub(r"^\w+ PROTO[^\n]*\n", "", darts, flags=re.M)
own_data = set(re.findall(r"^(\w+) (?:dd|db|dw|REAL4|REAL8|AnimPack3D|CityProp3D)\b",data,re.M))
own_data.update(re.findall(r"^(\w+) LABEL\b",data,re.M))
coff = Coff(ROOT / "build/graphics/poker.obj")
helper = bytearray(coff.data)
at = struct.unpack_from("<I",helper,8)[0]
@ -80,6 +94,8 @@ def prepare():
types[name] = "CitySpriteCrop3D"
for name,kind in re.findall(r"^(\w+)\s+(db|dw|dd)\b",all_source,re.M|re.I):
types.setdefault(name,{"db":"BYTE","dw":"WORD","dd":"DWORD"}[kind.lower()])
for name,kind in re.findall(r"^(\w+)\s+(REAL4|REAL8)\b",all_source,re.M|re.I):
types[name] = kind.upper()
for index,name in coff.symbols.items():
pos = at+index*18
_,sec,kind,storage,_ = struct.unpack_from("<IhHBB",helper,pos+8)
@ -135,7 +151,6 @@ def main():
parser.add_argument("--prove-seam-regression",action="store_true")
parser.add_argument("--prove-distance-regression",action="store_true")
parser.add_argument("--prove-bar-camera-regression",action="store_true")
parser.add_argument("--with-fortune-ai",action="store_true")
args = parser.parse_args()
if args.parity:
native,shipping = Coff(OUT/"module.obj"),Coff(ROOT/"build/graphics/poker.obj")
@ -151,10 +166,6 @@ def main():
assert "DrawCityObject3D" not in room, "tarot furniture must not face the camera"
assert room.count("DrawFortuneFixedProp3D") == 24
stem = "module"
if args.with_fortune_ai:
stem = "module-live-ai"
source = (OUT/"module.asm").read_text(encoding="cp1251")
(OUT/(stem+".asm")).write_text("CASINO_FORTUNE_AI_LIVE equ 1\n" + source,encoding="cp1251")
if args.prove_bar_camera_regression:
stem = "module-camera-regression"
source = (OUT/"module.asm").read_text(encoding="cp1251")
@ -224,6 +235,31 @@ def main():
assert blue > 45000, (name, "chamber must use its own blue palette", blue)
modal = Image.open(OUT/"fortune-unavailable.bmp").convert("RGB").crop((20,65,620,100))
assert len(modal.getcolors(600*35)) >= 2, "native fortune dialogue status must be drawn"
# These are frames rendered by the actual x86 procedures, not a Python scene simulation.
motion = [Image.open(OUT/f"fortune-session-motion-{i:02}.bmp").convert("RGB") for i in range(64)]
assert len({im.crop((134,32,414,282)).tobytes() for im in motion}) >= 48
motion[0].save(OUT/"fortune-session-shuffle.gif",save_all=True,
append_images=motion[1:],duration=25,loop=0)
stages = ("deal-one","deal-two","deal-three","flip-one","explain-one",
"flip-two","explain-two","flip-three","explain-three","summary")
frames = [Image.open(OUT/f"fortune-session-{s}.bmp").convert("RGB") for s in stages]
assert len({im.crop((190,285,475,398)).tobytes() for im in frames}) >= 7
question = frames[0].crop((18,400,620,444)).tobytes()
assert all(im.crop((18,400,620,444)).tobytes() == question for im in frames)
for name in ("room","shuffle","deal","gesture","orb-hands","orb-mist","cards"):
path = ROOT/f"assets/animations/fortune/{name}-v1.bin"
data = path.read_bytes()
magic,w,h,count,size = struct.unpack_from("<4s4I",data)
assert magic == b"ANM1" and len(data) == size+20 and size == w*h*4*count
assert count <= 64 and size <= 32*1024*1024
if count > 1:
pixels = [data[20+i*w*h*4:20+(i+1)*w*h*4] for i in range(count)]
assert len(set(pixels)) >= count-3, (name,"animation frames must differ")
if name == "cards":
for i in range(22):
upright = Image.frombytes("RGBA",(w,h),pixels[i],"raw","BGRA")
reversed_card = Image.frombytes("RGBA",(w,h),pixels[i+22],"raw","BGRA")
assert upright.transpose(Image.Transpose.ROTATE_180).tobytes() == reversed_card.tobytes()
entrance = Image.open(OUT / (("old-seam-" if args.prove_seam_regression else "") +
"upper-entrance-open.bmp")).convert("RGB").crop((200,150,300,350))
dominant = max(count for count,_ in entrance.getcolors(20000))/20000
@ -308,8 +344,6 @@ def main():
x0,y0,x1,y1 = sprite.getbbox()
assert 0 < x0 < x1 < w and 0 < y0 < y1 < h, "uncropped musician cutout"
print("PASS: native stairs/doorways, automatic door, tickets, gait, portal, secret passage, bar camera and fortune dialogue")
if args.with_fortune_ai:
print("PASS: actual x86 game input/IPC/rendering with the x64 local model, response and child cleanup")
if __name__ == "__main__":

View file

@ -0,0 +1,148 @@
"""Compile fresh native spectator/game code and run autonomous matches, not mock results."""
from pathlib import Path
import argparse
import hashlib
import re
import struct
import subprocess
from PIL import Image
from check_card_exit_build import Coff
from check_terminals import signature
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "build/observer"
def read(path):
raw = (ROOT / path).read_bytes()
try:
return raw.decode("utf8").replace("\r\n", "\n")
except UnicodeDecodeError:
return raw.decode("cp1251").replace("\r\n", "\n")
def proc(text, name):
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", text, re.M | re.S).group()
def expand(path):
return re.sub(r"^include ([^\n]+)$", lambda m: expand(m[1].strip().replace("\\", "/")),
read(path), flags=re.M)
def prepare():
OUT.mkdir(parents=True, exist_ok=True)
code = "\n".join(expand("src_graphics/" + name + ".inc") for name in
("observer", "native_poker", "native_durak", "native_blackjack", "pyramid_play", "eightball",
"card_tables_render", "durak_render"))
for path, names in (("platform_code", ("SaveProgress3D",)),
("npc_contracts", ("NpcRecordRound3D",)),
("charms", ("TriggerCharmLossProtection",)),
("card_table_input", ("HandleCardExit3D", "RequestCardExit3D", "LeaveCardTable3D", "TableBusy3D"))):
code += "\n" + "\n".join(proc(read("src_graphics/" + path + ".inc"), name) for name in names)
code += "\n" + proc(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D")
code += "\n" + read("tools/tests/durak_test.inc")
code += "\nPlayCasinoSound3D PROC arg0:DWORD,arg1:DWORD,arg2:DWORD\n ret\nPlayCasinoSound3D ENDP\n"
# Stop before any persistence side effect even if a tested observer barrier regresses.
for name in ("RefreshTerminalAchievements3D", "NpcEnsureJournal3D"):
code += f"\n{name} PROC\n invoke ExitProcess,8402\n ret\n{name} ENDP\n"
code += "\n" + read("tools/tests/observer_test.inc")
code = code.replace(":SDWORD", ":DWORD")
data = read("src_graphics/observer_data.inc")
data += "\n.data\nactionBmpHeader db 42h,4Dh\n dd 1228854\n dw 0,0\n dd 54,40,640,-480\n dw 1,32\n dd 0,1228800,0,0,0,0\n"
own_funcs = set(re.findall(r"^(\w+) PROC", code, re.M))
own_data = set(re.findall(r"^(\w+)\s+(?:db|dd)\b", data + "\n" + code, re.M))
source = "\n".join(read(str(p.relative_to(ROOT))) for p in
[ROOT / "poker.asm", ROOT / "casino3d.inc", ROOT / "casino3d_data.inc",
*sorted(p for p in (ROOT / "src_graphics").glob("*.inc") if p.name != "renderer.inc")])
structures = [re.search(rf"^{name} STRUCT\b.*?^{name} ENDS", source, re.M | re.S).group()
for name in ("AnimPack3D", "BJBot3D")]
types = {name: kind for name, kind in re.findall(r"^(\w+)\s+(AnimPack3D|BJBot3D)\b", source, re.M)}
types.update({name: {"db": "BYTE", "dw": "WORD", "dd": "DWORD"}[kind.lower()]
for name, kind in re.findall(r"^(\w+)\s+(db|dw|dd)\b", source, re.M | re.I)})
types.update({name: kind.upper() for name, kind in
re.findall(r"^(\w+)\s+(REAL4|REAL8)\b", source, re.M | re.I)})
coff = Coff(ROOT / "build/graphics/poker.obj")
helper = bytearray(coff.data)
symbol_at = struct.unpack_from("<I", helper, 8)[0]
externs, protos = [], {}
for index, name in coff.symbols.items():
pos = symbol_at + index * 18
_, section, kind, storage, _ = struct.unpack_from("<IhHBB", helper, pos + 8)
if section > 0 and re.fullmatch(r"[A-Za-z_]\w*", name) and storage in (2, 3):
secname = helper[20+(section-1)*40:28+(section-1)*40].rstrip(b"\0")
if secname in (b".data", b".bss", b".const"):
helper[pos+16] = 3 if name in own_data else 2
if name not in own_data:
externs.append(f"EXTERN {name}:{types.get(name, 'DWORD')}")
match = re.fullmatch(r"_(\w+)@(\d+)", name)
if match and kind == 0x20 and section > 0:
helper[pos+16] = 3 if match[1] in own_funcs else 2
if match[1] not in own_funcs:
protos[match[1]] = int(match[2]) // 4
for name, params in re.findall(r"^(\w+)\s+PROTO([^\n]*)", source, re.M):
protos.setdefault(name, params.count(":DWORD"))
for name in own_funcs:
protos[name] = len(re.findall(r":S?DWORD", proc(code, name).splitlines()[0]))
constants = dict(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)", source, re.M))
used = set(re.findall(r"^(\w+) equ", data, re.M))
ordered = []
def constant(name):
if name not in constants or name in used:
return
used.add(name)
for dep in re.findall(r"\b[A-Za-z_]\w*", constants[name]):
constant(dep)
ordered.append(f"{name} equ {constants[name]}")
for name in re.findall(r"\b[A-Za-z_]\w*", code + data):
constant(name)
header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures + ordered + externs)
header += "\noption language:stdcall\n" + "\n".join(
name + " PROTO" + (" " + ",".join([":DWORD"]*count) if count else "") for name, count in protos.items())
data = re.sub(r"^\w+ PROTO[^\n]*\n", "", data, flags=re.M)
(OUT / "helpers.obj").write_bytes(helper)
(OUT / "module.asm").write_text(header + "\n" + data + "\n.code\n" + code + "\nend ObserverTestStart\n", encoding="cp1251")
return {name for name in own_funcs if not name.startswith("DurakTest") and name != "RunDurakTest3D"} - {
"PlayCasinoSound3D", "WriteActionSnapshot3D", "ObserverTestStart",
"RefreshTerminalAchievements3D", "NpcEnsureJournal3D"}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--parity", action="store_true")
args = parser.parse_args()
functions = prepare()
if args.parity:
tested, shipping = Coff(OUT / "module.obj"), Coff(ROOT / "build/graphics/poker.obj")
for name in functions:
symbol = next(key for key in tested.functions if key.startswith("_" + name + "@"))
assert signature(tested, symbol, tail_branches=True) == signature(shipping, symbol, tail_branches=True), name
print("PASS: native observer and game code match the shipping object")
return
masm = Path("F:/MASM32/masm32")
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
"/Fobuild/observer/module.obj", "build/observer/module.asm"], cwd=ROOT, check=True)
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:CONSOLE", "/ENTRY:ObserverTestStart@0",
f"/LIBPATH:{sdk}", f"/LIBPATH:{masm/'lib'}", "/OUT:build/observer/module.exe",
"build/observer/module.obj", "build/observer/helpers.obj", "kernel32.lib", "user32.lib",
"gdi32.lib", "winmm.lib", "ws2_32.lib"], cwd=ROOT, check=True)
def profile():
return {str(p): hashlib.sha256(p.read_bytes()).digest() for p in (ROOT / "runtime").rglob("*")
if p.is_file()}
before = profile()
run = subprocess.run([str(OUT / "module.exe")], cwd=ROOT, timeout=180)
assert run.returncode == 0, f"Native observer assertion: {run.returncode}"
assert profile() == before, "Observation changed persistent profile files"
sheet = Image.new("RGB", (1280, 480*4))
for i, name in enumerate(("menu", "poker", "blackjack", "durak", "transfer", "pyramid", "eight")):
frame = Image.open(OUT / (name + ".bmp")).convert("RGB")
assert len(frame.getcolors(640*480)) >= (4 if name == "menu" else 500), name
frame.save(OUT / (name + ".png"))
sheet.paste(frame, ((i % 2)*640, (i // 2)*480))
sheet.save(OUT / "contact.png")
print("PASS: 80 autonomous card matches, both billiard seats, pause, profile restore and no persistent writes")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,42 @@
.data
dtHallSnapshot db "build\hall-expansion\darts-hall.bmp",0
.code
ExpansionDartsTests PROC
mov expansionStage,8510
mov hallLevel3D,0
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET casinoMap
mov playerX,29*FP_1
mov playerY,28*FP_1
mov playerAngle,0
invoke CanWalk3D,playerX,playerY
test eax,eax
jz dt_hall_fail
call UpdateInteriorEye3D
call ExpansionFrame
invoke ExpansionSnapshot,OFFSET dtHallSnapshot
call CheckHallInteractions3D
cmp interactTargetId,INTERACT_DARTS
jne dt_hall_fail
call TriggerHallInteraction3D
test eax,eax
jz dt_hall_fail
cmp engineMode,MODE_DARTS
jne dt_hall_fail
cmp dtError3D,0
jne dt_hall_fail
cmp dtScores3D,500
jne dt_hall_fail
cmp dtScores3D[4],500
jne dt_hall_fail
cmp playerX,29*FP_1
jne dt_hall_fail
cmp playerY,28*FP_1
jne dt_hall_fail
call FreeDarts3D
mov engineMode,MODE_WALK
ret
dt_hall_fail:
invoke ExitProcess,8510
ret
ExpansionDartsTests ENDP

374
tools/tests/darts_test.inc Normal file
View file

@ -0,0 +1,374 @@
.data
dtVectors3D LABEL DWORD
dd 0,53,20,1
dd 0,103,60,3
dd 0,166,40,2
dd 16,50,1,1
dd 32,98,3,3
dd 51,158,2,2
dd 31,43,18,1
dd 61,83,54,3
dd 98,134,36,2
dd 43,31,4,1
dd 83,61,12,3
dd 134,98,8,2
dd 50,16,13,1
dd 98,32,39,3
dd 158,51,26,2
dd 53,0,6,1
dd 103,0,18,3
dd 166,0,12,2
dd 50,-16,10,1
dd 98,-32,30,3
dd 158,-51,20,2
dd 43,-31,15,1
dd 83,-61,45,3
dd 134,-98,30,2
dd 31,-43,2,1
dd 61,-83,6,3
dd 98,-134,4,2
dd 16,-50,17,1
dd 32,-98,51,3
dd 51,-158,34,2
dd 0,-53,3,1
dd 0,-103,9,3
dd 0,-166,6,2
dd -16,-50,19,1
dd -32,-98,57,3
dd -51,-158,38,2
dd -31,-43,7,1
dd -61,-83,21,3
dd -98,-134,14,2
dd -43,-31,16,1
dd -83,-61,48,3
dd -134,-98,32,2
dd -50,-16,8,1
dd -98,-32,24,3
dd -158,-51,16,2
dd -53,0,11,1
dd -103,0,33,3
dd -166,0,22,2
dd -50,16,14,1
dd -98,32,42,3
dd -158,51,28,2
dd -43,31,9,1
dd -83,61,27,3
dd -134,98,18,2
dd -31,43,12,1
dd -61,83,36,3
dd -98,134,24,2
dd -16,50,5,1
dd -32,98,15,3
dd -51,158,10,2
dd 0,0,50,2
dd 0,6,50,2
dd 0,10,25,1
dd 0,16,20,1
dd 0,190,0,0
dd 2147483647,0,0,0
dd -2147483648,0,0,0
dtVectorCount3D equ 67
dtTestTriples3D dd 0
dtTestMetricPath db "build\darts\aim-metrics.bin",0
.code
DartsTestWriteMetrics PROC uses ebx
local written:DWORD
invoke CreateFileA,OFFSET dtTestMetricPath,40000000h,0,0,2,80h,0
cmp eax,-1
je dt_test_metric_fail
mov ebx,eax
invoke WriteFile,ebx,OFFSET dtTestTriples3D,4,ADDR written,0
invoke CloseHandle,ebx
ret
dt_test_metric_fail:
invoke ExitProcess,8505
ret
DartsTestWriteMetrics ENDP
DartsTestStart PROC uses ebx esi edi
local legs:DWORD
call InitPaths3D
lea esi,dtVectors3D
mov ebx,dtVectorCount3D
dt_test_vector:
invoke DartsScore3D,[esi],[esi+4]
cmp eax,[esi+8]
jne dt_test_fail
cmp edx,[esi+12]
jne dt_test_fail
add esi,16
dec ebx
jnz dt_test_vector
mov playerX,123456
mov playerY,789012
mov bankroll,12345
call ResetDarts3D
mov dtCursorX3D,320*256
mov dtCursorY3D,214*256
mov dtClock3D,384
mov dtHolding3D,1
mov dtHoldTime3D,0
call DartsAim3D
cmp dtAimX3D,10
jl dt_test_fail
cmp dtAimX3D,13
jg dt_test_fail
mov ebx,dtAimX3D
mov dtHoldTime3D,4000
call DartsAim3D
mov eax,dtAimX3D
sub eax,ebx
cmp eax,8
jne dt_test_fail
; Fine mouse motion must survive the hall's coarser 320px UI coordinates.
mov engineMode,MODE_DARTS
mov viewWidth3D,2560
mov viewHeight3D,1440
mov viewLeft3D,0
mov viewTop3D,0
invoke WindowEvents3D,0,200h,0,02820500h
call DartsAim3D
mov ebx,dtAimX3D
invoke WindowEvents3D,0,200h,0,02820502h
call DartsAim3D
cmp dtAimX3D,ebx
jle dt_test_fail
cmp mouseX3D,160
jne dt_test_fail
mov ebx,dtAimY3D
invoke WindowEvents3D,0,200h,0,02840502h
call DartsAim3D
cmp dtAimY3D,ebx
jge dt_test_fail
cmp mouseY3D,107
jne dt_test_fail
call ResetDarts3D
call DartsCamera3D
invoke DartsProject3D,-248,0,DT_WALL
mov ebx,eax
invoke DartsProject3D,248,0,DT_WALL
sub eax,ebx
cmp eax,170
jl dt_test_fail
cmp eax,180
jg dt_test_fail
; Camping at the centre of T20 no longer guarantees a triple every time.
mov dtCursorX3D,320*256
mov dtCursorY3D,214*256-103*DT_FOCAL*256/DT_WALL
mov dtClock3D,0
mov dtTestTriples3D,0
mov edi,100
dt_test_sway_samples:
call DartsAim3D
invoke DartsScore3D,dtAimX3D,dtAimY3D
cmp eax,60
jne @F
inc dtTestTriples3D
@@:
add dtClock3D,16
dec edi
jnz dt_test_sway_samples
cmp dtTestTriples3D,10
jb dt_test_fail
cmp dtTestTriples3D,60
ja dt_test_fail
call DartsTestWriteMetrics
mov dtState3D,DT_FLIGHT
mov dtTimer3D,400
mov deltaTime3D,16
mov pressedKeys3D[9],1
call HandleDarts3D
cmp dtScoreOpen3D,1
jne dt_test_fail
cmp dtTimer3D,400
jne dt_test_fail
mov pressedKeys3D[9],1
call HandleDarts3D
cmp dtScoreOpen3D,0
jne dt_test_fail
cmp dtTimer3D,416
jne dt_test_fail
call ResetDarts3D
mov dtHitX3D,0
mov dtHitY3D,103
mov dtRemaining3D,59
mov dtVisitStart3D,100
mov dtState3D,DT_FLIGHT
call SettleDarts3D
cmp dtRemaining3D,100
jne dt_test_fail
cmp dtBust3D,1
jne dt_test_fail
cmp dtCount3D,1
jne dt_test_fail
call SettleDarts3D
cmp dtCount3D,1
jne dt_test_fail
call ResetDarts3D
mov dtHitX3D,0
mov dtHitY3D,166
mov dtRemaining3D,40
mov dtVisitStart3D,40
mov dtState3D,DT_FLIGHT
call SettleDarts3D
cmp dtRemaining3D,0
jne dt_test_fail
mov dtTimer3D,1300
mov deltaTime3D,16
call HandleDarts3D
cmp dtState3D,DT_WIN
jne dt_test_fail
call ResetDarts3D
mov dtHitY3D,166
mov dtRemaining3D,41
mov dtVisitStart3D,41
mov dtState3D,DT_FLIGHT
call SettleDarts3D
cmp dtRemaining3D,1
jne dt_test_fail
cmp dtBust3D,0
jne dt_test_fail
mov dtCount3D,3
mov dtState3D,DT_RETURN
mov dtTimer3D,750
call HandleDarts3D
cmp dtCount3D,0
jne dt_test_fail
cmp dtVisit3D,2
jne dt_test_fail
cmp dtActor3D,1
jne dt_test_fail
cmp dtRemaining3D,500
jne dt_test_fail
cmp dtScores3D,1
jne dt_test_fail
cmp dtScores3D[4],500
jne dt_test_fail
call ResetDarts3D
mov engineMode,MODE_DARTS
mov viewWidth3D,640
mov viewHeight3D,480
mov viewLeft3D,0
mov viewTop3D,0
mov mouseClick3D,0
invoke WindowEvents3D,0,200h,0,00D60140h
cmp mouseClick3D,0
jne dt_test_fail
invoke WindowEvents3D,0,201h,1,00D60140h
call HandleDarts3D
cmp dtState3D,DT_AIM
jne dt_test_fail
cmp dtHolding3D,1
jne dt_test_fail
invoke WindowEvents3D,0,202h,0,00D60140h
call HandleDarts3D
cmp dtState3D,DT_RELEASE
jne dt_test_fail
cmp dtThrows3D,1
jne dt_test_fail
mov ebx,dtHitX3D
mov esi,dtHitY3D
invoke WindowEvents3D,0,200h,0,00640064h
call HandleDarts3D
cmp dtHitX3D,ebx
jne dt_test_fail
cmp dtHitY3D,esi
jne dt_test_fail
call ResetDarts3D
mov dtHitX3D,80
mov dtHitY3D,103
mov dtState3D,DT_FLIGHT
mov dtTimer3D,0
call DartsCamera3D
cmp dtCamZ3D,0
jne dt_test_fail
mov dtTimer3D,600
call DartsCamera3D
cmp dtCamZ3D,DT_WALL/2-160
jne dt_test_fail
cmp dtYawSin3D,50000
jl dt_test_fail
mov dtTimer3D,1200
call DartsCamera3D
cmp dtCamZ3D,DT_WALL-420
jne dt_test_fail
invoke DartsProject3D,80,103,DT_WALL
cmp eax,320
jl dt_test_fail
cmp eax,420
jg dt_test_fail
cmp edx,120
jl dt_test_fail
cmp edx,214
jg dt_test_fail
cmp dtYawSin3D,12000
jg dt_test_fail
; Actual bot input ignores the cursor and automatically releases a dart.
call ResetDarts3D
mov dtActor3D,1
mov mouseX3D,0
mov mouseY3D,0
mov deltaTime3D,1600
call HandleDarts3D
cmp dtState3D,DT_RELEASE
jne dt_test_fail
cmp dtThrows3D,1
jne dt_test_fail
cmp dtScores3D,500
jne dt_test_fail
; Complete a real match through normal phase/visit transitions.
mov legs,20
dt_test_another_match:
call ResetDarts3D
mov edi,5000
dt_test_match:
cmp dtState3D,DT_WIN
je dt_test_match_done
cmp dtState3D,DT_AIM
jne dt_test_match_tick
cmp dtActor3D,0
jne dt_test_match_tick
call DartsBotAim3D
mov eax,dtBotAimX3D
mov dtAimX3D,eax
mov eax,dtBotAimY3D
mov dtAimY3D,eax
call ThrowDarts3D
dt_test_match_tick:
mov deltaTime3D,1400
call HandleDarts3D
cmp dtScores3D,500
ja dt_test_fail
cmp dtScores3D[4],500
ja dt_test_fail
dec edi
jnz dt_test_match
jmp dt_test_fail
dt_test_match_done:
mov eax,dtWinner3D
cmp dtScores3D[eax*4],0
jne dt_test_fail
dec legs
jnz dt_test_another_match
cmp playerX,123456
jne dt_test_fail
cmp playerY,789012
jne dt_test_fail
cmp bankroll,12345
jne dt_test_fail
call LoadDarts3D
test eax,eax
jz dt_test_assets
call DartsTestFrames
call FreeDarts3D
xor eax,eax
jmp dt_test_exit
dt_test_assets:
mov eax,8502
jmp dt_test_exit
dt_test_fail:
mov eax,8501
dt_test_exit:
invoke ExitProcess,eax
DartsTestStart ENDP

64
tools/tests/darts_ui.inc Normal file
View file

@ -0,0 +1,64 @@
; Interactive fixture reuses the actual native scene, window events and presenter.
.code
DartsUiStart PROC
call InitPaths3D
call Create3DWindow
test eax,eax
jz dt_ui_fail
invoke SetWindowLongA,h3DWnd,-4,OFFSET WindowEvents3D
call ToggleFullscreen3D
call BeginDarts3D
call RenderDarts3D
invoke ShowWindow,h3DWnd,1
call Present3D
invoke SetForegroundWindow,h3DWnd
invoke GetTickCount
mov previousTick3D,eax
dt_ui_loop:
invoke PeekMessageA,OFFSET msg3D,0,0,0,1
test eax,eax
jz dt_ui_frame
cmp dword ptr msg3D[4],12h
je dt_ui_close
invoke TranslateMessage,OFFSET msg3D
invoke DispatchMessageA,OFFSET msg3D
jmp dt_ui_loop
dt_ui_frame:
invoke GetTickCount
mov frameStart3D,eax
sub eax,previousTick3D
cmp eax,50
jbe @F
mov eax,50
@@:
mov deltaTime3D,eax
mov eax,frameStart3D
mov previousTick3D,eax
invoke GetForegroundWindow
cmp eax,h3DWnd
jne dt_ui_idle
invoke Pressed3D,1Bh
test eax,eax
jnz dt_ui_close
invoke Pressed3D,7Ah
test eax,eax
jz @F
call ToggleFullscreen3D
@@:
call HandleDarts3D
call RenderDarts3D
call Present3D
dt_ui_idle:
invoke Sleep,16
jmp dt_ui_loop
dt_ui_close:
call FreeDarts3D
invoke ReleaseDC,h3DWnd,h3DDC
invoke DestroyWindow,h3DWnd
dt_ui_exit:
invoke ExitProcess,0
ret
dt_ui_fail:
invoke ExitProcess,8503
ret
DartsUiStart ENDP

View file

@ -1,112 +1,243 @@
.data
fortuneLiveQuestion db 209,242,238,232,242,32,235,232,32,237,224,247,224,242,252,32,243,247,232,242,252,241,255,32,240,232,241,238,226,224,237,232,254,63,0
fortuneLiveShot db "build\hall-expansion\fortune-dialog-answer.bmp",0
fortuneLiveBank dd 0
fortuneSessionBank dd 0
fortuneSessionPlayer dd 0
fortuneSessionMotion db "build\hall-expansion\fortune-session-motion-"
fortuneSessionDigits db "00.bmp",0
fortuneSessionQuestion db 193,243,228,229,242,32,235,232,32,231,224,226,242,240,224,32,228,238,230,228,252,63,0
fortuneSessionShot0 db "build\hall-expansion\fortune-session-input.bmp",0
fortuneSessionShot1 db "build\hall-expansion\fortune-session-shuffle.bmp",0
fortuneSessionShot2 db "build\hall-expansion\fortune-session-deal-one.bmp",0
fortuneSessionShot3 db "build\hall-expansion\fortune-session-deal-two.bmp",0
fortuneSessionShot4 db "build\hall-expansion\fortune-session-deal-three.bmp",0
fortuneSessionShot5 db "build\hall-expansion\fortune-session-flip-one.bmp",0
fortuneSessionShot6 db "build\hall-expansion\fortune-session-explain-one.bmp",0
fortuneSessionShot7 db "build\hall-expansion\fortune-session-flip-two.bmp",0
fortuneSessionShot8 db "build\hall-expansion\fortune-session-explain-two.bmp",0
fortuneSessionShot9 db "build\hall-expansion\fortune-session-flip-three.bmp",0
fortuneSessionShot10 db "build\hall-expansion\fortune-session-explain-three.bmp",0
fortuneSessionShot11 db "build\hall-expansion\fortune-session-summary.bmp",0
fortuneSessionShot12 db "build\hall-expansion\fortune-session-done.bmp",0
fortuneSessionShot13 db "build\hall-expansion\fortune-session-orb-rise.bmp",0
fortuneSessionShot14 db "build\hall-expansion\fortune-session-orb-mist.bmp",0
fortuneSessionShot15 db "build\hall-expansion\fortune-session-orb-final.bmp",0
fortuneSessionShots dd OFFSET fortuneSessionShot0,OFFSET fortuneSessionShot1,OFFSET fortuneSessionShot2,OFFSET fortuneSessionShot3
dd OFFSET fortuneSessionShot4,OFFSET fortuneSessionShot5,OFFSET fortuneSessionShot6,OFFSET fortuneSessionShot7
dd OFFSET fortuneSessionShot8,OFFSET fortuneSessionShot9,OFFSET fortuneSessionShot10,OFFSET fortuneSessionShot11
dd OFFSET fortuneSessionShot12
.code
FortuneDialogNativeTests PROC uses esi edi
FortuneSessionStepTest PROC
add animationClock3D,100
call UpdateFortuneSession3D
ret
FortuneSessionStepTest ENDP
FortuneDialogNativeTests PROC uses ebx esi edi
mov expansionStage,80
mov hallLevel3D,3
mov engineMode,MODE_WALK
mov worldMap3D,OFFSET fortuneRoomMap3D
mov floorTexture3D,134
mov playerX,FP_1*155/10
mov playerY,11*FP_1
mov playerAngle,256
call UpdateInteriorEye3D
call ExpansionFrame
mov eax,bankroll
mov fortuneLiveBank,eax
mov fortuneSessionBank,eax
mov eax,playerX
mov fortuneSessionPlayer,eax
call BeginFortuneDialog3D
cmp fortuneSessionShuffle3D.frames,64
jne fortune_session_fail
cmp fortuneSessionDeal3D.frames,32
jne fortune_session_fail
cmp fortuneSessionCards3D.frames,45
jne fortune_session_fail
cmp fortuneProcess3D,0
jne fortune_session_fail
cmp fortuneRead3D,0
jne fortune_session_fail
invoke WindowEvents3D,0,102h,'1',0
cmp fortuneSubMode3D,FORTUNE_SUBMODE_TAROT
jne fortune_session_fail
; Lower edge of the question box is not an Exit-button hit target.
mov mouseX3D,260
mov mouseY3D,222
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp engineMode,MODE_FORTUNE
jne fortune_live_fail
cmp fortuneWorkerState3D,1
jne fortune_live_fail
jne fortune_session_fail
call SubmitFortuneQuestion3D
cmp fortunePhase3D,0
jne fortune_session_fail
mov esi,250
fortune_live_full_input:
fortune_session_input_limit:
invoke WindowEvents3D,0,102h,'a',0
dec esi
jnz fortune_live_full_input
jnz fortune_session_input_limit
cmp fortuneQuestionLength3D,240
jne fortune_live_fail
jne fortune_session_fail
cmp fortuneQuestion3D[240],0
jne fortune_live_fail
jne fortune_session_fail
invoke WindowEvents3D,0,102h,8,0
cmp fortuneQuestionLength3D,239
jne fortune_live_fail
jne fortune_session_fail
mov fortuneQuestionLength3D,0
mov fortuneQuestion3D,0
mov esi,OFFSET fortuneLiveQuestion
fortune_live_type:
mov esi,OFFSET fortuneSessionQuestion
fortune_session_input:
movzx eax,BYTE PTR [esi]
test eax,eax
jz fortune_live_ready
jz fortune_session_input_done
invoke WindowEvents3D,0,102h,eax,0
inc esi
jmp fortune_live_type
fortune_live_ready:
mov edi,9000
fortune_live_wait_ready:
call PollFortuneWorker3D
cmp fortuneWorkerState3D,2
je fortune_live_submit
cmp fortuneWorkerState3D,4
je fortune_live_fail
invoke Sleep,20
dec edi
jnz fortune_live_wait_ready
jmp fortune_live_fail
fortune_live_submit:
mov expansionStage,81
call SubmitFortuneQuestion3D
cmp fortuneWorkerState3D,3
jne fortune_live_fail
mov esi,fortuneQuestionLength3D
invoke WindowEvents3D,0,102h,'X',0
cmp fortuneQuestionLength3D,esi
jne fortune_live_fail
call SubmitFortuneQuestion3D
mov edi,9000
fortune_live_wait_answer:
call PollFortuneWorker3D
cmp fortuneWorkerState3D,2
je fortune_live_answer
cmp fortuneWorkerState3D,4
je fortune_live_fail
invoke Sleep,20
dec edi
jnz fortune_live_wait_answer
jmp fortune_live_fail
fortune_live_answer:
cmp fortuneAnswer3D,0
je fortune_live_fail
cmp fortuneQuestionLength3D,0
jne fortune_live_fail
jmp fortune_session_input
fortune_session_input_done:
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneLiveShot
invoke ExpansionSnapshot,OFFSET fortuneSessionShot0
call SubmitFortuneQuestion3D
cmp fortunePhase3D,1
jne fortune_session_fail
cmp fortuneDealt3D,0
jne fortune_session_fail
invoke WindowEvents3D,0,102h,'X',0
cmp fortuneQuestionLength3D,22
jne fortune_session_fail
call SubmitFortuneQuestion3D
cmp fortunePhase3D,1
jne fortune_session_fail
mov fortunePhaseTime3D,700
mov fortuneSpeechTime3D,1000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot1
cmp fortunePhaseTime3D,700
jne fortune_session_fail
xor edi,edi
fortune_session_motion:
mov eax,edi
xor edx,edx
mov ecx,10
div ecx
add al,'0'
add dl,'0'
mov fortuneSessionDigits[0],al
mov fortuneSessionDigits[1],dl
mov eax,edi
imul eax,25
mov fortunePhaseTime3D,eax
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionMotion
inc edi
cmp edi,64
jb fortune_session_motion
mov ebx,1
fortune_session_phases:
mov eax,fortuneDurations3D[ebx*4]
mov fortunePhaseTime3D,eax
call FortuneSessionStepTest
inc ebx
cmp fortunePhase3D,ebx
jne fortune_session_fail
mov fortunePhaseTime3D,600
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,fortuneSessionShots[ebx*4]
cmp ebx,12
jb fortune_session_phases
cmp fortuneDealt3D,3
jne fortune_session_fail
cmp fortuneRevealed3D,3
jne fortune_session_fail
cmp fortuneQuestionLength3D,22
jne fortune_session_fail
mov eax,fortuneSpeechLength3D
cmp fortuneSpeechVisible3D,eax
jne fortune_session_fail
; Distinct underlying cards even when orientation differs.
xor esi,esi
fortune_session_unique_outer:
mov eax,fortuneDrawCards3D[esi*4]
xor edx,edx
mov ecx,22
div ecx
mov edi,edx
lea ebx,[esi+1]
fortune_session_unique_inner:
cmp ebx,3
jae fortune_session_unique_next
mov eax,fortuneDrawCards3D[ebx*4]
xor edx,edx
div ecx
cmp edx,edi
je fortune_session_fail
inc ebx
jmp fortune_session_unique_inner
fortune_session_unique_next:
inc esi
cmp esi,2
jb fortune_session_unique_outer
mov expansionStage,81
; Bubble grows as glyphs appear and cannot exceed the scene edge.
invoke SetFortuneSpeech3D,OFFSET fortuneSummary3D
mov fortuneSpeechTime3D,280
call RenderFortuneDialog3D
mov esi,fortuneSpeechWidth3D
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
cmp fortuneSpeechWidth3D,esi
jbe fortune_session_fail
cmp fortuneSpeechWidth3D,204
ja fortune_session_fail
cmp fortuneSpeechHeight3D,140
ja fortune_session_fail
; Return to selection, then display the neutral, non-grading orb sequence.
mov mouseX3D,200
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp fortuneSubMode3D,0
jne fortune_session_fail
invoke WindowEvents3D,0,102h,'2',0
cmp fortuneSubMode3D,FORTUNE_SUBMODE_ORB
jne fortune_session_fail
mov fortunePhaseTime3D,700
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot13
mov fortunePhaseTime3D,1400
call FortuneSessionStepTest
cmp fortunePhase3D,2
jne fortune_session_fail
mov fortunePhaseTime3D,1500
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot14
mov fortunePhaseTime3D,5000
call FortuneSessionStepTest
cmp fortunePhase3D,3
jne fortune_session_fail
mov fortuneSpeechTime3D,3000
call RenderFortuneDialog3D
invoke ExpansionSnapshot,OFFSET fortuneSessionShot15
; Exit in the middle of an animation as well as after completion.
mov mouseX3D,260
mov mouseY3D,226
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp engineMode,MODE_WALK
jne fortune_live_fail
cmp fortuneProcess3D,0
jne fortune_live_fail
cmp fortuneRead3D,0
jne fortune_live_fail
cmp fortuneWrite3D,0
jne fortune_live_fail
jne fortune_session_fail
cmp fortuneSessionRoom3D.pixels,0
jne fortune_session_fail
call BeginFortuneDialog3D
invoke FortuneChar3D,'1'
invoke FortuneChar3D,'a'
call SubmitFortuneQuestion3D
mov mouseX3D,260
mov mouseY3D,232
mov mouseClick3D,1
call HandleFortuneDialog3D
cmp engineMode,MODE_WALK
jne fortune_session_fail
cmp fortuneSessionShuffle3D.pixels,0
jne fortune_session_fail
mov eax,bankroll
cmp eax,fortuneLiveBank
jne fortune_live_fail
cmp playerX,FP_1*155/10
jne fortune_live_fail
cmp playerY,11*FP_1
jne fortune_live_fail
cmp playerAngle,256
jne fortune_live_fail
cmp eax,fortuneSessionBank
jne fortune_session_fail
mov eax,playerX
cmp eax,fortuneSessionPlayer
jne fortune_session_fail
ret
fortune_live_fail:
call StopFortuneWorker3D
fortune_session_fail:
invoke ExitProcess,expansionStage
ret
FortuneDialogNativeTests ENDP

View file

@ -278,7 +278,7 @@ fortune_test_arrived:
invoke ExpansionSnapshot,OFFSET fortuneTestDialog
mov mouseClick3D,1
mov mouseX3D,260
mov mouseY3D,226
mov mouseY3D,232
call HandleFortuneDialog3D
cmp engineMode,MODE_WALK
jne fortune_test_fail

View file

@ -1162,9 +1162,8 @@ expansion_leaf_shut:
jne expansion_fail
call FortuneNativeTests
call BarCameraNativeTests
IFDEF CASINO_FORTUNE_AI_LIVE
call FortuneDialogNativeTests
ENDIF
call ExpansionDartsTests
invoke ExitProcess,0
expansion_fail:
invoke ExitProcess,expansionStage
@ -1173,3 +1172,4 @@ ExpansionTestStart ENDP
include tools/tests/fortune_room_test.inc
include tools/tests/bar_camera_test.inc
include tools/tests/fortune_dialog_test.inc
include tools/tests/darts_hall_test.inc

View file

@ -0,0 +1,161 @@
.data
obsTestGame dd 0
obsTestTicks dd 0
obsTestTurns dd 0
obsTestMenu db "build\observer\menu.bmp",0
obsTestPaths dd OFFSET obsTestPoker,OFFSET obsTestBlackjack,OFFSET obsTestDurak
dd OFFSET obsTestTransfer,OFFSET obsTestPyramid,OFFSET obsTestEight
obsTestPoker db "build\observer\poker.bmp",0
obsTestBlackjack db "build\observer\blackjack.bmp",0
obsTestDurak db "build\observer\durak.bmp",0
obsTestTransfer db "build\observer\transfer.bmp",0
obsTestPyramid db "build\observer\pyramid.bmp",0
obsTestEight db "build\observer\eight.bmp",0
.code
ObserverTestStart PROC
call InitPaths3D
mov arrivalConfirmed3D,1
mov bankroll,12345
mov charmCloverUses,17
mov charmDevilUses,18
mov obsActive3D,1
invoke TriggerCharmLossProtection,100
test eax,eax
jnz obs_test_fail
cmp bankroll,12345
jne obs_test_fail
mov obsActive3D,0
call BeginObserver3D
call RenderObserver3D
invoke WriteActionSnapshot3D,OFFSET obsTestMenu
obs_test_game:
mov eax,obsTestGame
mov obsSelection3D,eax
call StartObserver3D
cmp obsActive3D,1
jne obs_test_fail
mov obsTestTicks,0
mov obsTestTurns,0
obs_test_tick:
mov deltaTime3D,50
call ObserverTick3D
cmp obsTestGame,4
jb obs_test_cards
cmp poolState3D,POOL_STRIKE
jne @F
mov eax,poolPlayer3D
bts obsTestTurns,eax
@@:
cmp obsTestTicks,1500
jb obs_test_next_tick
cmp obsTestTurns,3
jne obs_test_fail
jmp obs_test_snapshot
obs_test_cards:
cmp obsRound3D,21
jb obs_test_next_tick
mov eax,obsWins3D[0]
add eax,obsWins3D[4]
add eax,obsWins3D[8]
cmp eax,20
jne obs_test_fail
jmp obs_test_snapshot
obs_test_next_tick:
inc obsTestTicks
cmp obsTestTicks,150000
jb obs_test_tick
jmp obs_test_fail
obs_test_snapshot:
; Advance into a real deal/turn before capturing the watching surface.
mov ecx,40
obs_test_render_advance:
push ecx
mov deltaTime3D,50
call ObserverTick3D
pop ecx
loop obs_test_render_advance
call RenderObserver3D
mov eax,obsTestGame
invoke WriteActionSnapshot3D,obsTestPaths[eax*4]
mov obsPaused3D,1
mov eax,obsRound3D
push eax
mov eax,obsTimer3D
push eax
mov deltaTime3D,50
call ObserverFrame3D
pop eax
cmp eax,obsTimer3D
jne obs_test_fail
pop eax
cmp eax,obsRound3D
jne obs_test_fail
mov pressedKeys3D[9],1
call ObserverFrame3D
cmp obsSpeed3D,2
jne obs_test_fail
mov pressedKeys3D[9],1
call ObserverFrame3D
cmp obsSpeed3D,4
jne obs_test_fail
mov pressedKeys3D[9],1
call ObserverFrame3D
cmp obsSpeed3D,1
jne obs_test_fail
call StopObserver3D
cmp bankroll,12345
jne obs_test_fail
cmp charmCloverUses,17
jne obs_test_fail
cmp charmDevilUses,18
jne obs_test_fail
cmp obsActive3D,0
jne obs_test_fail
inc obsTestGame
cmp obsTestGame,OBS_GAME_COUNT
jb obs_test_game
; The actual selector opens a table, and Escape unwinds watch -> menu -> hall.
mov pressedKeys3D[31h],1
call ObserverFrame3D
cmp obsActive3D,1
jne obs_test_fail
mov pressedKeys3D[1Bh],1
call ObserverFrame3D
cmp engineMode,MODE_OBSERVER
jne obs_test_fail
cmp bankroll,12345
jne obs_test_fail
mov mouseX3D,100
mov mouseY3D,174
mov mouseClick3D,1
call ObserverFrame3D
cmp obsActive3D,1
jne obs_test_fail
cmp poolVariant3D,1
jne obs_test_fail
mov mouseX3D,20
mov mouseY3D,230
mov mouseClick3D,1
call ObserverFrame3D
cmp obsPaused3D,1
jne obs_test_fail
mov mouseX3D,250
mov mouseClick3D,1
call ObserverFrame3D
cmp obsActive3D,0
jne obs_test_fail
mov pressedKeys3D[1Bh],1
call ObserverFrame3D
cmp engineMode,MODE_WALK
jne obs_test_fail
mov obsActive3D,1
call RunDurakTest3D
test eax,eax
jnz obs_test_exit
xor eax,eax
jmp obs_test_exit
obs_test_fail:
mov eax,8401
obs_test_exit:
invoke ExitProcess,eax
ObserverTestStart ENDP

View file

@ -70,27 +70,18 @@ try {
$cp1251 = [Text.Encoding]::GetEncoding(1251)
$asmText = $cp1251.GetString($asmBytes)
foreach ($needle in @(
'settingsCinematicText db "Cinematic -',
'settingsAdaptiveText db "Adaptive -',
'settingsStableText db "Stable -',
'AnimShouldDrawFrame PROC',
'AnimFrameDelay PROC',
'WriteAnimProfileFile PROC',
'SAVE_SIGN_SIZE equ 276',
'SAVE_FILE_SIZE equ 280',
'CalcSaveSignature PROC',
'mov DWORD PTR [saveBuffer+SAVE_SIGN_SIZE], eax',
'cmp charsRead, SAVE_FILE_SIZE',
'includelib ws2_32.lib',
'GameOnlineCasino PROC',
'OnlineRRSessionHost PROC',
'OnlineRRSessionClient PROC'
'includelib ws2_32.lib'
)) {
if ($asmText.IndexOf($needle) -lt 0) {
Fail "Missing source marker: $needle"
}
}
Pass 'Animation profile and save signature markers exist in poker.asm'
Pass 'Save signature and library markers exist in poker.asm'
if ($asmText -match 'MAMBO|mambo|PlayMambo') {
Fail 'MAMBO markers still exist in poker.asm'