[Scummvm-git-logs] scummvm master -> c79c6085136c119d0c2d90bd2cac7f711e2a1723
bluegr
noreply at scummvm.org
Mon Jul 27 01:08:15 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
7e549f9121 NANCY: NANCY12: More work on SewingMachinePuzzle
5dcba1b9b5 NANCY: NANCY10: Fix cursor shown while dragging glyphs in GridMapPuzzle
c79c608513 SCI: Implement the external DLL logic of Hoyle 5 Poker game
Commit: 7e549f9121028c1073108cdcca8e875d9f796dc6
https://github.com/scummvm/scummvm/commit/7e549f9121028c1073108cdcca8e875d9f796dc6
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-27T04:08:06+03:00
Commit Message:
NANCY: NANCY12: More work on SewingMachinePuzzle
The puzzle works correctly now
Changed paths:
engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
engines/nancy/action/puzzle/sewingmachinepuzzle.h
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
index 659fd93badd..5cde462aca9 100644
--- a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
@@ -34,6 +34,16 @@
namespace Nancy {
namespace Action {
+// Size of a single drawn stitch, in pixels.
+static const int kStitchSize = 2;
+
+// Fallback seam x used if the mask scan fails.
+static const int kSeamStartX = 490;
+
+// Scene event flag that switches the needle between its animated and static
+// overlays (true while the cloth is being fed).
+static const int16 kNeedleAnimFlag = 1004;
+
void SewingMachinePuzzle::readData(Common::SeekableReadStream &stream) {
// 87-byte PuzzleBase header blob.
readFilename(stream, _imageName); // blob 0x00
@@ -56,22 +66,11 @@ void SewingMachinePuzzle::classifyZones() {
for (uint i = 0; i < _zones.size(); ++i) {
const ActionZone &z = _zones[i];
switch (z.type) {
- case 0x0b: // the needle line: touching it plays a stitch cue and sets a flag
+ case 0x0b: // seam mask + mistake lines
_collisionZone = i;
break;
- case 0x0c: // bottom trigger: its SpecialEffect carries the win scene + fade
+ case 0x0c: // bottom completion trigger
_triggerZones.push_back(i);
- if (z.specialEffectId >= 1000 && _winScene.sceneID == kNoScene) {
- _winScene.sceneID = z.specialEffectId;
- _winEventFlag = z.val49;
- if (z.hasSpecialEffect) {
- _winHasFade = true;
- _winFadeType = z.seType;
- _winFadeTotalTime = z.seTotalTime;
- _winFadeToBlackTime = z.seFadeToBlackTime;
- _winFadeRect = z.seRect;
- }
- }
break;
case 0x14: // play-area boundary
_boundaryZone = i;
@@ -101,43 +100,130 @@ void SewingMachinePuzzle::playSoundBlock(const RandomSoundBlock &block) {
g_nancy->_sound->loadSound(desc);
g_nancy->_sound->playSound(desc);
+
+ showSubtitle(name);
+}
+
+void SewingMachinePuzzle::showSubtitle(const Common::String &soundName) {
+ // The mistake lines carry no inline caption; look the subtitle up by sound name,
+ // first in the Autotext table, then in the conversation table.
+ Common::String text;
+
+ const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+ if (autotext) {
+ text = autotext->texts.getValOrDefault(soundName, "");
+ }
+
+ if (text.empty()) {
+ const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
+ if (convo) {
+ text = convo->texts.getValOrDefault(soundName, "");
+ }
+ }
+
+ if (!text.empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(text);
+ }
+}
+
+Common::Point SewingMachinePuzzle::needleInStrip() const {
+ // The needle is fixed on screen; map it back into cloth space.
+ return _needleScreen - _offset;
}
void SewingMachinePuzzle::drawCloth() {
- // The cloth is a tall strip; the fixed needle (drawn by the scene) sits over it.
- // The scene shows through wherever the strip does not cover.
+ // Draw the cloth at native size wherever it has been dragged; only the part
+ // within the viewport is blitted, so the scene shows where the cloth is not.
+ Common::Rect src(-_offset.x, -_offset.y, -_offset.x + _drawSurface.w, -_offset.y + _drawSurface.h);
+ src.clip(Common::Rect(_image.w, _image.h));
+
_drawSurface.clear(g_nancy->_graphics->getTransColor());
- _drawSurface.blitFrom(_image, _clothPos);
+ if (!src.isEmpty()) {
+ Common::Point dest(src.left + _offset.x, src.top + _offset.y);
+ _drawSurface.blitFrom(_image, src, dest);
+ }
+
+ // Stitches sewn so far, as a dark dashed thread.
+ uint32 stitchColor = _drawSurface.format.RGBToColor(0, 0, 0);
+ Common::Rect surfaceBounds(_drawSurface.w, _drawSurface.h);
+ for (uint i = 0; i < _stitches.size(); ++i) {
+ int sx = _stitches[i].x + _offset.x;
+ int sy = _stitches[i].y + _offset.y;
+ Common::Rect dot(sx, sy, sx + kStitchSize, sy + kStitchSize);
+ dot.clip(surfaceBounds);
+ if (!dot.isEmpty()) {
+ _drawSurface.fillRect(dot, stitchColor);
+ }
+ }
+
_needsRedraw = true;
}
void SewingMachinePuzzle::feedCloth(const Common::Point &delta) {
- Common::Point newPos(CLIP<int>(_clothPos.x + delta.x, -_maxSteer, _maxSteer),
- CLIP<int>(_clothPos.y + delta.y, -_maxFeed, 0));
+ // Dragging moves the cloth: vertical feeds it, horizontal steers it.
+ Common::Point newOffset(CLIP<int>(_offset.x + delta.x, _minOffsetX, _maxOffsetX),
+ CLIP<int>(_offset.y + delta.y, _minOffsetY, _maxOffsetY));
- int moved = ABS(newPos.x - _clothPos.x) + ABS(newPos.y - _clothPos.y);
- _clothPos = newPos;
+ int moved = ABS(newOffset.x - _offset.x) + ABS(newOffset.y - _offset.y);
+ _offset = newOffset;
if (moved == 0) {
- // A pause resets the stitch stroke, matching the original's hysteresis.
- _strokeDistance = 0.0;
+ // The machine's needle only runs while the cloth is moving.
+ NancySceneState.setEventFlag(kNeedleAnimFlag, g_nancy->_false);
return;
}
- _strokeDistance += moved;
+ // Run the needle animation (the scene swaps in its animated overlay).
+ NancySceneState.setEventFlag(kNeedleAnimFlag, g_nancy->_true);
+
+ // Lay down a stitch each time the cloth advances past the spacing.
+ Common::Point needle = needleInStrip();
+ int spacing = MAX<int>(1, _params[1]);
+ if (_stitches.empty() ||
+ ABS(needle.x - _stitches.back().x) + ABS(needle.y - _stitches.back().y) >= spacing) {
+ _stitches.push_back(needle);
+ }
+
drawCloth();
+ checkSeam();
+
+ // Reaching any bottom trigger finishes the seam.
+ if (!_solved) {
+ Common::Point end = needleInStrip();
+ for (uint i = 0; i < _triggerZones.size(); ++i) {
+ if (_zones[_triggerZones[i]].rect.contains(end)) {
+ _solved = true;
+ _state = kActionTrigger;
+ break;
+ }
+ }
+ }
+}
- // A stitch cue (needle sound) fires each time the cloth advances past the threshold.
- if (_strokeDistance >= _params[1] && _collisionZone >= 0) {
- _strokeDistance = 0.0;
- playSoundBlock(_zones[_collisionZone]._sound);
+void SewingMachinePuzzle::checkSeam() {
+ if (!_hasSeamMask || _collisionZone < 0) {
+ return;
}
- // The seam is finished once the cloth has been fed all the way through.
- if (_maxFeed > 0 && _clothPos.y <= -_maxFeed && !_solved) {
- _solved = true;
- _state = kActionTrigger;
+ Common::Point needle = needleInStrip();
+ int mx = (int)((needle.x - _maskOrigin.x) * _maskScaleX);
+ int my = (int)((needle.y - _maskOrigin.y) * _maskScaleY);
+
+ // The needle strays off the seam when, while inside the collision region, it lands
+ // on the mask's background instead of the marked corridor.
+ bool off = mx >= 0 && my >= 0 && mx < _seamMask.w && my < _seamMask.h &&
+ _seamMask.getPixel(mx, my) == _offSeamColor;
+
+ // Edge-triggered: comment once on leaving the seam, re-armed only once the needle
+ // returns to it.
+ if (off && !_offSeam) {
+ const ActionZone &seam = _zones[_collisionZone];
+ playSoundBlock(seam._sound);
+ NancySceneState.setEventFlag(seam.tailId, seam.tailFlag ? g_nancy->_true : g_nancy->_false);
}
+
+ _offSeam = off;
}
void SewingMachinePuzzle::init() {
@@ -152,39 +238,104 @@ void SewingMachinePuzzle::init() {
g_nancy->_resource->loadImage(_imageName, _image);
_image.setTransparentColor(_drawSurface.getTransparentColor());
- // Feed scrolls the whole strip past the viewport; steer lets the cloth wiggle
- // horizontally to follow the seam under the needle (range is a tunable guess).
- _maxFeed = MAX(0, _image.h - _drawSurface.h);
- _maxSteer = 100;
+ // The needle's fixed sewing point = the bottom-center of the needle overlay's
+ // dest rect (167,0,285,170).
+ _needleScreen = Common::Point((167 + 285) / 2, 170);
+
+ // Clamp so the needle can reach any point on the freely dragged cloth.
+ _maxOffsetX = _needleScreen.x;
+ _minOffsetX = _needleScreen.x - _image.w;
+ _maxOffsetY = _needleScreen.y;
+ _minOffsetY = _needleScreen.y - _image.h;
+
+ // Fill from the top; the seam x under the needle is refined by the mask scan below.
+ _offset.y = CLIP<int>(0, _minOffsetY, _maxOffsetY);
+ _offset.x = CLIP<int>(_needleScreen.x - kSeamStartX, _minOffsetX, _maxOffsetX);
+
+ // The collision zone's overlay is a per-pixel mask of the seam path.
+ if (_collisionZone < 0 || _zones[_collisionZone].ovlName.empty()) {
+ return;
+ }
+
+ g_nancy->_resource->loadImage(Common::Path(_zones[_collisionZone].ovlName), _seamMask);
+
+ // The mask covers only the cloth's seam region = the collision zone's rect, so
+ // cloth pixels map to mask pixels by that origin + scale.
+ Common::Rect region = _zones[_collisionZone].rect;
+ if (region.width() <= 0 || region.height() <= 0) {
+ return;
+ }
+ _maskOrigin = Common::Point(region.left, region.top);
+ _maskScaleX = (double)_seamMask.w / region.width();
+ _maskScaleY = (double)_seamMask.h / region.height();
+
+ // Dark corridor on a light background: sample the background at a corner, then
+ // center the needle on the corridor at its row.
+ _offSeamColor = _seamMask.getPixel(0, 0);
+ int maskRow = CLIP<int>((int)((needleInStrip().y - _maskOrigin.y) * _maskScaleY), 0, _seamMask.h - 1);
+ int first = -1, last = -1;
+ for (int x = 0; x < _seamMask.w; ++x) {
+ if (_seamMask.getPixel(x, maskRow) != _offSeamColor) {
+ if (first < 0) {
+ first = x;
+ }
+ last = x;
+ }
+ }
+
+ if (first >= 0) {
+ int seamClothX = _maskOrigin.x + (int)(((first + last) / 2) / _maskScaleX);
+ _offset.x = CLIP<int>(_needleScreen.x - seamClothX, _minOffsetX, _maxOffsetX);
+ _hasSeamMask = true;
+ } else {
+ warning("SewingMachinePuzzle: seam mask '%s' has no seam on the needle row; off-seam detection disabled",
+ _zones[_collisionZone].ovlName.c_str());
+ }
}
void SewingMachinePuzzle::execute() {
switch (_state) {
case kBegin:
+ classifyZones(); // init() needs the seam zone's mask
init();
registerGraphics();
- classifyZones();
playSoundBlock(_soundBlock); // start the sewing-machine ambience
drawCloth();
_state = kRun;
break;
case kRun:
break;
- case kActionTrigger:
- // Seam finished: raise the win flag, then cross-dissolve to the scene the
- // trigger zone's SpecialEffect points at.
- if (_winEventFlag != -1) {
- NancySceneState.setEventFlag(_winEventFlag, g_nancy->_true);
+ case kActionTrigger: {
+ // Raise a flag for every bottom trigger the needle finished inside: the wide
+ // zone marks the seam attempted, the narrow (centered) zone marks it solved.
+ // They share the win scene, so cross-dissolve to it once.
+ Common::Point end = needleInStrip();
+ const ActionZone *sceneZone = nullptr;
+ for (uint i = 0; i < _triggerZones.size(); ++i) {
+ const ActionZone &tz = _zones[_triggerZones[i]];
+ if (!tz.rect.contains(end)) {
+ continue;
+ }
+ if (tz.tailId != -1) {
+ NancySceneState.setEventFlag(tz.tailId, tz.tailFlag ? g_nancy->_true : g_nancy->_false);
+ }
+ if (!sceneZone) {
+ sceneZone = &tz;
+ }
}
- if (_winScene.sceneID >= 1000 && _winScene.sceneID != kNoScene) {
- if (_winHasFade) {
- NancySceneState.specialEffect(_winFadeType, _winFadeTotalTime, _winFadeToBlackTime, _winFadeRect);
+
+ if (sceneZone && sceneZone->specialEffectId >= 1000) {
+ if (sceneZone->hasSpecialEffect) {
+ NancySceneState.specialEffect(sceneZone->seType, sceneZone->seTotalTime, sceneZone->seFadeToBlackTime, sceneZone->seRect);
}
- NancySceneState.changeScene(_winScene);
+ SceneChangeDescription scene;
+ scene.sceneID = sceneZone->specialEffectId;
+ NancySceneState.changeScene(scene);
}
finishExecution();
break;
}
+ }
}
void SewingMachinePuzzle::handleInput(NancyInput &input) {
@@ -206,7 +357,7 @@ void SewingMachinePuzzle::handleInput(NancyInput &input) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
_dragging = false;
- _strokeDistance = 0.0;
+ NancySceneState.setEventFlag(kNeedleAnimFlag, g_nancy->_false);
}
}
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.h b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
index 3e82d498087..0eb508b12a4 100644
--- a/engines/nancy/action/puzzle/sewingmachinepuzzle.h
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
@@ -30,13 +30,12 @@
namespace Nancy {
namespace Action {
-// Nancy12 sewing-machine puzzle (action record 162): the player feeds a piece of
-// cloth through a sewing machine needle.
-//
-// The cloth is a tall strip that the player drags to feed under the fixed needle;
-// stitch/needle sounds play as it slides, and feeding it all the way through
-// cross-dissolves to the trigger zone's win scene.
-// TODO: the horizontal steer range and the drag-to-feed scale are tunable guesses.
+// Nancy12 sewing-machine puzzle (action record 162): the player drags a tall cloth
+// image under a fixed needle - vertical feeds it, horizontal steers it onto the
+// pre-drawn seam. Straying off the seam plays one of Nancy's mistake lines; reaching
+// the end cross-dissolves to the trigger zone's win scene.
+// TODO: the needle spot comes from the scene's needle-overlay rect, not from this
+// record's own data.
class SewingMachinePuzzle : public RenderActionRecord {
public:
SewingMachinePuzzle() : RenderActionRecord(7) {}
@@ -57,12 +56,22 @@ protected:
void classifyZones();
// Plays one entry of a random-sound block (needle/stitch cues).
void playSoundBlock(const RandomSoundBlock &block);
- // Draws the sewing view (the "BED_Sewing_OVL" overlay).
+ // Shows a played voice line's subtitle, looked up by sound name in the CVTX text
+ // chunks (Autotext, then Convo).
+ void showSubtitle(const Common::String &soundName);
+ // Draws the visible part of the cloth strip plus the stitches sewn so far.
void drawCloth();
- // Advances the sewing by a drag delta, firing stitch/needle cues and marking
- // the puzzle solved once enough cloth has been fed through.
+
+ // Where the needle currently sits within the cloth strip.
+ Common::Point needleInStrip() const;
+ // Advances the sewing by a drag delta, marking the puzzle solved once the whole
+ // seam has been fed through.
void feedCloth(const Common::Point &delta);
+ // Tests the needle against the seam mask; leaving the marked corridor plays a
+ // mistake line and sets the zone's flag (edge-triggered, once per excursion).
+ void checkSeam();
+
// Background image ("BED_Sewing_OVL").
Common::Path _imageName;
@@ -82,27 +91,32 @@ protected:
int _boundaryZone = -1; // type 0x14
Common::Array<uint> _triggerZones; // type 0x0c: completion triggers
- // Win transition from a trigger zone's SpecialEffect: its leading id is the win
- // scene, the effect is the fade played over the change.
- SceneChangeDescription _winScene;
- int16 _winEventFlag = -1;
- bool _winHasFade = false;
- byte _winFadeType = 0;
- uint16 _winFadeTotalTime = 0;
- uint16 _winFadeToBlackTime = 0;
- Common::Rect _winFadeRect;
-
- // Runtime state. The cloth is a tall strip drawn at (_clothPos); dragging scrolls
- // it under the fixed needle (feed = vertical, steer = horizontal).
- Common::Point _clothPos; // top-left of the cloth on screen
- int _maxFeed = 0; // vertical scroll range (image height - viewport)
- int _maxSteer = 0; // horizontal steer range each way
- double _strokeDistance = 0.0; // movement accumulated toward the next stitch
+ // Runtime state. The cloth is drawn 1:1 at _offset and dragged freely under the
+ // fixed needle (vertical = feed, horizontal = steer); the viewport clips it.
+ Common::Point _offset; // cloth's top-left on screen
+ int _minOffsetX = 0;
+ int _maxOffsetX = 0;
+ int _minOffsetY = 0;
+ int _maxOffsetY = 0;
+ Common::Point _needleScreen; // needle's fixed spot on screen
bool _dragging = false;
Common::Point _lastDragPos;
bool _solved = false;
+ // Stitches sewn so far, in cloth-image space, drawn as a dark dashed thread.
+ Common::Array<Common::Point> _stitches;
+
+ // The seam mask covers only the cloth's seam region (the collision zone's rect),
+ // so cloth pixels map to mask pixels by that origin + scale, not the whole cloth.
+ uint32 _offSeamColor = 0; // mask background color (off the seam)
+ Common::Point _maskOrigin; // cloth-space top-left of the seam mask
+ double _maskScaleX = 1.0; // seam-mask pixels per cloth pixel (x)
+ double _maskScaleY = 1.0; // seam-mask pixels per cloth pixel (y)
+ bool _hasSeamMask = false;
+ bool _offSeam = false; // the needle was off the seam last check
+
Graphics::ManagedSurface _image;
+ Graphics::ManagedSurface _seamMask;
};
} // End of namespace Action
Commit: 5dcba1b9b5b2626239ff23fe40913c6a602aa111
https://github.com/scummvm/scummvm/commit/5dcba1b9b5b2626239ff23fe40913c6a602aa111
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-27T04:08:07+03:00
Commit Message:
NANCY: NANCY10: Fix cursor shown while dragging glyphs in GridMapPuzzle
Fix #17014
Changed paths:
engines/nancy/action/puzzle/gridmappuzzle.cpp
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index de5eb93673e..3b89655466d 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -231,6 +231,8 @@ void GridMapPuzzle::execute() {
break;
case kActionTrigger:
+ // Restore the cursor in case the player left while holding a glyph.
+ g_nancy->_cursor->showCursor(true);
g_nancy->_sound->stopSound(_pickupSound);
g_nancy->_sound->stopSound(_placeSound);
g_nancy->_sound->stopSound(_winSound);
@@ -360,10 +362,13 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
if (!hitMap)
hitItems = hitTestItems(mouseVP, iRow, iCol);
- // Both grids (map and items) show the grab-hand cursor across their whole
- // area; picking something up doesn't change it. The exit hotspot uses the
- // puzzle-exit cursor, and everything outside the grids (the letter strips,
- // the gap between the grids) keeps the idle eyeglass.
+ // A held glyph is drawn following the cursor, so hide the hardware cursor
+ // while carrying one â just the glyph shows, no hand. When empty-handed,
+ // both grids show the grab-hand, the exit hotspot uses the puzzle-exit
+ // cursor, and everything outside the grids (the letter strips, the gap
+ // between the grids) keeps the idle eyeglass.
+ g_nancy->_cursor->showCursor(_heldItem == -1);
+
if (!hitMap && !hitItems) {
if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
@@ -375,7 +380,8 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
return;
}
- g_nancy->_cursor->setCursorType(CursorManager::kDropHand);
+ if (_heldItem == -1)
+ g_nancy->_cursor->setCursorType(CursorManager::kDropHand);
if (!(input.input & NancyInput::kLeftMouseButtonUp))
return;
Commit: c79c6085136c119d0c2d90bd2cac7f711e2a1723
https://github.com/scummvm/scummvm/commit/c79c6085136c119d0c2d90bd2cac7f711e2a1723
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-27T04:08:07+03:00
Commit Message:
SCI: Implement the external DLL logic of Hoyle 5 Poker game
The poker logic has been implemented in an external DLL (PENGIN16.DLL),
which is loaded via the WinDLL kernel call. That DLL implements four
different handlers, used for betting, determining winners, discarding
cards and classify the current player's hand.
Assisted-by: Claude Code:claude-opus-4.8
Changed paths:
engines/sci/engine/hoyle5poker.cpp
engines/sci/engine/kmisc.cpp
diff --git a/engines/sci/engine/hoyle5poker.cpp b/engines/sci/engine/hoyle5poker.cpp
index 03ea7d546a9..b7bb942e05d 100644
--- a/engines/sci/engine/hoyle5poker.cpp
+++ b/engines/sci/engine/hoyle5poker.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/algorithm.h"
#include "sci/engine/features.h"
#include "sci/engine/hoyle5poker.h"
#include "sci/engine/kernel.h"
@@ -35,6 +36,16 @@ namespace Sci {
// The logic for the poker game in Hoyle Classic Games (Hoyle 5) is hardcoded
// in PENGIN16.DLL, which is then loaded and invoked via the kWinDLL kernel call.
// Note that the first player is the left one.
+//
+// The DLL is passed a single array of 16-bit words. The kernel entry point
+// (SCIDLLENTRY) dispatches on the operation code and calls one of four handlers:
+// op 1 -> FUN_1000_2016 (betting AI)
+// op 2 -> FUN_1000_17e4 (determine winner(s))
+// op 3 -> FUN_1000_1dc0 (discard AI)
+// op 4 -> FUN_1000_632e (classify the current player's hand)
+// Internally the DLL builds a linked list of cards for each hand and evaluates
+// it (FUN_1000_44c2). There are no wild cards, so the highest possible hand is a
+// royal flush, which the DLL reports with its own bit (0x100).
enum Hoyle5PokerSuits {
kSuitSpades = 0,
@@ -44,10 +55,10 @@ enum Hoyle5PokerSuits {
};
enum Hoyle5Operations {
- kCheckPlayerAction = 1, // localproc_0df8
- kCheckWinner = 2, // localproc_3020
- kCheckDiscard = 3, // PokerHand::think
- kCheckHand = 4 // PokerHand::whatAmI
+ kCheckPlayerAction = 1, // FUN_1000_2016
+ kCheckWinner = 2, // FUN_1000_17e4
+ kCheckDiscard = 3, // FUN_1000_1dc0 (PokerHand::think)
+ kCheckHand = 4 // FUN_1000_632e (PokerHand::whatAmI)
};
enum Hoyle5PlayerActions {
@@ -62,8 +73,10 @@ enum Hoyle5DiscardActions {
kDiscardActionDiscard = 1
};
+// Hand types, as returned by PokerHand::whatAmI (FUN_1000_44c2). Higher values
+// are stronger hands, which is what lets the winner check compare them directly.
enum Hoyle5HandType {
- kHandTypeFiveOfAKind = 1 << 8, // 256, five of a kind
+ kHandTypeRoyalFlush = 1 << 8, // 256, ace-high straight flush
kHandTypeStraightFlush = 1 << 7, // 128, straight flush
kHandTypeFourOfAKind = 1 << 6, // 64, four of a kind
kHandTypeFullHouse = 1 << 5, // 32, full house
@@ -92,7 +105,7 @@ enum Hoyle5PokerData {
kTotalBetPlayer2 = 13,
kTotalBetPlayer3 = 14,
kTotalBetPlayer4 = 15,
- // 16: related to the current bet
+ kAmountToCall = 16, // chips the current player must add to stay in
kCurrentPlayer = 17, // hand number
kCurrentStage = 18, // Stage 1: Card changes, 2: Betting
kCard0 = 19,
@@ -109,17 +122,21 @@ enum Hoyle5PokerData {
// 29 - 38: next clockwise player's cards (number + suit)
// 39 - 48: next clockwise player's cards (number + suit)
// 49 - 58: next clockwise player's cards (number + suit)
- kUnkVar = 59, // set by localproc_0df8 to global 906
+ kUnkVar = 59, // set by FUN_1000_0df8 to global 906
// ---- Return values - start ---------------------------
- kPlayerAction = 60, // flag, checked by localproc_0df8
- kWhatAmIResult = 61, // bitmask, 0 - 128, checked by PokerHand::whatAmI. Determines what kind of card each player has
- kWinningPlayers = 62, // bitmask, winning players (0000 - 1111 binary), checked by localproc_3020
+ kPlayerAction = 60, // flag, checked by FUN_1000_2016
+ kWhatAmIResult = 61, // hand type bitmask (see Hoyle5HandType)
+ kWinningPlayers = 62, // bitmask, winning players (0000 - 1111 binary)
kDiscardCard0 = 63, // flag, checked by PokerHand::think
kDiscardCard1 = 64, // flag, checked by PokerHand::think
kDiscardCard2 = 65, // flag, checked by PokerHand::think
kDiscardCard3 = 66, // flag, checked by PokerHand::think
kDiscardCard4 = 67, // flag, checked by PokerHand::think
// ---- Return values - end -----------------------------
+ kConfidencePlayer1 = 68, // 0x8c array; clamped 0 - 3 by the DLL entry point
+ // 69 - 71: confidence for the other players
+ kAggressionPlayer1 = 72, // 0x94 array; adjusted by chip stack in the entry point
+ // 73 - 75: aggression for the other players
// 77 is a random number (0 - 32767)
kLastRaise1 = 78,
kLastRaise2 = 79,
@@ -136,6 +153,27 @@ enum Hoyle5PokerData {
// 90 is a number
};
+// A single card, with its rank normalized so that aces are always high (14).
+struct PokerCard {
+ int rank; // 2 - 14
+ int suit; // see Hoyle5PokerSuits
+};
+
+// The classified result of a five card hand. The tie-break ranks list the
+// significant ranks in descending priority order, which mirrors how the DLL
+// reorders its card list before comparing two hands of the same type:
+// - four of a kind: quad rank, then kicker
+// - full house: trip rank, then pair rank
+// - three of a kind:trip rank, then the two kickers (descending)
+// - two pairs: high pair, low pair, then kicker
+// - one pair: pair rank, then the three kickers (descending)
+// - straights: the straight's high card (a wheel counts as five high)
+// - flush/highcard: all five ranks (descending)
+struct PokerHandInfo {
+ int handType;
+ int tieBreak[5];
+};
+
#ifdef DEBUG_POKER_LOGIC
Common::String getCardDescription(int16 card, int16 suit) {
Common::String result;
@@ -148,7 +186,7 @@ Common::String getCardDescription(int16 card, int16 suit) {
result = "Queen";
else if (card == 13)
result = "King";
- else if (card == 14)
+ else if (card == 14 || card == 1)
result = "Ace";
else
result = "Unknown";
@@ -210,270 +248,457 @@ void debugInputData(SciArray* data) {
#endif
-int getCardValue(int card) {
- return card == 1 ? 14 : card; // aces are the highest valued cards
+// Aces are stored as either 1 or 14 depending on the caller, but are always the
+// highest valued card in the DLL's evaluator.
+static int getCardValue(int card) {
+ return card == 1 ? 14 : card;
}
-int getCardTotal(SciArray *data, int player) {
- int result = 0;
+// Reads a player's five cards from the shared array into a normalized form.
+static void readPlayerCards(SciArray *data, int player, PokerCard cards[5]) {
+ for (int i = 0; i < 5; i++) {
+ cards[i].rank = getCardValue(data->getAsInt16(kCard0 + 10 * player + i * 2));
+ cards[i].suit = data->getAsInt16(kSuit0 + 10 * player + i * 2);
+ }
+}
- int cards[5] = {
- getCardValue(data->getAsInt16(kCard0 + 10 * player)),
- getCardValue(data->getAsInt16(kCard1 + 10 * player)),
- getCardValue(data->getAsInt16(kCard2 + 10 * player)),
- getCardValue(data->getAsInt16(kCard3 + 10 * player)),
- getCardValue(data->getAsInt16(kCard4 + 10 * player)),
- };
+static bool pokerCardRankGreater(const PokerCard &a, const PokerCard &b) {
+ return a.rank > b.rank;
+}
- Common::sort(cards, cards + 5, Common::Less<int>());
+// A distinct rank together with how many cards share it, used to build the
+// tie-break ordering (sorted by count, then rank, both descending).
+struct RankGroup {
+ int rank;
+ int count;
+};
- int sameRank = 0;
- int sameSuit = 0;
- int orderedCards = 0;
+static bool rankGroupGreater(const RankGroup &a, const RankGroup &b) {
+ if (a.count != b.count)
+ return a.count > b.count;
+ return a.rank > b.rank;
+}
+
+// Classifies a five card hand, mirroring PokerHand::whatAmI (FUN_1000_44c2).
+static PokerHandInfo classifyHand(const PokerCard cardsIn[5]) {
+ PokerHandInfo info;
+ info.handType = kHandTypeHighCard;
+ for (int i = 0; i < 5; i++)
+ info.tieBreak[i] = 0;
+
+ PokerCard cards[5];
+ for (int i = 0; i < 5; i++)
+ cards[i] = cardsIn[i];
+
+ // Sort the cards in descending rank order.
+ Common::sort(cards, cards + 5, pokerCardRankGreater);
+
+ // A flush is five cards of the same suit.
+ bool isFlush = true;
+ for (int i = 1; i < 5; i++) {
+ if (cards[i].suit != cards[0].suit)
+ isFlush = false;
+ }
+ // A straight requires five distinct, consecutive ranks. The DLL also treats
+ // A-2-3-4-5 (the "wheel") as a five-high straight.
+ bool distinct = true;
for (int i = 0; i < 4; i++) {
- if (cards[i] == cards[i + 1]) {
- if (sameRank == 0) {
- result += cards[i] + cards[i + 1];
- sameRank += 2;
- } else {
- result += cards[i + 1];
- sameRank++;
- }
- }
- if (cards[i] == cards[i + 1] - 1)
- orderedCards == 0 ? orderedCards += 2 : orderedCards++;
+ if (cards[i].rank == cards[i + 1].rank)
+ distinct = false;
}
- bool isFullHouse =
- (cards[0] == cards[1] && cards[1] == cards[2] && cards[3] == cards[4]) ||
- (cards[0] == cards[1] && cards[2] == cards[3] && cards[3] == cards[4]);
+ bool isStraight = false;
+ int straightHigh = 0;
+ if (distinct) {
+ if (cards[0].rank - cards[4].rank == 4) {
+ isStraight = true;
+ straightHigh = cards[0].rank;
+ } else if (cards[0].rank == 14 && cards[1].rank == 5 &&
+ cards[2].rank == 4 && cards[3].rank == 3 && cards[4].rank == 2) {
+ isStraight = true;
+ straightHigh = 5;
+ }
+ }
- if (isFullHouse || sameSuit == 5 || orderedCards == 5) {
- result = 0;
+ // Group the cards by rank, sorted by group size then rank (both descending).
+ RankGroup groups[5];
+ int groupCount = 0;
+ for (int i = 0; i < 5; i++) {
+ int j;
+ for (j = 0; j < groupCount; j++) {
+ if (groups[j].rank == cards[i].rank) {
+ groups[j].count++;
+ break;
+ }
+ }
+ if (j == groupCount) {
+ groups[groupCount].rank = cards[i].rank;
+ groups[groupCount].count = 1;
+ groupCount++;
+ }
+ }
+ Common::sort(groups, groups + groupCount, rankGroupGreater);
+
+ int largestGroup = groups[0].count;
+ bool hasFullHouse = (groupCount == 2 && largestGroup == 3);
+
+ if (isStraight && isFlush) {
+ // An ace-high straight flush is a royal flush.
+ info.handType = (straightHigh == 14) ? kHandTypeRoyalFlush : kHandTypeStraightFlush;
+ } else if (largestGroup == 4) {
+ info.handType = kHandTypeFourOfAKind;
+ } else if (hasFullHouse) {
+ info.handType = kHandTypeFullHouse;
+ } else if (isFlush) {
+ info.handType = kHandTypeFlush;
+ } else if (isStraight) {
+ info.handType = kHandTypeStraight;
+ } else if (largestGroup == 3) {
+ info.handType = kHandTypeThreeOfAKind;
+ } else if (groupCount == 3 && largestGroup == 2) {
+ info.handType = kHandTypeTwoPairs;
+ } else if (largestGroup == 2) {
+ info.handType = kHandTypeOnePair;
+ } else {
+ info.handType = kHandTypeHighCard;
+ }
+ // Fill in the tie-break ranks.
+ if (isStraight && !hasFullHouse && largestGroup < 3) {
+ // Straights (and straight flushes) only differ by their high card.
+ int high = straightHigh;
for (int i = 0; i < 5; i++)
- result += cards[i];
+ info.tieBreak[i] = high - i;
+ } else {
+ for (int i = 0; i < groupCount; i++)
+ info.tieBreak[i] = groups[i].rank;
}
- return result;
+ return info;
}
-// Checks a player's hand, and returns its type using a bitmask
-int checkHand(SciArray *data, int player = 0) {
- int cards[5] = {
- data->getAsInt16(kCard0 + 10 * player),
- data->getAsInt16(kCard1 + 10 * player),
- data->getAsInt16(kCard2 + 10 * player),
- data->getAsInt16(kCard3 + 10 * player),
- data->getAsInt16(kCard4 + 10 * player),
- };
-
- int suits[5] = {
- data->getAsInt16(kSuit0 + 10 * player),
- data->getAsInt16(kSuit1 + 10 * player),
- data->getAsInt16(kSuit2 + 10 * player),
- data->getAsInt16(kSuit3 + 10 * player),
- data->getAsInt16(kSuit4 + 10 * player),
- };
-
- Common::sort(cards, cards + 5, Common::Less<int>());
-
- int lastCard = -1;
- int pairs = 0;
- int sameRank = 0;
- int sameSuit = 0;
- int orderedCards = 0;
+// Compares two classified hands. Returns 1 if a is stronger, -1 if b is
+// stronger and 0 if they tie (a split pot). This is the showdown comparison
+// used by the DLL (FUN_1000_530a / FUN_1000_5c56).
+static int compareHands(const PokerHandInfo &a, const PokerHandInfo &b) {
+ if (a.handType != b.handType)
+ return a.handType > b.handType ? 1 : -1;
- for (int i = 0; i < 4; i++) {
- if (cards[i] == cards[i + 1] && cards[i] != lastCard)
- pairs++;
- if (cards[i] == cards[i + 1])
- sameRank == 0 ? sameRank += 2 : sameRank++;
- if (suits[i] == suits[i + 1])
- sameSuit == 0 ? sameSuit += 2 : sameSuit++;
- if (cards[i] == cards[i + 1] - 1)
- orderedCards == 0 ? orderedCards += 2 : orderedCards++;
-
- lastCard = cards[i];
+ for (int i = 0; i < 5; i++) {
+ if (a.tieBreak[i] != b.tieBreak[i])
+ return a.tieBreak[i] > b.tieBreak[i] ? 1 : -1;
}
- bool isFullHouse =
- (cards[0] == cards[1] && cards[1] == cards[2] && cards[3] == cards[4]) ||
- (cards[0] == cards[1] && cards[2] == cards[3] && cards[3] == cards[4]);
-
- if (pairs == 1 && sameRank == 2)
- return kHandTypeOnePair;
- else if (pairs == 2 && !isFullHouse)
- return kHandTypeTwoPairs;
- else if (sameRank == 3 && !isFullHouse)
- return kHandTypeThreeOfAKind;
- else if (orderedCards == 5 && sameSuit < 5)
- return kHandTypeStraight;
- else if (orderedCards < 5 && sameSuit == 5)
- return kHandTypeFlush;
- else if (isFullHouse)
- return kHandTypeFullHouse;
- else if (sameRank == 4)
- return kHandTypeFourOfAKind;
- else if (orderedCards == 5 && sameSuit == 5)
- return kHandTypeStraightFlush;
- else if (sameRank == 5)
- return kHandTypeFiveOfAKind;
-
- return kHandTypeHighCard;
+ return 0;
}
-struct Hand {
- int player;
- int handTotal;
-
- Hand(int p, int h) : player(p), handTotal(h) {}
-};
+// Classifies a player's hand and returns its type bitmask.
+static int checkHand(SciArray *data, int player = 0) {
+ PokerCard cards[5];
+ readPlayerCards(data, player, cards);
+ return classifyHand(cards).handType;
+}
-struct WinningHand : public Common::BinaryFunction<Hand, Hand, bool> {
- bool operator()(const Hand &x, const Hand &y) const { return x.handTotal > y.handTotal; }
-};
+// Determines the winning player(s) at showdown, mirroring FUN_1000_17e4.
+// Returns a bitmask of the winners (more than one bit is set on a split pot) and
+// stores the winning hand type in the shared array. Folded players (whose status
+// is -1) are ignored.
+static int getWinners(SciArray *data) {
+ int winners = 0;
+ PokerHandInfo best;
+ best.handType = -1;
-int getWinner(SciArray *data) {
- Hand playerHands[4] = {
- Hand(0, checkHand(data, 0)),
- Hand(1, checkHand(data, 1)),
- Hand(2, checkHand(data, 2)),
- Hand(3, checkHand(data, 3))
- };
+ for (int player = 0; player < 4; player++) {
+ if (data->getAsInt16(kStatusPlayer1 + player) == -1)
+ continue; // folded
+
+ PokerCard cards[5];
+ readPlayerCards(data, player, cards);
+ PokerHandInfo info = classifyHand(cards);
+
+ if (winners == 0) {
+ best = info;
+ winners = 1 << player;
+ } else {
+ int cmp = compareHands(info, best);
+ if (cmp > 0) {
+ best = info;
+ winners = 1 << player;
+ } else if (cmp == 0) {
+ winners |= 1 << player;
+ }
+ }
+ }
- Common::sort(playerHands, playerHands + 4, WinningHand());
+ if (best.handType >= 0)
+ data->setFromInt16(kWhatAmIResult, best.handType);
- if (playerHands[0].handTotal > playerHands[1].handTotal)
- return playerHands[0].player;
- else
- return getCardTotal(data, 0) > getCardTotal(data, 1) ? playerHands[0].player : playerHands[1].player;
+ return winners;
}
-int16 findMostFrequentCard(int *cards, int16 ignoreCard = -1) {
- int16 mostFrequentCard = 0;
- int16 maxCount = 0;
+// Returns the current player's confidence value (the 0x8c array), which the DLL
+// entry point clamps to the 0 - 3 range. It gates whether the AI chases a
+// gutshot straight draw.
+static int getConfidence(SciArray *data, int player) {
+ int value = data->getAsInt16(kConfidencePlayer1 + player);
+ return CLIP(value, 0, 3);
+}
- for (int16 i = 0; i <= 4; ++i) {
- int16 count = 0;
- for (int j = 0; j <= 4; ++j) {
- if (cards[i] == cards[j])
- count++;
+// Detects a four-card flush draw. If four of the five cards share a suit, the
+// index of the odd (off-suit) card is returned so it can be discarded, mirroring
+// FUN_1000_5062. Returns -1 if there is no flush draw.
+static int findFlushDrawDiscard(const PokerCard cards[5]) {
+ int suitCounts[4] = { 0, 0, 0, 0 };
+ for (int i = 0; i < 5; i++)
+ suitCounts[cards[i].suit]++;
+
+ for (int suit = 0; suit < 4; suit++) {
+ if (suitCounts[suit] == 4) {
+ for (int i = 0; i < 5; i++) {
+ if (cards[i].suit != suit)
+ return i;
+ }
}
+ }
- if (count > maxCount && cards[i] != ignoreCard) {
- maxCount = count;
- mostFrequentCard = cards[i];
+ return -1;
+}
+
+// Detects a four-card straight draw, mirroring FUN_1000_511c. If four of the
+// cards can be completed into a straight, the index of the odd card is returned
+// (so it can be discarded) and openEnded is set to distinguish an open-ended
+// draw (four consecutive ranks) from a gutshot (an inside gap). Returns -1 if
+// there is no straight draw. Aces are considered both high and low.
+static int findStraightDrawDiscard(const PokerCard cards[5], bool &openEnded) {
+ openEnded = false;
+
+ // Try dropping each card and testing whether the remaining four form a
+ // straight draw, preferring an open-ended draw over a gutshot.
+ int gutshotDiscard = -1;
+
+ for (int drop = 0; drop < 5; drop++) {
+ for (int aceLow = 0; aceLow < 2; aceLow++) {
+ int ranks[4];
+ int n = 0;
+ for (int i = 0; i < 5; i++) {
+ if (i == drop)
+ continue;
+ int rank = cards[i].rank;
+ if (aceLow && rank == 14)
+ rank = 1;
+ ranks[n++] = rank;
+ }
+
+ // The four ranks must be distinct.
+ bool ok = true;
+ int lowest = ranks[0], highest = ranks[0];
+ for (int i = 0; i < 4; i++) {
+ for (int j = i + 1; j < 4; j++) {
+ if (ranks[i] == ranks[j])
+ ok = false;
+ }
+ lowest = MIN(lowest, ranks[i]);
+ highest = MAX(highest, ranks[i]);
+ }
+ if (!ok)
+ continue;
+
+ int span = highest - lowest;
+ if (span == 3) {
+ // Four consecutive ranks: an open-ended draw.
+ openEnded = true;
+ return drop;
+ }
+ if (span == 4 && gutshotDiscard == -1) {
+ // Four ranks with a single inside gap: a gutshot.
+ gutshotDiscard = drop;
+ }
}
}
- return mostFrequentCard;
+ return gutshotDiscard;
}
-void handleDiscard(SciArray *data) {
- int16 player = data->getAsInt16(kCurrentPlayer);
-
- int cards[5] = {
- data->getAsInt16(kCard0 + 10 * player),
- data->getAsInt16(kCard1 + 10 * player),
- data->getAsInt16(kCard2 + 10 * player),
- data->getAsInt16(kCard3 + 10 * player),
- data->getAsInt16(kCard4 + 10 * player),
- };
-
- int hand = checkHand(data, player);
- int16 cardToKeep = findMostFrequentCard(cards);
- int16 cardToKeep2 = -1;
- if (hand != kHandTypeFiveOfAKind) {
- cardToKeep2 = findMostFrequentCard(cards, cardToKeep);
+// Implements the discard AI (op 3, FUN_1000_1dc0 / PokerHand::think). It decides
+// which of the current player's cards to keep and which to discard, then writes
+// a keep/discard flag for each card into the shared array.
+static void handleDiscard(SciArray *data) {
+ int player = data->getAsInt16(kCurrentPlayer);
+
+ // The acting player's cards are always in the first block (indices 19-28);
+ // kCurrentPlayer only indexes the per-player state arrays (mood etc.).
+ PokerCard cards[5];
+ readPlayerCards(data, 0, cards);
+
+ PokerHandInfo info = classifyHand(cards);
+
+ // Count how many cards share each rank, so complete groups can be kept.
+ int rankCounts[5];
+ for (int i = 0; i < 5; i++) {
+ rankCounts[i] = 0;
+ for (int j = 0; j < 5; j++) {
+ if (cards[j].rank == cards[i].rank)
+ rankCounts[i]++;
+ }
}
- data->setFromInt16(kDiscardCard0, kDiscardActionKeep);
- data->setFromInt16(kDiscardCard1, kDiscardActionKeep);
- data->setFromInt16(kDiscardCard2, kDiscardActionKeep);
- data->setFromInt16(kDiscardCard3, kDiscardActionKeep);
- data->setFromInt16(kDiscardCard4, kDiscardActionKeep);
+ bool discard[5] = { false, false, false, false, false };
- switch (hand) {
- case kHandTypeFiveOfAKind:
+ switch (info.handType) {
+ case kHandTypeRoyalFlush:
case kHandTypeStraightFlush:
case kHandTypeFullHouse:
case kHandTypeFlush:
case kHandTypeStraight:
- // Nothing is discarded
+ // A made hand of five cards: keep everything.
break;
- case kHandTypeThreeOfAKind:
+
case kHandTypeFourOfAKind:
- case kHandTypeOnePair:
+ case kHandTypeThreeOfAKind:
case kHandTypeTwoPairs:
- // Discard the odd ones out. We don't have a full house case in this branch
- for (int i = 0; i <= 4; ++i) {
- if (cards[i] == cardToKeep && hand != kHandTypeTwoPairs)
- data->setFromInt16(kDiscardCard0 + i, kDiscardActionKeep);
- else if ((cards[i] == cardToKeep || cards[i] == cardToKeep2) && hand == kHandTypeTwoPairs)
- data->setFromInt16(kDiscardCard0 + i, kDiscardActionKeep);
- else
- data->setFromInt16(kDiscardCard0 + i, kDiscardActionDiscard);
+ case kHandTypeOnePair:
+ // Keep the paired cards, discard the odd ones out. This keeps four
+ // cards for quads/two pairs, three for trips and two for a pair, which
+ // matches the keep counts the DLL derives in FUN_1000_56bc.
+ for (int i = 0; i < 5; i++) {
+ if (rankCounts[i] == 1)
+ discard[i] = true;
+ }
+ break;
+
+ case kHandTypeHighCard: {
+ int flushDrawDiscard = findFlushDrawDiscard(cards);
+ if (flushDrawDiscard >= 0) {
+ // Chase the flush by discarding the single off-suit card.
+ discard[flushDrawDiscard] = true;
+ break;
+ }
+
+ bool openEnded = false;
+ int straightDrawDiscard = findStraightDrawDiscard(cards, openEnded);
+ // An open-ended draw is always chased; a gutshot only when the player is
+ // confident (its clamped confidence value has reached the maximum).
+ if (straightDrawDiscard >= 0 && (openEnded || getConfidence(data, player) >= 3)) {
+ discard[straightDrawDiscard] = true;
+ break;
+ }
+
+ // No draw worth chasing: keep the highest one or two cards. The DLL
+ // keeps a single card when the top card is a king or ace, and otherwise
+ // occasionally does so as well.
+ int highestRank = 0;
+ for (int i = 0; i < 5; i++)
+ highestRank = MAX(highestRank, cards[i].rank);
+
+ bool keepOne = (highestRank >= 13) || (g_sci->getRNG().getRandomNumber(32767) > 0x6000);
+ int keepCount = keepOne ? 1 : 2;
+
+ // Discard everything but the highest keepCount cards.
+ for (int i = 0; i < 5; i++)
+ discard[i] = true;
+ for (int kept = 0; kept < keepCount; kept++) {
+ int bestIndex = -1;
+ for (int i = 0; i < 5; i++) {
+ if (discard[i] && (bestIndex == -1 || cards[i].rank > cards[bestIndex].rank))
+ bestIndex = i;
+ }
+ if (bestIndex >= 0)
+ discard[bestIndex] = false;
}
break;
- case kHandTypeHighCard:
- // Everything is discarded
- data->setFromInt16(kDiscardCard0, kDiscardActionDiscard);
- data->setFromInt16(kDiscardCard1, kDiscardActionDiscard);
- data->setFromInt16(kDiscardCard2, kDiscardActionDiscard);
- data->setFromInt16(kDiscardCard3, kDiscardActionDiscard);
- data->setFromInt16(kDiscardCard4, kDiscardActionDiscard);
+ }
+
+ default:
break;
}
+
+ for (int i = 0; i < 5; i++)
+ data->setFromInt16(kDiscardCard0 + i, discard[i] ? kDiscardActionDiscard : kDiscardActionKeep);
}
-void handleRaiseOrCall(SciArray *data) {
- // Raise if the player has money, call otherwise
- int16 player = data->getAsInt16(kCurrentPlayer);
- int16 bet = data->getAsInt16(kCurrentBet);
- int16 playerBet = data->getAsInt16(kTotalBetPlayer1 + player);
- int16 chips = data->getAsInt16(kTotalChipsPlayer1 + player);
-
- if (playerBet < bet && chips > bet)
- data->setFromInt16(kPlayerAction, kPlayerActionRaise);
- else
- data->setFromInt16(kPlayerAction, kPlayerActionCall);
+// Returns a rough win expectation (0 - 100) for a classified hand, used by the
+// simplified betting AI below.
+static int getHandStrength(const PokerHandInfo &info) {
+ switch (info.handType) {
+ case kHandTypeRoyalFlush: return 100;
+ case kHandTypeStraightFlush: return 99;
+ case kHandTypeFourOfAKind: return 96;
+ case kHandTypeFullHouse: return 92;
+ case kHandTypeFlush: return 82;
+ case kHandTypeStraight: return 72;
+ case kHandTypeThreeOfAKind: return 62;
+ case kHandTypeTwoPairs: return 48;
+ case kHandTypeOnePair:
+ // A high pair is worth noticeably more than a low one.
+ return 26 + (info.tieBreak[0] - 2);
+ default:
+ // High card: scale by the highest card held.
+ return info.tieBreak[0] - 2;
+ }
}
-void handlePlayerAction(SciArray *data) {
- // TODO: This implementation is somewhat better than completely
- // random actions, but it's still severely lacking
- warning("The Poker player action logic has not been implemented yet");
+// Implements the betting AI (op 1, FUN_1000_2016). The original DLL runs a
+// recursive expected-value search over the possible betting sequences
+// (FUN_1000_269c). AI folds when it cannot afford to continue with a weak
+// hand, and otherwise weighs the hand's strength against the pot odds,
+// adjusted by the player's aggression, to decide whether to check, call or
+// raise.
+static void handlePlayerAction(SciArray *data) {
+ int player = data->getAsInt16(kCurrentPlayer);
+ int chips = data->getAsInt16(kTotalChipsPlayer1 + player);
+ int amountToCall = data->getAsInt16(kAmountToCall);
+ int pot = data->getAsInt16(kCurrentPot);
+
+ // As with the discard logic, the acting player's cards are in the first
+ // block; kCurrentPlayer indexes the chip/bet/aggression arrays.
+ PokerCard cards[5];
+ readPlayerCards(data, 0, cards);
+ PokerHandInfo info = classifyHand(cards);
+ int strength = getHandStrength(info);
+
+ // Confidence and aggression are derived by the DLL entry point from the
+ // player's chip stack (a short stack plays cautiously, a big stack pushes).
+ int aggression = data->getAsInt16(kAggressionPlayer1 + player);
+ aggression = CLIP(aggression, 0, 3);
+
+ // If the player cannot cover the call, only a strong hand keeps going.
+ if (amountToCall > 0 && chips < amountToCall) {
+ data->setFromInt16(kPlayerAction, strength >= 70 ? kPlayerActionCall : kPlayerActionFold);
+ return;
+ }
- int16 player = data->getAsInt16(kCurrentPlayer);
- int hand = checkHand(data, player);
- bool shouldBluff = g_sci->getRNG().getRandomBit();
+ // Pot odds: the fraction of the (post-call) pot the call would cost.
+ int potOdds = 0;
+ if (amountToCall > 0 && pot + amountToCall > 0)
+ potOdds = (amountToCall * 100) / (pot + amountToCall);
+
+ // A higher aggression raises the effective strength (and adds occasional
+ // bluffs); the threshold to raise scales down with it.
+ int effectiveStrength = strength + aggression * 6;
+ int raiseThreshold = 78 - aggression * 6;
+
+ if (amountToCall == 0) {
+ // Betting is free: raise with a good hand (or a bluff), otherwise check.
+ bool bluff = (int)g_sci->getRNG().getRandomNumber(99) < aggression * 5;
+ if (effectiveStrength >= raiseThreshold || bluff)
+ data->setFromInt16(kPlayerAction, kPlayerActionRaise);
+ else
+ data->setFromInt16(kPlayerAction, kPlayerActionCheck);
+ return;
+ }
- if (shouldBluff) {
- handleRaiseOrCall(data);
+ if (effectiveStrength < potOdds) {
+ // The pot is not offering the right price. Fold, but bluff occasionally.
+ bool bluff = (int)g_sci->getRNG().getRandomNumber(99) < aggression * 4;
+ data->setFromInt16(kPlayerAction, bluff ? kPlayerActionCall : kPlayerActionFold);
return;
}
- switch (hand) {
- case kHandTypeFiveOfAKind:
- case kHandTypeStraightFlush:
- case kHandTypeFullHouse:
- case kHandTypeFlush:
- case kHandTypeStraight:
- case kHandTypeThreeOfAKind:
- case kHandTypeFourOfAKind:
- case kHandTypeTwoPairs:
- handleRaiseOrCall(data);
- break;
- case kHandTypeOnePair:
+ if (effectiveStrength >= raiseThreshold)
+ data->setFromInt16(kPlayerAction, kPlayerActionRaise);
+ else
data->setFromInt16(kPlayerAction, kPlayerActionCall);
- break;
- case kHandTypeHighCard:
- data->setFromInt16(kPlayerAction, kPlayerActionFold);
- // TODO: kPlayerActionCheck
- break;
- }
}
reg_t hoyle5PokerEngine(SciArray *data) {
@@ -489,7 +714,7 @@ reg_t hoyle5PokerEngine(SciArray *data) {
handlePlayerAction(data);
break;
case kCheckWinner:
- data->setFromInt16(kWinningPlayers, 1 << getWinner(data));
+ data->setFromInt16(kWinningPlayers, getWinners(data));
break;
case kCheckDiscard:
handleDiscard(data);
diff --git a/engines/sci/engine/kmisc.cpp b/engines/sci/engine/kmisc.cpp
index d1a525e13bd..37876fdcee4 100644
--- a/engines/sci/engine/kmisc.cpp
+++ b/engines/sci/engine/kmisc.cpp
@@ -924,9 +924,6 @@ reg_t kWinDLL(EngineState *s, int argc, reg_t *argv) {
switch (operation) {
case 0: // load DLL
- if (dllName == "PENGIN16.DLL")
- showScummVMDialog(_("The Poker logic is hardcoded in an external DLL, and is not implemented yet. There exists some dummy logic for now, where opponent actions are chosen randomly"));
-
// This is originally a call to LoadLibrary() and to the Watcom function GetIndirectFunctionHandle
return make_reg(0, 1000); // fake ID for loaded DLL, normally returned from Windows LoadLibrary()
case 1: // free DLL
More information about the Scummvm-git-logs
mailing list