Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

4
engines/groovie/POTFILES Normal file
View File

@@ -0,0 +1,4 @@
engines/groovie/detection.cpp
engines/groovie/metaengine.cpp
engines/groovie/saveload.cpp
engines/groovie/script.cpp

View File

@@ -0,0 +1,4 @@
# This file is included from the main "configure" script
# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
add_engine groovie "Groovie" yes "groovie2" "7th Guest" "highres"
add_engine groovie2 "Groovie 2 games" yes "" "" "jpeg 16bit" "midi mpeg2"

View File

@@ -0,0 +1,6 @@
begin_section("Groovie");
add_person("Henry Bush", "spookypeanut", "");
add_person("Ray Carro", "Die4Ever", "");
add_person("Scott Thomas", "ST", "");
add_person("Jordi Vilalta Prat", "jvprat", "");
end_section();

524
engines/groovie/cursor.cpp Normal file
View File

@@ -0,0 +1,524 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/cursor.h"
#include "groovie/groovie.h"
#include "common/debug.h"
#include "common/archive.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/textconsole.h"
#include "graphics/cursorman.h"
namespace Groovie {
// Cursor Manager
GrvCursorMan::GrvCursorMan(OSystem *system) :
_syst(system), _lastTime(0), _current(255), _cursor(nullptr), _lastFrame(0) {
}
GrvCursorMan::~GrvCursorMan() {
// Delete the cursors
for (uint cursor = 0; cursor < _cursors.size(); cursor++) {
delete _cursors[cursor];
}
CursorMan.popAllCursors();
}
void GrvCursorMan::show(bool visible) {
CursorMan.showMouse(visible);
}
uint16 GrvCursorMan::getStyle() {
return _current;
}
void GrvCursorMan::setStyle(uint16 newStyle) {
// Reset the animation
_lastFrame = 254;
_lastTime = 1;
// Save the new cursor
_current = newStyle;
_cursor = _cursors[newStyle & 0xFF];
// Show the first frame
_cursor->enable();
animate();
}
void GrvCursorMan::animate() {
if (_lastTime) {
int newTime = _syst->getMillis();
if (newTime - _lastTime >= 66) {
_lastFrame++;
_lastFrame %= _cursor->getFrames();
_cursor->showFrame(_lastFrame);
_lastTime = _syst->getMillis();
}
}
}
// t7g Cursor
class Cursor_t7g : public Cursor {
public:
Cursor_t7g(uint8 *img, uint8 *pal);
void enable() override;
void showFrame(uint16 frame) override;
private:
byte *_img;
byte *_pal;
};
Cursor_t7g::Cursor_t7g(uint8 *img, uint8 *pal) :
_pal(pal) {
_width = img[0];
_height = img[1];
_numFrames = img[2];
uint8 elinor1 = img[3];
uint8 elinor2 = img[4];
_img = img + 5;
debugC(1, kDebugCursor, "Groovie::Cursor: width: %d, height: %d, frames:%d", _width, _height, _numFrames);
debugC(1, kDebugCursor, "Groovie::Cursor: elinor: 0x%02X (%d), 0x%02X (%d)", elinor1, elinor1, elinor2, elinor2);
}
void Cursor_t7g::enable() {
// Apply the palette
CursorMan.replaceCursorPalette(_pal, 0, 32);
}
void Cursor_t7g::showFrame(uint16 frame) {
// Set the mouse cursor
int offset = _width * _height * frame;
CursorMan.replaceCursor((const byte *)_img + offset, _width, _height, _width >> 1, _height >> 1, 0);
}
// t7g Cursor Manager
#define NUM_IMGS 9
static const uint16 cursorDataOffsets[NUM_IMGS] = {
0x0000, 0x182f, 0x3b6d, 0x50cc, 0x6e79, 0x825d, 0x96d7, 0xa455, 0xa776
};
#define NUM_PALS 7
//Pals: 0xb794, 0xb7f4, 0xb854, 0xb8b4, 0xb914, 0xb974, 0xb9d4
#define NUM_STYLES 11
// pyramid is cursor 8, eyes are 9 & 10
const uint GrvCursorMan_t7g::_cursorImg[NUM_STYLES] = {3, 5, 4, 3, 1, 0, 2, 6, 7, 8, 8};
const uint GrvCursorMan_t7g::_cursorPal[NUM_STYLES] = {0, 0, 0, 0, 2, 0, 1, 3, 5, 4, 6};
GrvCursorMan_t7g::GrvCursorMan_t7g(OSystem *system, Common::MacResManager *macResFork) :
GrvCursorMan(system) {
Common::SeekableReadStream *robgjd = nullptr;
if (macResFork) {
// Open the cursors file from the resource fork
robgjd = macResFork->getResource("rob.gjd");
} else {
// Open the cursors file
robgjd = SearchMan.createReadStreamForMember("rob.gjd");
}
if (!robgjd)
error("Groovie::Cursor: Couldn't open rob.gjd");
// Load the images
for (uint imgnum = 0; imgnum < NUM_IMGS; imgnum++) {
robgjd->seek(cursorDataOffsets[imgnum]);
_images.push_back(loadImage(*robgjd));
}
// Load the palettes
robgjd->seek(-0x60 * NUM_PALS, SEEK_END);
for (uint palnum = 0; palnum < NUM_PALS; palnum++) {
_palettes.push_back(loadPalette(*robgjd));
}
// Build the cursors
for (uint cursor = 0; cursor < NUM_STYLES; cursor++) {
Cursor *s = new Cursor_t7g(_images[_cursorImg[cursor]], _palettes[_cursorPal[cursor]]);
_cursors.push_back(s);
}
delete robgjd;
}
GrvCursorMan_t7g::~GrvCursorMan_t7g() {
// Delete the images
for (uint img = 0; img < _images.size(); img++) {
delete[] _images[img];
}
// Delete the palettes
for (uint pal = 0; pal < _palettes.size(); pal++) {
delete[] _palettes[pal];
}
}
byte *GrvCursorMan_t7g::loadImage(Common::SeekableReadStream &file) {
uint16 decompbytes = 0, offset, i, length;
uint8 flagbyte, lengthmask = 0x0F, offsetlen, var_8;
byte *cursorStorage = new byte[65536];
uint8 *runningcursor = cursorStorage;
bool finished = false;
while (!(finished || file.eos())) {
flagbyte = file.readByte();
for (i = 1; i <= 8; i++) {
if (!file.eos()) {
if (flagbyte & 1) {
*(runningcursor++) = file.readByte();
decompbytes++;
} else {
var_8 = file.readByte();
offsetlen = file.readByte();
if (var_8 == 0 && offsetlen == 0) {
finished = true;
break;
}
length = (offsetlen & lengthmask) + 3;
offsetlen >>= 4;
offset = (offsetlen << 8) + var_8;
decompbytes += length;
for (; length > 0; length--, runningcursor++) {
*(runningcursor) = *(runningcursor - offset);
}
}
flagbyte = flagbyte >> 1;
}
}
}
debug(9, "GrvCursorMan_t7g::loadImage(): decompressed %d bytes", decompbytes);
return cursorStorage;
}
byte *GrvCursorMan_t7g::loadPalette(Common::SeekableReadStream &file) {
byte *palette = new byte[3 * 32];
file.read(palette, 3 * 32);
return palette;
}
// v2 Cursor
class Cursor_v2 : public Cursor {
public:
Cursor_v2(Common::File &file);
~Cursor_v2() override;
void enable() override;
void showFrame(uint16 frame) override;
void blendCursor(uint32 *dst, int frame, int w, int h);
static void show2Cursors(Cursor_v2 *c1, uint16 frame1, Cursor_v2 *c2, uint16 frame2);
private:
// Currently locked to 16bit
byte *_img;
Graphics::PixelFormat _format;
void decodeFrame(byte *pal, byte *data, byte *dest, uint32 size);
};
Cursor_v2::Cursor_v2(Common::File &file) {
byte *pal = new byte[0x20 * 3];
_format = g_system->getScreenFormat();
_numFrames = file.readUint16LE();
_width = file.readUint16LE();
_height = file.readUint16LE();
_hotspotX = file.readUint16LE();
_hotspotY = file.readUint16LE();
_img = new byte[_width * _height * _numFrames * 4];
debugC(1, kDebugCursor, "Groovie::Cursor: width: %d, height: %d, frames:%d, hotspot: %d, %d", _width, _height, _numFrames, _hotspotX, _hotspotY);
uint16 tmp16;
int loop2count = file.readUint16LE();
debugC(5, kDebugCursor, "loop2count?: %d", loop2count);
for (int l = 0; l < loop2count; l++) {
tmp16 = file.readUint16LE();
debugC(5, kDebugCursor, "loop2a: %d", tmp16); // Index frame can merge to/from?
tmp16 = file.readUint16LE();
debugC(5, kDebugCursor, "loop2b: %d", tmp16); // Number of frames?
}
file.read(pal, 0x20 * 3);
for (int f = 0; f < _numFrames; f++) {
uint32 tmp32 = file.readUint32LE();
debugC(5, kDebugCursor, "loop3: %d", tmp32);
byte *data = new byte[tmp32];
file.read(data, tmp32);
decodeFrame(pal, data, _img + (f * _width * _height * 4), tmp32);
delete[] data;
}
delete[] pal;
}
Cursor_v2::~Cursor_v2() {
delete[] _img;
}
void Cursor_v2::decodeFrame(byte *pal, byte *data, byte *dest, uint32 size) {
// Scratch memory
byte *tmp = new byte[_width * _height * 4]();
byte *ptr = tmp;
byte ctrA = 0, ctrB = 0;
byte alpha = 0, palIdx = 0;
byte r, g, b;
const byte alphaDecoded[8] = {0, 36, 73, 109, 146, 182, 219, 255}; // Calculated by: alpha = ((int)(*data & 0xE0) * 255) / 224;
// Start frame decoding
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
if (!size) {
debugC(1, kDebugCursor, "Cursor_v2::decodeFrame(): Frame underflow");
break;
}
// If both counters are empty
if (ctrA == 0 && ctrB == 0) {
if (*data & 0x80) {
ctrA = (*data++ & 0x7F) + 1;
size--;
} else {
ctrB = *data++ + 1;
alpha = alphaDecoded[(*data & 0xE0) >> 5];
palIdx = *data++ & 0x1F;
size -= 2;
}
}
if (ctrA) {
// Block type A - chunk of non-continuous pixels
palIdx = *data & 0x1F;
alpha = alphaDecoded[(*data++ & 0xE0) >> 5];
size--;
r = *(pal + palIdx);
g = *(pal + palIdx + 0x20);
b = *(pal + palIdx + 0x40);
ctrA--;
} else {
// Block type B - chunk of continuous pixels
r = *(pal + palIdx);
g = *(pal + palIdx + 0x20);
b = *(pal + palIdx + 0x40);
ctrB--;
}
// Decode pixel
if (alpha) {
ptr[0] = alpha;
ptr[1] = r;
ptr[2] = g;
ptr[3] = b;
}
ptr += 4;
}
if (!size)
break;
}
// Convert to screen format
// NOTE: Currently locked to 32bpp
ptr = tmp;
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
*(uint32 *)dest = _format.ARGBToColor(ptr[0], ptr[1], ptr[2], ptr[3]);
dest += 4;
ptr += 4;
}
}
delete[] tmp;
}
void Cursor_v2::enable() {
}
void Cursor_v2::showFrame(uint16 frame) {
int offset = _width * _height * frame * 4;
// SDL uses keycolor even though we're using ABGR8888, so just set it to a pink color that isn't used
uint32 keycolor = _format.ARGBToColor(0, 255, 128, 255);
CursorMan.replaceCursor((const byte *)(_img + offset), _width, _height, _hotspotX, _hotspotY, keycolor, false, &_format);
}
void blendCursorPixel(uint32 &d, uint32 &s) {
#ifdef SCUMM_LITTLE_ENDIAN
static const int kAIndex = 0;
static const int kBIndex = 1;
static const int kGIndex = 2;
static const int kRIndex = 3;
#else
static const int kAIndex = 3;
static const int kBIndex = 2;
static const int kGIndex = 1;
static const int kRIndex = 0;
#endif
byte *dst = (byte *)&d;
byte *src = (byte *)&s;
if (src[kAIndex] == 255) {
d = s;
} else if (src[kAIndex] > 0) {
dst[kAIndex] = MAX(src[kAIndex], dst[kAIndex]);
dst[kRIndex] = ((src[kRIndex] * src[kAIndex]) + dst[kRIndex] * (255 - src[kAIndex])) >> 8;
dst[kGIndex] = ((src[kGIndex] * src[kAIndex]) + dst[kGIndex] * (255 - src[kAIndex])) >> 8;
dst[kBIndex] = ((src[kBIndex] * src[kAIndex]) + dst[kBIndex] * (255 - src[kAIndex])) >> 8;
}
// In case of alpha == 0 just do nothing
}
void Cursor_v2::blendCursor(uint32 *dst, int frame, int w, int h) {
uint32 *src = (uint32 *)_img;
src += _width * _height * frame;
int offX = (w - _width) / 2;
int offY = (h - _height) / 2;
for (int y = 0; y < _height; y++) {
for (int x = 0; x < _width; x++) {
blendCursorPixel(dst[x + offX + (y + offY) * w], src[x + y * _width]);
}
}
}
void Cursor_v2::show2Cursors(Cursor_v2 *c1, uint16 frame1, Cursor_v2 *c2, uint16 frame2) {
int width = MAX(c1->_width, c2->_width);
int height = MAX(c1->_height, c2->_height);
uint32 *img = new uint32[width * height]();
c2->blendCursor(img, frame2, width, height);
c1->blendCursor(img, frame1, width, height);
// SDL uses keycolor even though we're using ABGR8888, so just set it to a pink color that isn't used
Graphics::PixelFormat format = g_system->getScreenFormat();
uint32 keycolor = format.ARGBToColor(0, 255, 128, 255);
// replaceCursor copies the buffer, so we're ok to delete it
CursorMan.replaceCursor((const byte *)img, width, height, c1->_hotspotX, c1->_hotspotY, keycolor, false, &c1->_format);
delete[] img;
}
// v2 Cursor Manager
GrvCursorMan_v2::GrvCursorMan_v2(OSystem *system) :
GrvCursorMan(system), _cursor2(nullptr), _lastFrame2(0) {
// Open the icons file
Common::File iconsFile;
if (!iconsFile.open("icons.ph") && !iconsFile.open("icons.bin"))
error("Groovie::Cursor: Couldn't open icons.ph or icons.bin");
// Verify the signature
uint32 tmp32 = iconsFile.readUint32BE();
uint16 tmp16 = iconsFile.readUint16LE();
if (tmp32 != MKTAG('i','c','o','n') || tmp16 != 1)
error("Groovie::Cursor: %s signature failed: %s %d", iconsFile.getName(), tag2str(tmp32), tmp16);
// Read the number of icons
uint16 nicons = iconsFile.readUint16LE();
// Read the icons
for (int i = 0; i < nicons; i++) {
Cursor *s = new Cursor_v2(iconsFile);
_cursors.push_back(s);
}
iconsFile.close();
}
GrvCursorMan_v2::~GrvCursorMan_v2() {
}
void GrvCursorMan_v2::animate() {
if (_lastTime) {
int newTime = _syst->getMillis();
if (newTime - _lastTime >= 66) {
_lastFrame++;
_lastFrame %= _cursor->getFrames();
if (_cursor2) {
_lastFrame2++;
_lastFrame2 %= _cursor2->getFrames();
Cursor_v2::show2Cursors((Cursor_v2 *)_cursor, _lastFrame, (Cursor_v2 *)_cursor2, _lastFrame2);
} else {
_cursor->showFrame(_lastFrame);
}
_lastTime = _syst->getMillis();
}
}
}
void GrvCursorMan_v2::setStyle(uint16 newStyle) {
// HACK: Cursor 4 is actually cursor 3, but with some changes to alpha blending
// (which is currently not handled)
uint8 newStyleLow = newStyle & 0xFF;
GrvCursorMan::setStyle(newStyleLow == 4 ? 3 : newStyle);
if (newStyle & 0x8000) {
_cursor2 = _cursors.back();
_lastFrame2 = 254;
} else {
_cursor2 = nullptr;
}
// fix _current back to cursor 4 so that getStyle returns the proper number
if (newStyleLow == 4)
_current++;
}
} // End of Groovie namespace

106
engines/groovie/cursor.h Normal file
View File

@@ -0,0 +1,106 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_CURSOR_H
#define GROOVIE_CURSOR_H
#include "common/array.h"
#include "common/system.h"
namespace Common {
class MacResManager;
}
namespace Groovie {
class Cursor {
public:
virtual ~Cursor() {}
uint16 getFrames() { return _numFrames; }
virtual void enable() = 0;
virtual void showFrame(uint16 frame) = 0;
protected:
uint16 _width;
uint16 _height;
uint16 _hotspotX;
uint16 _hotspotY;
uint16 _numFrames;
};
class GrvCursorMan {
public:
GrvCursorMan(OSystem *system);
virtual ~GrvCursorMan();
virtual void show(bool visible);
virtual void animate();
virtual void setStyle(uint16 newStyle);
virtual uint16 getStyle();
protected:
OSystem *_syst;
// Animation variables
uint8 _lastFrame;
uint32 _lastTime;
// Styles
Common::Array<Cursor *> _cursors;
uint16 _current;
Cursor *_cursor;
};
class GrvCursorMan_t7g : public GrvCursorMan {
public:
GrvCursorMan_t7g(OSystem *system, Common::MacResManager *macResFork = 0);
~GrvCursorMan_t7g() override;
private:
// Styles data
static const uint _cursorImg[];
static const uint _cursorPal[];
// Cursors data
Common::Array<byte *> _images;
Common::Array<byte *> _palettes;
// Loading functions
byte *loadImage(Common::SeekableReadStream &file);
byte *loadPalette(Common::SeekableReadStream &file);
};
class GrvCursorMan_v2 : public GrvCursorMan {
public:
GrvCursorMan_v2(OSystem *system);
~GrvCursorMan_v2() override;
void animate() override;
void setStyle(uint16 newStyle) override;
private:
Cursor *_cursor2;
uint8 _lastFrame2;
};
} // End of Groovie namespace
#endif // GROOVIE_CURSOR_H

160
engines/groovie/debug.cpp Normal file
View File

@@ -0,0 +1,160 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/debug.h"
#include "groovie/graphics.h"
#include "groovie/groovie.h"
#include "groovie/resource.h"
#include "groovie/script.h"
#include "common/debug-channels.h"
#include "common/system.h"
#include "graphics/paletteman.h"
namespace Groovie {
Debugger::Debugger(GroovieEngine *vm) :
_vm(vm), _script(_vm->_script) {
// Register the debugger comands
registerCmd("step", WRAP_METHOD(Debugger, cmd_step));
registerCmd("go", WRAP_METHOD(Debugger, cmd_go));
registerCmd("pc", WRAP_METHOD(Debugger, cmd_pc));
registerCmd("fg", WRAP_METHOD(Debugger, cmd_fg));
registerCmd("bg", WRAP_METHOD(Debugger, cmd_bg));
registerCmd("mem", WRAP_METHOD(Debugger, cmd_mem));
registerCmd("var", WRAP_METHOD(Debugger, cmd_mem)); // alias
registerCmd("load", WRAP_METHOD(Debugger, cmd_loadgame));
registerCmd("save", WRAP_METHOD(Debugger, cmd_savegame));
registerCmd("playref", WRAP_METHOD(Debugger, cmd_playref));
registerCmd("dumppal", WRAP_METHOD(Debugger, cmd_dumppal));
registerCmd("dumpfile", WRAP_METHOD(Debugger, cmd_dumpfile));
}
Debugger::~Debugger() {
}
int Debugger::getNumber(const char *arg) {
return strtol(arg, (char **)nullptr, 0);
}
bool Debugger::cmd_step(int argc, const char **argv) {
_script->step();
return true;
}
bool Debugger::cmd_go(int argc, const char **argv) {
_script->step();
return false;
}
bool Debugger::cmd_fg(int argc, const char **argv) {
_vm->_graphicsMan->updateScreen(&_vm->_graphicsMan->_foreground);
return false;
}
bool Debugger::cmd_bg(int argc, const char **argv) {
_vm->_graphicsMan->updateScreen(&_vm->_graphicsMan->_background);
return false;
}
bool Debugger::cmd_pc(int argc, const char **argv) {
if (argc == 2) {
int val = getNumber(argv[1]);
_script->_currentInstruction = val;
}
debugPrintf("pc = 0x%04X (%d)\n", _script->_currentInstruction, _script->_currentInstruction);
return true;
}
bool Debugger::cmd_mem(int argc, const char **argv) {
if (argc >= 2) {
int pos = getNumber(argv[1]);
uint8 val;
if (argc >= 3) {
// Set
val = getNumber(argv[2]);
_script->_variables[pos] = val;
} else {
// Get
val = _script->_variables[pos];
}
debugPrintf("%s[0x%04X] = 0x%02X\n", argv[0], pos, val);
} else {
debugPrintf("Syntax: %s <addr> [<val>]\n", argv[0]);
}
return true;
}
bool Debugger::cmd_loadgame(int argc, const char **argv) {
if (argc == 2) {
int slot = getNumber(argv[1]);
_script->loadgame(slot);
} else {
debugPrintf("Syntax: load <slot>\n");
}
return true;
}
bool Debugger::cmd_savegame(int argc, const char **argv) {
if (argc == 2) {
int slot = getNumber(argv[1]);
_script->directGameSave(slot, "debug save");
} else {
debugPrintf("Syntax: save <slot>\n");
}
return true;
}
bool Debugger::cmd_playref(int argc, const char **argv) {
if (argc == 2) {
int ref = getNumber(argv[1]);
_script->playvideofromref(ref);
} else {
debugPrintf("Syntax: playref <videorefnum>\n");
}
return true;
}
bool Debugger::cmd_dumppal(int argc, const char **argv) {
uint16 i;
byte palettedump[256 * 3];
_vm->_system->getPaletteManager()->grabPalette(palettedump, 0, 256);
for (i = 0; i < 256; i++) {
debugPrintf("%3d: %3d,%3d,%3d\n", i, palettedump[(i * 3)], palettedump[(i * 3) + 1], palettedump[(i * 3) + 2]);
}
return true;
}
bool Debugger::cmd_dumpfile(int argc, const char **argv) {
if (argc == 2) {
Common::String fileName = argv[1];
debugPrintf("Dumping %s...\n", argv[1]);
_vm->_resMan->dumpResource(fileName);
} else {
debugPrintf("Syntax: %s <filename>\n", argv[0]);
}
return true;
}
} // End of Groovie namespace

58
engines/groovie/debug.h Normal file
View File

@@ -0,0 +1,58 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_DEBUG_H
#define GROOVIE_DEBUG_H
#include "gui/debugger.h"
namespace Groovie {
class GroovieEngine;
class Script;
class Debugger : public GUI::Debugger {
public:
Debugger(GroovieEngine *vm);
~Debugger() override;
private:
GroovieEngine *_vm;
Script *_script;
int getNumber(const char *arg);
bool cmd_step(int argc, const char **argv);
bool cmd_go(int argc, const char **argv);
bool cmd_pc(int argc, const char **argv);
bool cmd_bg(int argc, const char **argv);
bool cmd_fg(int argc, const char **argv);
bool cmd_mem(int argc, const char **argv);
bool cmd_loadgame(int argc, const char **argv);
bool cmd_savegame(int argc, const char **argv);
bool cmd_playref(int argc, const char **argv);
bool cmd_dumppal(int argc, const char **argv);
bool cmd_dumpfile(int argc, const char **argv);
};
} // End of Groovie namespace
#endif // GROOVIE_DEBUG_H

View File

@@ -0,0 +1,316 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/system.h"
#include "common/translation.h"
#include "engines/advancedDetector.h"
#include "groovie/detection.h"
#include "groovie/groovie.h"
using namespace Common;
namespace Groovie {
static const DebugChannelDef debugFlagList[] = {
{Groovie::kDebugVideo, "Video", "Debug video and audio playback"},
{Groovie::kDebugResource, "Resource", "Debug resource management"},
{Groovie::kDebugScript, "Script", "Debug the scripts"},
{Groovie::kDebugUnknown, "Unknown", "Report values of unknown data in files"},
{Groovie::kDebugHotspots, "Hotspots", "Show the hotspots"},
{Groovie::kDebugCursor, "Cursor", "Debug cursor decompression / switching"},
{Groovie::kDebugMIDI, "MIDI", "Debug MIDI / XMIDI files"},
{Groovie::kDebugScriptvars, "Scriptvars", "Print out any change to script variables"},
{Groovie::kDebugLogic, "Logic", "Debug the AI puzzles in the logic folder and TLC questionnaires"},
{Groovie::kDebugFast, "Fast", "Play videos quickly, with no sound (unstable)"},
DEBUG_CHANNEL_END};
static const PlainGameDescriptor groovieGames[] = {
// Games
{"t7g", "The 7th Guest"},
{"11h", "The 11th Hour: The Sequel to The 7th Guest"},
{"11hsu", "The 11th Hour: Souped Up"},
{"making11h", "The Making of The 11th Hour"},
{"clandestiny", "Clandestiny"},
{"unclehenry", "Uncle Henry's Playhouse"},
{"tlc", "Tender Loving Care"},
{nullptr, nullptr}};
const int BASE_FLAGS = ADGF_NO_FLAGS;
#define GROOVIEGAME(id, extra, f1, x1, s1, f2, x2, s2, language, platform, flags, guiOptions, version) \
{ \
{ \
id, extra, \
AD_ENTRY2s(f1, x1, s1, f2, x2, s2), \
language, platform, (flags), \
(guiOptions) \
}, \
version \
}
#define T7GENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("t7g", extra, f1, x1, s1, f2, x2, s2, language, platform, flags, GUIO10(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GUIO_NOASPECT, GUIO_NOSFX, GAMEOPTION_T7G_FAST_MOVIE_SPEED, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_EASIER_AI, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieT7G)
#define T7GNOMIDIENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags, guiOptions) \
GROOVIEGAME("t7g", extra, f1, x1, s1, f2, x2, s2, language, platform, flags, (GUIO_NOMIDI GUIO_NOASPECT GUIO_NOSFX GAMEOPTION_T7G_FAST_MOVIE_SPEED GAMEOPTION_SLIMHOTSPOTS GAMEOPTION_SPEEDRUN guiOptions), kGroovieT7G)
#define T11HENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("11h", extra, f1, x1, s1, f2, x2, s2, language, platform, flags, GUIO8(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM GUIO_NOASPECT, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_EASIER_AI, GAMEOPTION_FINAL_HOUR, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieT11H)
#define T11HDEMOENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("11h", extra, f1, x1, s1, f2, x2, s2, language, platform, flags | ADGF_DEMO, GUIO7(GUIO_NOLAUNCHLOAD, GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GUIO_NOASPECT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieT11H)
#define T11HMAKINGOFENTRY(f1, x1, s1, f2, x2, s2, language, platform) \
GROOVIEGAME("making11h", "", f1, x1, s1, f2, x2, s2, language, platform, ADGF_NO_FLAGS, GUIO6(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GUIO_NOASPECT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieT11H)
#define CLANENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("clandestiny", extra, f1, x1, s1, f2, x2, s2, language, platform, flags, GUIO6(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_EASIER_AI, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieCDY)
#define CLANDEMOENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("clandestiny", extra, f1, x1, s1, f2, x2, s2, language, platform, flags | ADGF_DEMO, GUIO5(GUIO_NOMIDI, GUIO_NOLAUNCHLOAD, GUIO_NOASPECT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieCDY)
#define UHPENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("unclehenry", extra, f1, x1, s1, f2, x2, s2, language, platform, flags, GUIO5(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_EASIER_AI, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieUHP)
#define TLCENTRY(extra, f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("tlc", extra, f1, x1, s1, f2, x2, s2, language, platform, flags | ADGF_CD, GUIO4(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieTLC)
#define TLCDVDENTRY(f1, x1, s1, f2, x2, s2, language, platform) \
GROOVIEGAME("tlc", MetaEngineDetection::GAME_NOT_IMPLEMENTED, f1, x1, s1, f2, x2, s2, language, platform, ADGF_DVD, GUIO4(GUIO_NOMIDI, GUIO_NOASPECT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieTLC)
#define TLCDEMOENTRY(f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("tlc", "Demo", f1, x1, s1, f2, x2, s2, language, platform, flags | ADGF_DEMO, GUIO3(GUIO_NOMIDI, GUIO_NOASPECT, GUIO_NOLAUNCHLOAD), kGroovieTLC)
#define TLCTRAILERENTRY(f1, x1, s1, f2, x2, s2, language, platform, flags) \
GROOVIEGAME("tlc", "Trailer", f1, x1, s1, f2, x2, s2, language, platform, flags | ADGF_DEMO, GUIO3(GUIO_NOMIDI, GUIO_NOASPECT, GUIO_NOLAUNCHLOAD), kGroovieTLC)
// clang-format off
static const GroovieGameDescription gameDescriptions[] = {
// groovie.cpp requires the first file to be the main .grv file for v2 games, might as well stick to that convention for v1 games from now on too
/*==== The 7th Guest ====*/
// The 7th Guest 25th Anniversary
T7GENTRY("25th Anniversary Edition", "script.grv", "d1b8033b40aa67c076039881eccce90d", 16659,
"AT.gjd", "bbaa95ce6e600a8ba5b2902326cd11f8", 28827446, EN_ANY, kPlatformWindows, ADGF_UNSTABLE | ADGF_REMASTERED),
// The 7th Guest DOS English
T7GENTRY("", "script.grv", "d1b8033b40aa67c076039881eccce90d", 16659,
"AT.gjd", "bbaa95ce6e600a8ba5b2902326cd11f8", 45171574, EN_ANY, kPlatformDOS, BASE_FLAGS),
// The 7th Guest Mac English
T7GENTRY("", "script.grv", nullptr, AD_NO_SIZE,// FIXMEMD5
"T7GMac", "acdc4a58dd3f007f65e99b99d78e0bce", 1814029, EN_ANY, kPlatformMacintosh, ADGF_MACRESFORK),
// The 7th Guest DOS Russian (Akella)
T7GENTRY("", "script.grv", "d1b8033b40aa67c076039881eccce90d", 16659,
"intro.gjd", nullptr, 31711554, RU_RUS, kPlatformDOS, BASE_FLAGS),
// The 7th Guest iOS English
T7GNOMIDIENTRY("", "script.grv", "d1b8033b40aa67c076039881eccce90d", 16659,
"SeventhGuest", nullptr, AD_NO_SIZE, EN_ANY, kPlatformIOS, BASE_FLAGS, GAMEOPTION_ORIGINAL_SAVELOAD GAMEOPTION_EASIER_AI),
// "Guest" early DOS demo
T7GNOMIDIENTRY("The early \"Guest\" demo is not supported", "playtlc.exe", "9cff0e9649ddf49e9fe5168730aa7201", 254768,
"FACE.VDX", "614f820265274dc99d8869de67df1718", 1382814,
EN_ANY, kPlatformDOS, ADGF_DEMO | ADGF_UNSUPPORTED, GUIO_NONE),
/*==== The 11th Hour ====*/
// The 11th Hour DOS/Windows English (Available on Steam)
T11HENTRY("", "script.grv", "bdb9a783d4debe477ac3856adc454c17", 62447,
"introd1.gjd", "9ec3e727182fbe40ee23e786721180eb", 6437077, EN_ANY, kPlatformWindows, BASE_FLAGS),
// The 11th Hour DOS/Windows German
T11HENTRY("", "script.grv", "560e90b47054639668e44a8b365fbe26", 62447, "introd1.gjd", nullptr, AD_NO_SIZE, DE_DEU, kPlatformWindows, BASE_FLAGS),
// The 11th Hour DOS/Windows French
T11HENTRY("", "script.grv", "752c0a8ea62a1207c8583f3dbc16e6ef", 62447, "introd1.gjd", nullptr, AD_NO_SIZE, FR_FRA, kPlatformWindows, BASE_FLAGS),
// The 11th Hour DOS/Windows Russian (Akella)
T11HENTRY("", "script.grv", "bdb9a783d4debe477ac3856adc454c17", 62447,
"introd1.gjd", "b80c6d88ac576cdd6f98d1e467629108", 1516, RU_RUS, kPlatformWindows, BASE_FLAGS),
// The 11th Hour Mac English
T11HENTRY("", "script.grv", "bdb9a783d4debe477ac3856adc454c17", 62447,
"The 11th Hour Installer", "bcdb4040b27f15b18f39fb9e496d384a", 1002987, EN_ANY, kPlatformMacintosh, BASE_FLAGS),
// The 11th Hour Mac English (Installed)
T11HENTRY("Installed", "script.grv", "bdb9a783d4debe477ac3856adc454c17", 62447,
"el01.mov", "70f42dfc25b1488a08011dc45bb5145d", 6039, EN_ANY, kPlatformMacintosh, BASE_FLAGS),
// The 11th Hour: Souped Up
GROOVIEGAME("11hsu", "", "suscript.grv", NULL, AD_NO_SIZE,
"introd1.gjd", "9ec3e727182fbe40ee23e786721180eb", 6437077, EN_ANY, kPlatformWindows, BASE_FLAGS,
GUIO7(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM GUIO_NOASPECT, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_EASIER_AI_DEFAULT, GAMEOPTION_SLIMHOTSPOTS, GAMEOPTION_SPEEDRUN), kGroovieT11H),
/*==== The 11th Hour Demos ====*/
// The 11th Hour DOS Interactive Demo English https://archive.org/details/11th_Hour_demo
T11HDEMOENTRY("Interactive Demo", "demo.grv", "5faec559b9abf18cf143751b420208dc", 15991,
"dvmod1a.gjd", "e304fe68f95c54fc82d785768e372892", 8068568, EN_ANY, kPlatformDOS, BASE_FLAGS),
// The 11th Hour DOS Interactive Demo English (packaged with the Non-Interactive Demo) https://archive.org/details/11HDEMO
T11HDEMOENTRY("Interactive Demo", "demo.grv", "824b1a051f841a50ab7a6b4c10180bbc", 15898,
"dvmod1a.gjd", "e304fe68f95c54fc82d785768e372892", 8068568, EN_ANY, kPlatformDOS, BASE_FLAGS),
// The 11th Hour DOS Non-Interactive Demo English https://archive.org/details/11HDEMO
T11HDEMOENTRY("Non-Interactive Demo", "niloop.grv", "b4c35a2a6ebaf72fbd830b590d48f8ea", 456,
"dvmod1b.gjd", "43eb268ef6d64a75b9846df5be453d30", 11264100, EN_ANY, kPlatformDOS, BASE_FLAGS),
/*==== The Making of The 11th Hour ====*/
// all are in english even if they came packaged with alternate language versions of the game
// I removed the hash check for now so they all match with a single entry since the language field is useless here
// The Making of The 11th Hour DOS/Windows
T11HMAKINGOFENTRY("makingof.grv", nullptr, 994, "zmakd2a.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows),
/*// The Making of The 11th Hour DOS/Windows English
T11HMAKINGOFENTRY(GROOVIEFILES("makingof.grv", "12e1e5eef2c7a9536cd12ac800b31408", 994, "zmakd2a.gjd"), EN_ANY, kPlatformWindows),
// The Making of The 11th Hour DOS/Windows German
T11HMAKINGOFENTRY(GROOVIEFILES("makingof.grv", "03492c6ad3088b3f9f51a3eaba6b8c8e", 994, "zmakd2a.gjd"), EN_ANY, kPlatformWindows),
// The Making of The 11th Hour DOS/Windows French
T11HMAKINGOFENTRY(GROOVIEFILES("makingof.grv", "77c4ae4deb0e323ccd7dcca0f99de2b9", 994, "zmakd2a.gjd"), EN_ANY, kPlatformWindows),
// The Making of The 11th Hour Macintosh English
T11HMAKINGOFENTRY(AD_ENTRY2s("makingof.grv", "12e1e5eef2c7a9536cd12ac800b31408", 994,
"The 11th Hour Installer", "bcdb4040b27f15b18f39fb9e496d384a", 1002987), EN_ANY, kPlatformMacintosh),*/
// The Making of The 11th Hour Macintosh
T11HMAKINGOFENTRY("makingof.grv", nullptr, 994,
"The 11th Hour Installer", nullptr, 1002987, EN_ANY, kPlatformMacintosh),
/*==== Clandestiny Demos ====*/
// Clandestiny Trailer Macintosh English
CLANDEMOENTRY("Trailer", "trailer.grv", "a7c8bdc4e8bff621f4f50928a95eaaba", 6,
"The 11th Hour Installer", "bcdb4040b27f15b18f39fb9e496d384a", 1002987, EN_ANY, kPlatformMacintosh, BASE_FLAGS),
// Clandestiny PC Demo English https://archive.org/details/Clandestiny_demo
CLANDEMOENTRY("Demo", "clandemo.grv", "faa863738da1c93673ed58a4b9597a63", 6744, "cddemo.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows, BASE_FLAGS),
// Clandestiny PC Trailer English https://downloads.scummvm.org/frs/demos/groovie/clandestiny-dos-ni-demo-en.zip
CLANDEMOENTRY("Trailer", "trailer.grv", "a7c8bdc4e8bff621f4f50928a95eaaba", 6,
"zclan.gjd", "4a7258166916fcc0d217c8f21fa3cc79", 20454932, EN_ANY, kPlatformWindows, BASE_FLAGS),
/*==== Clandestiny ====*/
// Clandestiny PC English
CLANENTRY("", "clanmain.grv", "dd424120fa1daa9d6b576d0ba22a4936", 54253, "ACT01MUS.MPG", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows, BASE_FLAGS),
// Clandestiny Mac/iOS App Store
CLANENTRY("Mac/iOS", "CLANMAIN.GRV", "dd424120fa1daa9d6b576d0ba22a4936", 54253, "ACT01MUS.m4a", nullptr, AD_NO_SIZE, EN_ANY, kPlatformUnknown, BASE_FLAGS),
/*==== Uncle Henry's Playhouse ====*/
// Uncle Henry's Playhouse PC English (1996-09-13)
UHPENTRY("", "tpot.grv", "849dc7e5309e1b9acf72d8abc9e145df", 11693, "trt7g.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows, BASE_FLAGS),
// Uncle Henry's Playhouse PC German
// Funsoft (1997-02-14)
UHPENTRY("", "tpot.grv", "30d06af7669004f1ea7a99a5ebdb6935", 10469, "trt7g.gjd", nullptr, AD_NO_SIZE, DE_DEU, kPlatformWindows, BASE_FLAGS),
// Uncle Henry's Playhouse Beta Version PC English (1996-09-05)
UHPENTRY("", "tpot.grv", "123113a26d4bdad6d1f88a53ec6b28a3", 11686, "tpt.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows, BASE_FLAGS),
/*==== Tender Loving Care ====*/
// Tender Loving Care PC English (CD-ROM 1998-05-01)
TLCENTRY("CD", "tlcmain.grv", "47c235155de5103e72675fe7294720b8", 17479, "tlcnav.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows, ADGF_CD),
// Tender Loving Care PC English (DVD-ROM 1998-06-12)
TLCDVDENTRY("tlcmain.grv", "8a591c47d24dde38615e6ea2e79b51a5", 17375, "tlcnav.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows),
// Tender Loving Care PC English (DVD-ROM 1998-08-26)
TLCDVDENTRY("tlcmain.grv", "151af191015beb6f662919153e6c28d8", 17379, "tlcnav.gjd", nullptr, AD_NO_SIZE, EN_ANY, kPlatformWindows),
// Tender Loving Care PC German (CD-ROM 1998-04-08)
// "Die Versuchung", Funsoft
TLCENTRY("CD", "tlcmain.grv", "3459a25a5f31b430d320cba2e47d3c98", 17353, "tlcnav.gjd", nullptr, AD_NO_SIZE, DE_DEU, kPlatformWindows, BASE_FLAGS),
// Tender Loving Care PC German (DVD-ROM 1998-08-23)
// "Die Versuchung", Conspiracy Entertainment Europe
TLCDVDENTRY("tlcmain.grv", "50e62d41ad2cddd0f31ea0a542338387", 17344, "tlcnav.gjd", nullptr, AD_NO_SIZE, DE_DEU, kPlatformWindows),
// Tender Loving Care PC Demo German (CD-ROM 1998-03-23)
// https://archive.org/details/Tender_Loving_Care_demo
TLCDEMOENTRY("tlcmain.grv", "6ec818f595eedca6570280af0c681642", 17361, "tlcnav.gjd", nullptr, AD_NO_SIZE, DE_DEU, kPlatformWindows, BASE_FLAGS),
// Tender Loving Care PC Trailer (CD-ROM 1998-03-23)
// On the same disc with the above German demo
TLCTRAILERENTRY("preview.grv", "d95401509a0ef251e8c340737edf728c", 19, "drama1.gjd", "2a4ca274d832675248e51baf7e537bb3", 390727225, UNK_LANG, kPlatformWindows, BASE_FLAGS),
// Tender Loving Care PC Trailer (CD-ROM 1998-05-14)
// Included on the English version of the full game. The preview.grv
// file is also on the Englissh DVD (1998-09-10), but I do not know if
// it actually has the trailer or not. Since the trailer does not
// contain any spoken language, it's possible that this entry can be
// merged with the German one above.
TLCTRAILERENTRY("preview.grv", "d95401509a0ef251e8c340737edf728c", 19, "drama1.gjd", "0ac95ecdd0c8388199bf453de9f7b527", 396742303, UNK_LANG, kPlatformWindows, BASE_FLAGS),
{AD_TABLE_END_MARKER, kGroovieT7G}
};
// clang-format on
static const char *const directoryGlobs[] = {
"MIDI",
"GROOVIE",
"MEDIA",
nullptr
};
class GroovieMetaEngineDetection : public AdvancedMetaEngineDetection<GroovieGameDescription> {
public:
GroovieMetaEngineDetection() : AdvancedMetaEngineDetection(gameDescriptions, groovieGames) {
// Use kADFlagUseExtraAsHint in order to distinguish the 11th hour from
// its "Making of" as well as the Clandestiny Trailer; they all share
// the same MD5.
// TODO: Is this the only reason, or are there others (like the three
// potentially sharing a single directory) ? In the former case, then
// perhaps a better solution would be to add additional files
// to the detection entries. In the latter case, this TODO should be
// replaced with an according explanation.
_flags = kADFlagUseExtraAsHint;
_guiOptions = GUIO2(GUIO_NOSUBTITLES, GUIO_NOASPECT);
// Need MIDI directory to detect 11H Mac Installed
_maxScanDepth = 2;
_directoryGlobs = directoryGlobs;
}
const char *getName() const override {
return "groovie";
}
const char *getEngineName() const override {
return "Groovie";
}
const char *getOriginalCopyright() const override {
return "Groovie Engine (C) 1990-1996 Trilobyte";
}
const DebugChannelDef *getDebugChannels() const override {
return debugFlagList;
}
};
} // End of namespace Groovie
REGISTER_PLUGIN_STATIC(GROOVIE_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, Groovie::GroovieMetaEngineDetection);

View File

@@ -0,0 +1,55 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_DETECTION_H
#define GROOVIE_DETECTION_H
#include "engines/advancedDetector.h"
namespace Groovie {
enum EngineVersion {
kGroovieT7G,
kGroovieT11H,
kGroovieCDY,
kGroovieUHP,
kGroovieTLC
};
struct GroovieGameDescription {
AD_GAME_DESCRIPTION_HELPERS(desc);
ADGameDescription desc;
EngineVersion version; // Version of the engine
};
#define GAMEOPTION_T7G_FAST_MOVIE_SPEED GUIO_GAMEOPTIONS1
#define GAMEOPTION_ORIGINAL_SAVELOAD GUIO_GAMEOPTIONS2
#define GAMEOPTION_EASIER_AI GUIO_GAMEOPTIONS3
#define GAMEOPTION_FINAL_HOUR GUIO_GAMEOPTIONS4
#define GAMEOPTION_SPEEDRUN GUIO_GAMEOPTIONS5
#define GAMEOPTION_EASIER_AI_DEFAULT GUIO_GAMEOPTIONS6
#define GAMEOPTION_SLIMHOTSPOTS GUIO_GAMEOPTIONS7
} // End of namespace Groovie
#endif

134
engines/groovie/font.cpp Normal file
View File

@@ -0,0 +1,134 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/array.h"
#include "common/textconsole.h"
#include "graphics/surface.h"
#include "groovie/font.h"
namespace Groovie {
T7GFont::T7GFont() : _maxHeight(0), _maxWidth(0), _glyphs(nullptr) {
}
T7GFont::~T7GFont() {
delete[] _glyphs;
}
bool T7GFont::load(Common::SeekableReadStream &stream) {
// Read the mapping of characters to glyphs
if (stream.read(_mapChar2Glyph, 128) < 128) {
error("Groovie::T7GFont: Couldn't read the character to glyph map");
return false;
}
// Calculate the number of glyphs
byte numGlyphs = 0;
for (int i = 0; i < 128; i++)
if (_mapChar2Glyph[i] >= numGlyphs)
numGlyphs = _mapChar2Glyph[i] + 1;
// Read the glyph offsets
uint16 *glyphOffsets = new uint16[numGlyphs];
for (int i = 0; i < numGlyphs; i++)
glyphOffsets[i] = stream.readUint16LE();
if (stream.eos()) {
error("Groovie::T7GFont: Couldn't read the glyph offsets");
delete[] glyphOffsets;
return false;
}
// Allocate the glyph data
delete[] _glyphs;
_glyphs = new Glyph[numGlyphs];
// Ensure we're ready to read the first glyph. (Most versions don't
// need it, but the russian one does. This fixes bug #5481.)
stream.seek(glyphOffsets[0]);
// Read the glyphs
_maxHeight = _maxWidth = 0;
for (int i = 0; (i < numGlyphs) && !stream.eos(); i++) {
// Verify we're at the expected stream position
if (stream.pos() != glyphOffsets[i]) {
uint16 offset = glyphOffsets[i];
delete[] glyphOffsets;
error("Groovie::T7GFont: Glyph %d starts at %d but the current "
"offset is %d", i, offset, (int)stream.pos());
return false;
}
// Read the glyph information
Glyph *g = &_glyphs[i];
g->width = stream.readByte();
g->julia = stream.readByte();
// Read the pixels data into a dynamic array (we don't know its length)
Common::Array<byte> data;
data.reserve(300);
byte b = stream.readByte();
while (!stream.eos() && (b != 0xFF)) {
data.push_back(b);
b = stream.readByte();
}
// Verify the pixel data size
assert (data.size() % g->width == 0);
g->height = data.size() / g->width;
// Copy the pixel data into the definitive static array
g->pixels = new byte[data.size()];
memcpy(g->pixels, data.begin(), data.size());
// Update the max values
if (g->width > _maxWidth)
_maxWidth = g->width;
if (g->height > _maxHeight)
_maxHeight = g->height;
}
delete[] glyphOffsets;
return true;
}
void T7GFont::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 color) const {
// We ignore the color, as the font is already colored
const Glyph *glyph = getGlyph(chr);
const byte *src = glyph->pixels;
byte *target = (byte *)dst->getBasePtr(x, y);
for (int i = 0; i < glyph->height; i++) {
memcpy(target, src, glyph->width);
src += glyph->width;
target += dst->pitch;
}
}
const T7GFont::Glyph *T7GFont::getGlyph(uint32 chr) const {
assert (chr < 128);
byte numGlyph = _mapChar2Glyph[chr];
return &_glyphs[numGlyph];
}
} // End of Groovie namespace

62
engines/groovie/font.h Normal file
View File

@@ -0,0 +1,62 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_FONT_H
#define GROOVIE_FONT_H
#include "common/stream.h"
#include "graphics/font.h"
namespace Groovie {
class T7GFont : public Graphics::Font {
public:
T7GFont();
~T7GFont() override;
bool load(Common::SeekableReadStream &stream);
int getFontHeight() const override { return _maxHeight; }
int getMaxCharWidth() const override { return _maxWidth; }
int getCharWidth(uint32 chr) const override { return getGlyph(chr)->width; }
void drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 color) const override;
private:
int _maxHeight, _maxWidth;
struct Glyph {
Glyph() : pixels(0), width(0), height(0), julia(0) {}
~Glyph() { delete[] pixels; }
byte width;
byte height;
byte julia;
byte *pixels;
};
byte _mapChar2Glyph[128];
Glyph *_glyphs;
const Glyph *getGlyph(uint32 chr) const;
};
} // End of Groovie namespace
#endif // GROOVIE_FONT_H

View File

@@ -0,0 +1,200 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/graphics.h"
#include "groovie/groovie.h"
#include "common/rect.h"
#include "common/system.h"
#include "graphics/paletteman.h"
namespace Groovie {
GraphicsMan::GraphicsMan(GroovieEngine *vm) :
_vm(vm), _changed(false), _fading(0), _fadeStartTime(0) {
// Create the game surfaces
_foreground.create(640, 320, _vm->_pixelFormat);
_background.create(640, 320, _vm->_pixelFormat);
_savedground.create(640, 480, _vm->_pixelFormat);
}
GraphicsMan::~GraphicsMan() {
// Free the game surfaces
_foreground.free();
_background.free();
_savedground.free();
}
void GraphicsMan::update() {
if (_fading) {
// Set the start time
uint32 time = _vm->_system->getMillis() - _fadeStartTime;
// Scale the time
int step = (time * 20 << 3) / 1000;
if (step > 256) {
step = 256;
}
// Apply the current fading
applyFading(step);
// Check for the end
if (step == 256) {
_fading = 0;
// Clear the buffer when ending the fade out
if (_fading == 2)
_foreground.fillRect(Common::Rect(640, _foreground.h), 0);
}
}
// Update the screen if needed and reset the status
if (_changed) {
_vm->_system->updateScreen();
_changed = false;
}
}
void GraphicsMan::switchToFullScreen(bool fullScreen) {
// retain the image we currently have, Samantha's moves depend on this
_background.copyFrom(_foreground);
_foreground.free();
if (fullScreen) {
_foreground.create(640, 480, _vm->_pixelFormat);
_foreground.copyRectToSurface(_background, 0, 80, Common::Rect(0, 0, 640, 320));
_background.free();
_background.create(640, 480, _vm->_pixelFormat);
} else {
_foreground.create(640, 320, _vm->_pixelFormat);
_foreground.copyRectToSurface(_background, 0, 0, Common::Rect(0, 80, 640, 400));
_background.free();
_background.create(640, 320, _vm->_pixelFormat);
}
_changed = true;
}
void GraphicsMan::change() {
_changed = true;
}
void GraphicsMan::mergeFgAndBg() {
uint32 i;
byte *countf, *countb;
countf = (byte *)_foreground.getPixels();
countb = (byte *)_background.getPixels();
for (i = 640 * _foreground.h; i; i--) {
if (255 == *(countf)) {
*(countf) = *(countb);
}
countf++;
countb++;
}
}
void GraphicsMan::updateScreen(Graphics::Surface *source) {
if (!isFullScreen())
_vm->_system->copyRectToScreen(source->getPixels(), source->pitch, 0, 80, 640, 320);
else
_vm->_system->copyRectToScreen(source->getPixels(), source->pitch, 0, 0, 640, 480);
change();
}
void GraphicsMan::saveScreen() {
Graphics::Surface *screen = _vm->_system->lockScreen();
_vm->_graphicsMan->_savedground.copyFrom(screen->getSubArea(Common::Rect(0, 0, 640, 480)));
_vm->_system->unlockScreen();
}
void GraphicsMan::restoreScreen() {
_vm->_system->copyRectToScreen(_savedground.getPixels(), _savedground.pitch, 0, 0, 640, 480);
change();
}
bool GraphicsMan::isFading() {
return _fading;
}
void GraphicsMan::fadeIn(byte *pal) {
// Set the start time
_fadeStartTime = _vm->_system->getMillis();
// Copy the target palette
memcpy(_paletteFull, pal, 3 * 256);
// Set the current fading
_fading = 1;
// Apply a black palette right now
applyFading(0);
}
void GraphicsMan::fadeOut() {
// Set the start time
_fadeStartTime = _vm->_system->getMillis();
// Get the current palette
_vm->_system->getPaletteManager()->grabPalette(_paletteFull, 0, 256);
// Set the current fading
_fading = 2;
}
void GraphicsMan::applyFading(int step) {
// Calculate the fade factor for the given step
int factorR = 0, factorG = 0, factorB = 0;
if (_fading == 1) {
// Fading in
factorR = (step << 2);
factorG = (step << 1);
factorB = step;
if (factorR > 256) factorR = 256;
if (factorG > 256) factorG = 256;
if (factorB > 256) factorB = 256;
} else if (_fading == 2) {
// Fading out
factorR = 256 - step;
factorG = 256 - (step << 1);
if (factorR < 0) factorR = 0;
if (factorG < 0) factorG = 0;
factorB = factorG;
}
// Calculate the new palette
byte newpal[256 * 3];
for (int i = 0; i < 256; i++) {
newpal[(i * 3) + 0] = (_paletteFull[(i * 3) + 0] * factorR) / 256;
newpal[(i * 3) + 1] = (_paletteFull[(i * 3) + 1] * factorG) / 256;
newpal[(i * 3) + 2] = (_paletteFull[(i * 3) + 2] * factorB) / 256;
}
// Set the screen palette
_vm->_system->getPaletteManager()->setPalette(newpal, 0, 256);
// Request a screen update
change();
}
} // End of Groovie namespace

View File

@@ -0,0 +1,68 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_GRAPHICS_H
#define GROOVIE_GRAPHICS_H
#include "graphics/surface.h"
namespace Groovie {
class GroovieEngine;
class GraphicsMan {
public:
GraphicsMan(GroovieEngine *vm);
~GraphicsMan();
// Buffers
void update();
void change();
void mergeFgAndBg();
void switchToFullScreen(bool fullScreen);
bool isFullScreen() { return (_foreground.h == 480); }
void updateScreen(Graphics::Surface *source);
void saveScreen();
void restoreScreen();
Graphics::Surface _foreground; // The main surface that most things are drawn to
Graphics::Surface _background; // Used occasionally, mostly (only?) in puzzles
Graphics::Surface _savedground; // Buffer to save and restore the current screen. Used when opening the gamebook in 11H
// Palette fading
bool isFading();
void fadeIn(byte *pal);
void fadeOut();
private:
GroovieEngine *_vm;
bool _changed;
// Palette fading
void applyFading(int step);
int _fading;
byte _paletteFull[256 * 3];
uint32 _fadeStartTime;
};
} // End of Groovie namespace
#endif // GROOVIE_GRAPHICS_H

515
engines/groovie/groovie.cpp Normal file
View File

@@ -0,0 +1,515 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/groovie.h"
#include "groovie/cursor.h"
#include "groovie/detection.h"
#include "groovie/graphics.h"
#include "groovie/script.h"
#include "groovie/music.h"
#include "groovie/resource.h"
#include "groovie/video/vdx.h"
#ifdef ENABLE_GROOVIE2
#include "groovie/video/roq.h"
#endif
#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/compression/stuffit.h"
#include "common/textconsole.h"
#include "backends/audiocd/audiocd.h"
#include "engines/util.h"
#include "graphics/fontman.h"
#include "audio/mixer.h"
namespace Groovie {
const int GroovieEngine::AUTOSAVE_SLOT = MAX_SAVES - 1;
GroovieEngine::GroovieEngine(OSystem *syst, const GroovieGameDescription *gd) :
Engine(syst), _gameDescription(gd), _script(nullptr),
_resMan(nullptr), _grvCursorMan(nullptr), _videoPlayer(nullptr), _musicPlayer(nullptr),
_graphicsMan(nullptr), _macResFork(nullptr), _waitingForInput(false), _font(nullptr),
_spookyMode(false) {
// Adding the default directories
const Common::FSNode gameDataDir(ConfMan.getPath("path"));
SearchMan.addSubDirectoryMatching(gameDataDir, "groovie");
SearchMan.addSubDirectoryMatching(gameDataDir, "media");
SearchMan.addSubDirectoryMatching(gameDataDir, "system");
SearchMan.addSubDirectoryMatching(gameDataDir, "MIDI");
_modeSpeed = kGroovieSpeedNormal;
if (ConfMan.hasKey("fast_movie_speed") && ConfMan.getBool("fast_movie_speed"))
_modeSpeed = kGroovieSpeedFast;
}
GroovieEngine::~GroovieEngine() {
// Delete the remaining objects
delete _resMan;
delete _grvCursorMan;
delete _videoPlayer;
delete _musicPlayer;
delete _graphicsMan;
delete _script;
delete _macResFork;
}
Common::Error GroovieEngine::run() {
if (_gameDescription->version == kGroovieT11H && getPlatform() == Common::kPlatformMacintosh) {
// Load the Mac installer with the lowest priority (in case the user has installed
// the game and has the MIDI folder present; faster to just load them)
Common::Archive *archive = Common::createStuffItArchive("The 11th Hour Installer", true);
if (archive)
SearchMan.add("The 11th Hour Installer", archive);
}
// TODO: remove this default logging when we're done testing?
DebugMan.enableDebugChannel(kDebugScript);
DebugMan.enableDebugChannel(kDebugScriptvars);
DebugMan.enableDebugChannel(kDebugLogic);
DebugMan.enableDebugChannel(kDebugVideo);
if (gDebugLevel < 0)
gDebugLevel = 0;
_script = new Script(this, _gameDescription->version);
// Initialize the graphics
switch (_gameDescription->version) {
case kGroovieT11H:
case kGroovieCDY:
case kGroovieUHP:
case kGroovieTLC: {
// Request the mode with the highest precision available
Graphics::PixelFormat format(4, 8, 8, 8, 8, 24, 16, 8, 0);
initGraphics(640, 480, &format);
if (_system->getScreenFormat() != format)
return Common::kUnsupportedColorMode;
// Save the enabled mode
_pixelFormat = format;
break;
}
case kGroovieT7G:
initGraphics(640, 480);
_pixelFormat = Graphics::PixelFormat::createFormatCLUT8();
break;
default:
error("GROOVIE: Unknown Game version. groovie.cpp:run()");
}
// Create debugger. It requires GFX to be initialized
Debugger *debugger = new Debugger(this);
setDebugger(debugger);
_script->setDebugger(debugger);
// Create the graphics manager
_graphicsMan = new GraphicsMan(this);
// Create the resource and cursor managers and the video player
// Prepare the font too
switch (_gameDescription->version) {
case kGroovieT7G:
if (getPlatform() == Common::kPlatformMacintosh) {
_macResFork = new Common::MacResManager();
if (!_macResFork->open(_gameDescription->desc.filesDescriptions[0].fileName))
error("Could not open %s as a resource fork", _gameDescription->desc.filesDescriptions[0].fileName);
// The Macintosh release used system fonts. We use GUI fonts.
_font = FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
} else {
Common::File fontfile;
if (!fontfile.open("sphinx.fnt")) {
error("Couldn't open sphinx.fnt");
return Common::kNoGameDataFoundError;
} else if (!_sphinxFont.load(fontfile)) {
error("Error loading sphinx.fnt");
return Common::kUnknownError;
}
fontfile.close();
_font = &_sphinxFont;
}
_resMan = new ResMan_t7g(_macResFork);
_grvCursorMan = new GrvCursorMan_t7g(_system, _macResFork);
_videoPlayer = new VDXPlayer(this);
break;
case kGroovieT11H:
case kGroovieCDY:
case kGroovieUHP:
case kGroovieTLC:
_font = FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
_resMan = new ResMan_v2();
_grvCursorMan = new GrvCursorMan_v2(_system);
#ifdef ENABLE_GROOVIE2
_videoPlayer = new ROQPlayer(this);
_soundQueue.setVM(this);
#endif
break;
default:
error("GROOVIE: Unknown Game version. groovie.cpp:run()");
}
switch (_gameDescription->version) {
case kGroovieT7G:
// Detect ScummVM Music Enhancement Project presence (T7G only)
if (Common::File::exists("gu16.ogg")) {
// Load player for external files
_musicPlayer = new MusicPlayerIOS(this);
break;
}
// else, fall through
case kGroovieT11H:
// Create the music player
switch (getPlatform()) {
case Common::kPlatformMacintosh:
if (_gameDescription->version == kGroovieT7G)
_musicPlayer = new MusicPlayerMac_t7g(this);
else
_musicPlayer = new MusicPlayerMac_v2(this);
break;
case Common::kPlatformIOS:
_musicPlayer = new MusicPlayerIOS(this);
break;
default:
_musicPlayer = new MusicPlayerXMI(this, _gameDescription->version == kGroovieT7G ? "fat" : "sample");
break;
}
break;
case kGroovieCDY:
case kGroovieUHP:
_musicPlayer = new MusicPlayerClan(this);
break;
case kGroovieTLC:
_musicPlayer = new MusicPlayerTlc(this);
break;
}
// Load volume levels
syncSoundSettings();
// Get the name of the main script
Common::String filename;
if (_gameDescription->version == kGroovieT7G) {
filename = "script.grv";
// Run The 7th Guest's demo if requested
if (ConfMan.hasKey("demo_mode") && ConfMan.getBool("demo_mode"))
filename = "demo.grv";
} else {
filename = _gameDescription->desc.filesDescriptions[0].fileName;
}
// Check the script file extension
if (!filename.hasSuffixIgnoreCase(".grv")) {
error("%s isn't a valid script filename", filename.c_str());
return Common::kUnknownError;
}
// Load the script
if (!_script->loadScript(filename)) {
error("Couldn't load the script file %s", filename.c_str());
return Common::kUnknownError;
}
// Should I load a saved game?
if (ConfMan.hasKey("save_slot")) {
// Get the requested slot
int slot = ConfMan.getInt("save_slot");
_script->directGameLoad(slot);
}
// Game timer counter
int tmr = 0;
// Check that the game files and the audio tracks aren't together run from
// the same cd
if (getPlatform() != Common::kPlatformIOS && _gameDescription->version == kGroovieT7G) {
if (!existExtractedCDAudioFiles()
&& !isDataAndCDAudioReadFromSameCD()) {
warnMissingExtractedCDAudio();
}
_system->getAudioCDManager()->open();
}
while (!shouldQuit()) {
// Handle input
Common::Event ev;
while (_eventMan->pollEvent(ev)) {
switch (ev.type) {
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
_script->setAction(ev.customType);
break;
case Common::EVENT_KEYDOWN:
// Send the event to the scripts
_script->setKbdChar(ev.kbd.ascii);
// Continue the script execution to handle the key
_waitingForInput = false;
break;
case Common::EVENT_MAINMENU:
// Closing the GMM
case Common::EVENT_MOUSEMOVE:
// Continue the script execution, the mouse pointer
// may fall inside a different hotspot now
_waitingForInput = false;
break;
case Common::EVENT_LBUTTONDOWN:
// Send the event to the scripts
_script->setMouseClick(1);
// Continue the script execution to handle
// the click
_waitingForInput = false;
break;
case Common::EVENT_RBUTTONDOWN:
// Send the event to the scripts (to skip the video)
_script->setMouseClick(2);
break;
case Common::EVENT_QUIT:
quitGame();
break;
default:
break;
}
}
// The event loop may have triggered the quit status. In this case,
// stop the execution.
if (shouldQuit()) {
continue;
}
if (_graphicsMan->isFading()) {
// We're waiting for a fading to end, let the CPU rest
// for a while and continue
_system->delayMillis(10);
} else {
if (_waitingForInput) {
// Still waiting for input, just update the mouse, game timer and then wait a bit more
_grvCursorMan->animate();
_system->updateScreen();
// Wait a little bit between increments. While mouse is moving, this triggers
// only negligably slower.
if (tmr >= 500) {
_script->timerTick();
tmr = 0;
}
_system->delayMillis(10);
tmr += 10;
// the script doesn't unset _waitingForInput
// so we unset it here in order to let the script run as many steps as it needs to
// this makes the game more responsive
_waitingForInput = false;
}
// Everything's fine, execute another script step
_script->step();
}
// Update the screen if required
_graphicsMan->update();
_soundQueue.tick();
}
return Common::kNoError;
}
void GroovieEngine::pauseEngineIntern(bool pause) {
Engine::pauseEngineIntern(pause);
if (_musicPlayer)
_musicPlayer->pause(pause);
}
Common::Platform GroovieEngine::getPlatform() const {
return _gameDescription->desc.platform;
}
EngineVersion GroovieEngine::getEngineVersion() const {
return _gameDescription->version;
}
bool GroovieEngine::hasFeature(EngineFeature f) const {
return
(f == kSupportsReturnToLauncher) ||
(f == kSupportsSavingDuringRuntime) ||
(f == kSupportsLoadingDuringRuntime);
}
bool GroovieEngine::canLaunchLoad() const {
if (_gameDescription->desc.guiOptions == nullptr)
return false;
return strstr(_gameDescription->desc.guiOptions, GUIO_NOLAUNCHLOAD) != nullptr;
}
bool GroovieEngine::isDemo() const {
return _gameDescription->desc.flags & ADGF_DEMO;
}
void GroovieEngine::syncSoundSettings() {
Engine::syncSoundSettings();
bool mute = ConfMan.getBool("mute");
// Set the music volume
_musicPlayer->setUserVolume(mute ? 0 : ConfMan.getInt("music_volume"));
// Videos just contain one digital audio track, which can be used for
// both SFX or Speech, but the engine doesn't know what they contain, so
// we have to use just one volume setting for videos.
// We use "speech" because most users will want to change the videos
// volume when they can't hear the speech because of the music.
_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType,
mute ? 0 : ConfMan.getInt("speech_volume"));
}
bool GroovieEngine::canLoadGameStateCurrently(Common::U32String *msg) {
// TODO: verify the engine has been initialized
if (isDemo())
return false;
if (_script)
return true;
else
return false;
}
bool GroovieEngine::canSaveGameStateCurrently(Common::U32String *msg) {
// TODO: verify the engine has been initialized
if (isDemo())
return false;
if (_script)
return _script->canDirectSave();
else
return false;
}
Common::Error GroovieEngine::loadGameState(int slot) {
_script->directGameLoad(slot);
// TODO: Use specific error codes
return Common::kNoError;
}
Common::Error GroovieEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
_script->directGameSave(slot,desc);
// TODO: Use specific error codes
return Common::kNoError;
}
int GroovieEngine::getAutosaveSlot() const {
return AUTOSAVE_SLOT;
}
void GroovieEngine::waitForInput() {
_waitingForInput = true;
}
SoundEffectQueue::SoundEffectQueue() {
_vm = nullptr;
_player = nullptr;
_file = nullptr;
}
void SoundEffectQueue::setVM(GroovieEngine *vm) {
_vm = vm;
#ifdef ENABLE_GROOVIE2
_player = new ROQSoundPlayer(vm);
#endif
}
void SoundEffectQueue::queue(Common::SeekableReadStream *soundfile, uint32 loops) {
if (_queue.size() > 20) {
stopAll();
}
_queue.push({soundfile, loops});
for (uint32 i = 1; i < loops; i++) {
_queue.push({soundfile, loops});
}
tick();
}
void SoundEffectQueue::tick() {
#ifdef ENABLE_GROOVIE2
if (_file && !_player->playFrame()) {
_vm->_script->setBitFlag(0, true);
return;
}
if (_queue.size() == 0) {
deleteFile();
return;
}
SoundQueueEntry entry = _queue.front();
if (entry._loops != 0 || _queue.size() > 1) {
_queue.pop();
}
if (_file != entry._file) {
deleteFile();
}
_file = entry._file;
_vm->_script->setBitFlag(0, true);
_file->seek(0);
_player->load(_file, 0);
_player->playFrame();
if (_player->isFastForwarding()) {
stopAll();
}
#endif
}
void SoundEffectQueue::deleteFile() {
if (_file) {
delete _file;
_file = nullptr;
_vm->_script->setBitFlag(0, false);
}
}
void SoundEffectQueue::stopAll() {
if (_file && _player) {
_player->stopAudioStream();
}
_queue.clear();
deleteFile();
}
} // End of namespace Groovie

171
engines/groovie/groovie.h Normal file
View File

@@ -0,0 +1,171 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_GROOVIE_H
#define GROOVIE_GROOVIE_H
#include "groovie/debug.h"
#include "groovie/font.h"
#include "engines/engine.h"
#include "graphics/pixelformat.h"
#include "groovie/detection.h"
namespace Common {
class MacResManager;
}
/**
* This is the namespace of the Groovie engine.
*
* Status of this engine: This engine supports both versions of the Groovie
* game engine. The 7th Guest uses the first revision of Groovie, and is
* now fully completable. All remaining Groovie games use V2 of the engine,
* which is under slow development.
*
* Games using this engine:
* - The 7th Guest (completable)
* - The 11th Hour
* - Clandestiny
* - Uncle Henry's Playhouse
* - Tender Loving Care
*/
namespace Groovie {
class GraphicsMan;
class GrvCursorMan;
class MusicPlayer;
class ResMan;
class Script;
class VideoPlayer;
enum DebugLevels {
kDebugVideo = 1,
kDebugResource,
kDebugScript,
kDebugUnknown,
kDebugHotspots,
kDebugCursor,
kDebugMIDI,
kDebugScriptvars,
kDebugLogic,
kDebugFast,
//kDebugTlcGame,
};
/**
* This enum reflects the available movie speed settings:
* - Normal: play videos at a normal speed
* - Fast: play videos with audio at a fast speed. Videos without audio,
* like teeth animations, are played at their regular speed to avoid
* audio sync issues
*/
enum GameSpeed {
kGroovieSpeedNormal,
kGroovieSpeedFast
};
enum GROOVIEAction {
kActionNone,
kActionSkip
};
#define MAX_SAVES 25
struct GroovieGameDescription;
struct SoundQueueEntry {
Common::SeekableReadStream *_file;
uint32 _loops;
};
class SoundEffectQueue {
public:
SoundEffectQueue();
void setVM(GroovieEngine *vm);
void queue(Common::SeekableReadStream *soundfile, uint32 loops);
void tick();
void stopAll();
protected:
void deleteFile();
VideoPlayer *_player;
GroovieEngine *_vm;
Common::Queue<SoundQueueEntry> _queue;
Common::SeekableReadStream *_file;
};
class GroovieEngine : public Engine {
public:
static const int AUTOSAVE_SLOT;
GroovieEngine(OSystem *syst, const GroovieGameDescription *gd);
~GroovieEngine() override;
Common::Platform getPlatform() const;
EngineVersion getEngineVersion() const;
int getAutosaveSlot() const override;
bool canLaunchLoad() const;
bool isDemo() const;
protected:
// Engine APIs
Common::Error run() override;
void pauseEngineIntern(bool pause) override;
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
Common::Error loadGameState(int slot) override;
Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave = false) override;
void syncSoundSettings() override;
public:
void waitForInput();
bool isWaitingForInput() { return _waitingForInput; }
Graphics::PixelFormat _pixelFormat;
bool _spookyMode;
Script *_script;
ResMan *_resMan;
GrvCursorMan *_grvCursorMan;
VideoPlayer *_videoPlayer;
SoundEffectQueue _soundQueue;
MusicPlayer *_musicPlayer;
GraphicsMan *_graphicsMan;
const Graphics::Font *_font;
Common::MacResManager *_macResFork;
GameSpeed _modeSpeed;
private:
const GroovieGameDescription *_gameDescription;
bool _waitingForInput;
T7GFont _sphinxFont;
};
} // End of namespace Groovie
#endif // GROOVIE_GROOVIE_H

View File

@@ -0,0 +1,840 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/beehive.h"
namespace Groovie {
namespace {
extern const int8 beehiveLogicTable1[368];
extern const int8 beehiveLogicTable2[800];
}
void BeehiveGame::overrideClick(byte *vars) {
if (overrideIndex >= overrideMoves.size())
return;
int move = overrideMoves[overrideIndex];
vars[0] = move / 10;
vars[1] = move % 10;
}
void BeehiveGame::overrideMove(byte *vars) {
if (overrideIndex >= overrideMoves.size())
return;
int from = overrideMoves[overrideIndex++];
int to = overrideMoves[overrideIndex++];
vars[0] = from / 10;
vars[1] = from % 10;
vars[2] = to / 10;
vars[3] = to % 10;
}
void BeehiveGame::run(byte *scriptVariables) {
int8 *hexagons = (int8 *)scriptVariables + 25;
int8 *hexDifference = (int8 *)scriptVariables + 13;
byte op = scriptVariables[14] - 1;
enum kBeehiveColor {
kBeehiveColorYellow = -1,
kBeehiveColorRed = 1
};
debugC(1, kDebugLogic, "Beehive subop %d", op);
int8 v21, v22, v24;
int8 tempState[64];
// init hexDifference on every iteration
*hexDifference = 4;
switch (op) {
case 0: // Init board's hexagons
_maxDepth = 4;
memset(_beehiveState, 0, HEXCOUNT);
_beehiveState[0] = kBeehiveColorYellow;
_beehiveState[4] = kBeehiveColorRed;
_beehiveState[34] = kBeehiveColorYellow;
_beehiveState[60] = kBeehiveColorRed;
_beehiveState[56] = kBeehiveColorYellow;
_beehiveState[26] = kBeehiveColorRed;
return;
case 1:
memset(hexagons, 0, HEXCOUNT);
scriptVariables[85] = 0;
sub02(&v22, tempState);
if (v22) {
for (int i = 0; i < v22; i++)
scriptVariables[tempState[i] + 25] = kBeehiveColorRed;
} else {
*hexDifference = getHexDifference();
}
return;
case 2: // Player clicks on a honey-filled (source) hexagon
memset(hexagons, 0, HEXCOUNT);
scriptVariables[85] = 0;
//overrideClick(scriptVariables);
v24 = 10 * scriptVariables[0] + scriptVariables[1];
debugC(2, kDebugLogic, "Beehive player clicked %d", (int)v24);
selectSourceHexagon(v24, &v22, tempState);
for (int j = 0; j < v22; j++)
scriptVariables[tempState[j] + 25] = kBeehiveColorRed;
scriptVariables[v24 + 25] = kBeehiveColorRed;
return;
case 3: // Player moves into an empty (destination) hexagon
scriptVariables[24] = 1;
scriptVariables[4] = 2;
overrideMove(scriptVariables);
v24 = 10 * scriptVariables[0] + scriptVariables[1];
v22 = 10 * scriptVariables[2] + scriptVariables[3];
debugC(1, kDebugLogic, "Beehive player moved from %d, to %d", (int)v24, (int)v22);
sub16(v24, v22, hexDifference, (int8 *)scriptVariables + 16, (int8 *)scriptVariables + 17);
scriptVariables[15] = scriptVariables[16];
sub04(v24, v22, (int8 *)scriptVariables);
return;
case 4: // Stauf plays
scriptVariables[24] = 1;
scriptVariables[4] = 1;
calcStaufMove(&v24, &v22, hexDifference, &v21, (int8 *)scriptVariables + 16, (int8 *)scriptVariables + 17);
// Execute method tail
break;
case 5: // Calculate board state after every move
if (scriptVariables[24] == 1) {
scriptVariables[0] = scriptVariables[2];
scriptVariables[1] = scriptVariables[3];
scriptVariables[24] = 0;
}
if (scriptVariables[16]) {
int8 v16 = scriptVariables[16] - 1;
*hexDifference = 1;
scriptVariables[16] = v16;
v24 = 10 * scriptVariables[0] + scriptVariables[1];
int8 v23 = scriptVariables[v16 + 17];
scriptVariables[2] = v23 / 10;
scriptVariables[3] = v23 % 10;
sub04(v24, v23, (int8 *)scriptVariables);
} else {
*hexDifference = 4 - (scriptVariables[4] == 2 ? 1 : 0);
}
return;
case 6:
scriptVariables[24] = 1;
scriptVariables[4] = 2;
calcSamanthaMove(&v24, &v22, hexDifference, &v21, (int8 *)scriptVariables + 16, (int8 *)scriptVariables + 17);
// Execute method tail
break;
default:
return;
}
if (v24 == -1) {
*hexDifference = getHexDifference();
} else {
scriptVariables[0] = v24 / 10;
scriptVariables[1] = v24 % 10;
scriptVariables[2] = v22 / 10;
scriptVariables[3] = v22 % 10;
sub04(v24, v22, (int8 *)scriptVariables);
}
}
void BeehiveGame::sub02(int8 *a1, int8 *a2) {
int8 v9 = -1;
*a1 = 0;
while (findCell(_beehiveState, &v9, -1)) {
bool v3 = false;
for (int i = 0; i < 6; i++) {
if (v3)
break;
int8 move = beehiveLogicTable1[6 * v9 + i];
if (move != -1 && !_beehiveState[move]) {
a2[*a1] = v9;
v3 = true;
++*a1;
}
}
for (int i = 0; i < 12; i++) {
if (v3)
break;
int8 move = beehiveLogicTable2[12 * v9 + i];
if (move != -1 && !_beehiveState[move]) {
a2[*a1] = v9;
v3 = true;
++*a1;
}
}
}
if (!*a1) {
for (int i = 0; i < HEXCOUNT; ++i)
if (!_beehiveState[i])
_beehiveState[i] = 1;
}
}
void BeehiveGame::sub04(int8 a1, int8 a2, int8 *scriptVariables) {
int v3 = 0;
if (scriptVariables[13] == 1) {
if (beehiveLogicTable1[6 * a1] != a2) {
for (; v3 < 5; v3++) {
if (beehiveLogicTable1[6 * a1 + v3] == a2)
break;
}
}
int v7 = v3 + 12;
scriptVariables[5] = v7 / 10;
scriptVariables[6] = v7 % 10;
return;
}
scriptVariables[10] = 0;
scriptVariables[7] = 0;
if (beehiveLogicTable2[12 * a1] != a2) {
for (; v3 < 11; v3++) {
if (beehiveLogicTable2[12 * a1 + v3] == a2)
break;
}
}
scriptVariables[5] = v3 / 10;
int8 v5 = -1;
int8 v6 = -1;
scriptVariables[6] = v3 % 10;
switch (v3) {
case 0:
v6 = beehiveLogicTable1[6 * a1];
break;
case 1:
v5 = beehiveLogicTable1[6 * a1];
// fall through
case 2:
v6 = beehiveLogicTable1[6 * a1 + 1];
break;
case 3:
v5 = beehiveLogicTable1[6 * a1 + 1];
// fall through
case 4:
v6 = beehiveLogicTable1[6 * a1 + 2];
break;
case 5:
v5 = beehiveLogicTable1[6 * a1 + 2];
// fall through
case 6:
v6 = beehiveLogicTable1[6 * a1 + 3];
break;
case 7:
v5 = beehiveLogicTable1[6 * a1 + 3];
// fall through
case 8:
v6 = beehiveLogicTable1[6 * a1 + 4];
break;
case 9:
v5 = beehiveLogicTable1[6 * a1 + 4];
// fall through
case 10:
v6 = beehiveLogicTable1[6 * a1 + 5];
break;
case 11:
v6 = beehiveLogicTable1[6 * a1 + 5];
v5 = beehiveLogicTable1[6 * a1];
break;
default:
v6 = 0;
break;
}
int8 v4 = 0;
if (v5 != -1)
v4 = _beehiveState[v5];
if (_beehiveState[v6]) {
scriptVariables[8] = v6 / 10;
scriptVariables[9] = v6 % 10;
scriptVariables[7] = 2 - (_beehiveState[v6] == 1 ? 1 : 0);
}
if (v4) {
scriptVariables[11] = v5 / 10;
scriptVariables[12] = v5 % 10;
scriptVariables[10] = 2 - (v4 == 1 ? 1 : 0);
}
}
void BeehiveGame::calcSamanthaMove(int8 *a1, int8 *a2, int8 *a3, int8 *a4, int8 *a5, int8 *a6) {
int8 params[4];
*a4 = 0;
_maxDepth = 5;// in the original game Samantha did 4 like Stauf
if (calcMove(_beehiveState, -125, -1, _maxDepth, 0, params) == 125
&& (*a4 = 1, calcMove(_beehiveState, -125, -1, _maxDepth, 1, params) == 125)) {
*a1 = -1;
*a2 = -1;
for (int i = 0; i < HEXCOUNT; ++i) {
if (!_beehiveState[i])
_beehiveState[i] = 1;
}
} else {
*a1 = params[1];
*a2 = params[2];
*a3 = params[0];
sub17(_beehiveState, -1, params, a5, a6);
}
}
void BeehiveGame::calcStaufMove(int8 *a1, int8 *a2, int8 *a3, int8 *a4, int8 *a5, int8 *a6) {
int8 params[4];
*a4 = 0;
_maxDepth = 4;
if (_easierAi) {
int numPieces = 0;
for (int i = 0; i < HEXCOUNT; ++i)
numPieces += _beehiveState[i] != 0;
if (numPieces < 16)
_maxDepth = 3;
else
_maxDepth = 1;
}
if (calcMove(_beehiveState, 125, 1, _maxDepth, 0, params) == -125
&& (*a4 = 1, calcMove(_beehiveState, 125, 1, _maxDepth, 1, params) == -125)) {
*a1 = -1;
*a2 = -1;
for (int i = 0; i < HEXCOUNT; ++i) {
if (!_beehiveState[i])
_beehiveState[i] = -1;
}
} else {
*a1 = params[1];
*a2 = params[2];
*a3 = params[0];
sub17(_beehiveState, 1, params, a5, a6);
}
}
int8 BeehiveGame::sub11(int8 *beehiveState, int8 *a2, int8 *a3, int8 *a4, int8 a5, int8 a6, int8 *a7) {
if (*a2 == -1) {
if (!findCell(beehiveState, a2, a5))
return 0;
}
int8 v16 = 0;
while (1) {
while (1) {
if (v16)
return 1;
for (; *a3 < 6; (*a3)++) {
if (v16)
break;
int8 v9 = beehiveLogicTable1[6 * *a2 + *a3];
if (v9 != -1 && !beehiveState[v9] && *a2 < sub12(beehiveState, a5, v9, *a2)) {
v16 = 1;
*a7 = 1;
a7[1] = *a2;
a7[2] = beehiveLogicTable1[6 * *a2 + *a3];
}
}
if (*a4 >= 12)
break;
while (!v16) {
int8 v11 = beehiveLogicTable2[12 * *a2 + *a4];
if (v11 != -1
&& !beehiveState[v11]
&& !sub13(beehiveState, v11, a5)
&& sub13(beehiveState, beehiveLogicTable2[12 * *a2 + *a4], -a5)) {
int8 v12 = sub13(beehiveState, *a2, -a5);
int8 v13 = *a4 >> 1;
int8 v14 = ~(1 << v13) & v12;
if ((*a4 & 1) != 0) {
if (v13 == 5)
v14 &= ~1u;
else
v14 &= ~(1 << (v13 + 1));
}
if (!v14 || !sub13(beehiveState, *a2, a5) || a6) {
v16 = 1;
*a7 = 2;
a7[1] = *a2;
a7[2] = beehiveLogicTable2[12 * *a2 + *a4];
}
}
(*a4)++;
if (*a4 >= 12)
break;
}
if (*a4 >= 12)
break;
}
if (v16)
return 1;
if (!findCell(beehiveState, a2, a5))
return 0;
*a3 = 0;
*a4 = 0;
}
}
int8 BeehiveGame::sub12(int8 *beehiveState, int8 a2, int8 a3, int8 a4) {
int8 result = 125;
for (int i = 0; i < 6; i++) {
int8 v7 = beehiveLogicTable1[i + 6 * a3];
if (v7 != -1 && beehiveState[v7] == a2 && a4 != v7 && result > v7)
result = beehiveLogicTable1[i + 6 * a3];
}
return result;
}
int8 BeehiveGame::sub13(int8 *beehiveState, int8 a2, int8 a3) {
int result = 0;
for (int i = 0; i < 6; i++) {
int8 v5 = beehiveLogicTable1[6 * a2 + i];
if (v5 != -1 && beehiveState[v5] == a3)
result |= 1 << i;
}
return result;
}
void BeehiveGame::sub15(int8 *beehiveState, int8 a2, int8 *a3) {
beehiveState[a3[2]] = a2;
if (*a3 == 2)
beehiveState[a3[1]] = 0;
for (int i = 0; i < 6; ++i) {
int8 v4 = beehiveLogicTable1[6 * a3[2] + i];
if (v4 != -1) {
if (!(a2 + beehiveState[v4]))
beehiveState[v4] = a2;
}
}
}
void BeehiveGame::sub16(int8 a1, int8 a2, int8 *a3, int8 *a4, int8 *a5) {
int8 params[4];
params[0] = sub19(a1, a2);
params[1] = a1;
params[2] = a2;
*a3 = params[0];
sub17(_beehiveState, -1, params, a4, a5);
}
void BeehiveGame::sub17(int8 *beehiveState, int8 a2, int8 *a3, int8 *a4, int8 *a5) {
beehiveState[a3[2]] = a2;
if (*a3 == 2)
beehiveState[a3[1]] = 0;
*a4 = 0;
for (int i = 0; i < 6; i++) {
int8 v6 = beehiveLogicTable1[6 * a3[2] + i];
if (v6 != -1) {
if (!(a2 + beehiveState[v6])) {
beehiveState[v6] = a2;
a5[(*a4)++] = beehiveLogicTable1[6 * a3[2] + i];
}
}
}
}
void BeehiveGame::selectSourceHexagon(int8 a1, int8 *a2, int8 *a3) {
*a2 = 0;
for (int i = 0; i < 6; i++) {
int8 val = beehiveLogicTable1[6 * a1 + i];
if (val != -1 && !_beehiveState[val])
a3[(*a2)++] = val;
}
for (int i = 0; i < 12; i++) {
int val = beehiveLogicTable2[12 * a1 + i];
if (val != -1 && !_beehiveState[val])
a3[(*a2)++] = val;
}
}
int8 BeehiveGame::sub19(int8 a1, int8 a2) {
for (int i = 0; i < 6; i++)
if (beehiveLogicTable1[6 * a1 + i] == a2)
return 1;
return 2;
}
int8 BeehiveGame::calcMove(int8 *beehiveState, int8 a2, int8 a3, int8 depth, int a5, int8 *params) {
int8 paramsloc[4];
int8 params2[3];
int8 state[64];
if (!depth)
return getTotal(beehiveState);
int8 v7 = -125 * a3;
int8 v14 = 0;
int8 v13 = 0;
int8 v15 = -1;
if (sub11(beehiveState, &v15, &v14, &v13, a3, a5, params2)) {
do {
for (int i = 0; i < HEXCOUNT; i++)
state[i] = beehiveState[i];
sub15(state, a3, params2);
int8 v8 = calcMove(state, v7, -a3, depth - 1, a5, paramsloc);
if (a3 <= 0) {
if (v8 < v7) {
params[0] = params2[0];
params[1] = params2[1];
params[2] = params2[2];
v7 = v8;
}
if (a2 >= v7)
return v7;
} else {
if (v8 > v7) {
params[0] = params2[0];
params[1] = params2[1];
params[2] = params2[2];
v7 = v8;
}
if (a2 <= v7)
return v7;
}
} while (sub11(beehiveState, &v15, &v14, &v13, a3, a5, params2));
}
if (depth < _maxDepth && -125 * a3 == v7)
return getTotal(beehiveState);
else
return v7;
}
int8 BeehiveGame::getHexDifference() {
return (getTotal(_beehiveState) >= 0 ? 1 : 0) + 5;
}
int8 BeehiveGame::getTotal(int8 *hexagons) {
int8 result = 0;
for (int i = 0; i < HEXCOUNT; i++)
result += hexagons[i];
return result;
}
int8 BeehiveGame::findCell(int8 *beehiveState, int8 *pos, int8 key) {
for (int i = *pos + 1; i < HEXCOUNT; i++) {
if (beehiveState[i] == key) {
*pos = i;
return 1;
}
}
return 0;
}
namespace {
const int8 beehiveLogicTable1[368] = {
-1, 5, 6, 1, -1, -1,
0, 6, 7, 2, -1, -1,
1, 7, 8, 3, -1, -1,
2, 8, 9, 4, -1, -1,
3, 9, 10, -1, -1, -1,
-1, 11, 12, 6, 0, -1,
5, 12, 13, 7, 1, 0,
6, 13, 14, 8, 2, 1,
7, 14, 15, 9, 3, 2,
8, 15, 16, 10, 4, 3,
9, 16, 17, -1, -1, 4,
-1, 18, 19, 12, 5, -1,
11, 19, 20, 13, 6, 5,
12, 20, 21, 14, 7, 6,
13, 21, 22, 15, 8, 7,
14, 22, 23, 16, 9, 8,
15, 23, 24, 17, 10, 9,
16, 24, 25, -1, -1, 10,
-1, 26, 27, 19, 11, -1,
18, 27, 28, 20, 12, 11,
19, 28, 29, 21, 13, 12,
20, 29, 30, 22, 14, 13,
21, 30, 31, 23, 15, 14,
22, 31, 32, 24, 16, 15,
23, 32, 33, 25, 17, 16,
24, 33, 34, -1, -1, 17,
-1, -1, 35, 27, 18, -1,
26, 35, 36, 28, 19, 18,
27, 36, 37, 29, 20, 19,
28, 37, 38, 30, 21, 20,
29, 38, 39, 31, 22, 21,
30, 39, 40, 32, 23, 22,
31, 40, 41, 33, 24, 23,
32, 41, 42, 34, 25, 24,
33, 42, -1, -1, -1, 25,
-1, -1, 43, 36, 27, 26,
35, 43, 44, 37, 28, 27,
36, 44, 45, 38, 29, 28,
37, 45, 46, 39, 30, 29,
38, 46, 47, 40, 31, 30,
39, 47, 48, 41, 32, 31,
40, 48, 49, 42, 33, 32,
41, 49, -1, -1, 34, 33,
-1, -1, 50, 44, 36, 35,
43, 50, 51, 45, 37, 36,
44, 51, 52, 46, 38, 37,
45, 52, 53, 47, 39, 38,
46, 53, 54, 48, 40, 39,
47, 54, 55, 49, 41, 40,
48, 55, -1, -1, 42, 41,
-1, -1, 56, 51, 44, 43,
50, 56, 57, 52, 45, 44,
51, 57, 58, 53, 46, 45,
52, 58, 59, 54, 47, 46,
53, 59, 60, 55, 48, 47,
54, 60, -1, -1, 49, 48,
-1, -1, -1, 57, 51, 50,
56, -1, -1, 58, 52, 51,
57, -1, -1, 59, 53, 52,
58, -1, -1, 60, 54, 53,
59, -1, -1, -1, 55, 54,
0, 0
};
const int8 beehiveLogicTable2[800] = {
-1, -1, 11, 12, 13, 7, 2, -1, -1, -1, -1, -1,
-1, 5, 12, 13, 14, 8, 3, -1, -1, -1, -1, -1,
0, 6, 13, 14, 15, 9, 4, -1, -1, -1, -1, -1,
1, 7, 14, 15, 16, 10, -1, -1, -1, -1, -1, -1,
2, 8, 15, 16, 17, -1, -1, -1, -1, -1, -1, -1,
-1, -1, 18, 19, 20, 13, 7, 1, -1, -1, -1, -1,
-1, 11, 19, 20, 21, 14, 8, 2, -1, -1, -1, -1,
5, 12, 20, 21, 22, 15, 9, 3, -1, -1, -1, 0,
6, 13, 21, 22, 23, 16, 10, 4, -1, -1, -1, 1,
7, 14, 22, 23, 24, 17, -1, -1, -1, -1, -1, 2,
8, 15, 23, 24, 25, -1, -1, -1, -1, -1, -1, 3,
-1, -1, 26, 27, 28, 20, 13, 6, 0, -1, -1, -1,
-1, 18, 27, 28, 29, 21, 14, 7, 1, 0, -1, -1,
11, 19, 28, 29, 30, 22, 15, 8, 2, 1, 0, 5,
12, 20, 29, 30, 31, 23, 16, 9, 3, 2, 1, 6,
13, 21, 30, 31, 32, 24, 17, 10, 4, 3, 2, 7,
14, 22, 31, 32, 33, 25, -1, -1, -1, 4, 3, 8,
15, 23, 32, 33, 34, -1, -1, -1, -1, -1, 4, 9,
-1, -1, -1, 35, 36, 28, 20, 12, 5, -1, -1, -1,
-1, 26, 35, 36, 37, 29, 21, 13, 6, 5, -1, -1,
18, 27, 36, 37, 38, 30, 22, 14, 7, 6, 5, 11,
19, 28, 37, 38, 39, 31, 23, 15, 8, 7, 6, 12,
20, 29, 38, 39, 40, 32, 24, 16, 9, 8, 7, 13,
21, 30, 39, 40, 41, 33, 25, 17, 10, 9, 8, 14,
22, 31, 40, 41, 42, 34, -1, -1, -1, 10, 9, 15,
23, 32, 41, 42, -1, -1, -1, -1, -1, -1, 10, 16,
-1, -1, -1, -1, 43, 36, 28, 19, 11, -1, -1, -1,
-1, -1, -1, 43, 44, 37, 29, 20, 12, 11, -1, -1,
26, 35, 43, 44, 45, 38, 30, 21, 13, 12, 11, 18,
27, 36, 44, 45, 46, 39, 31, 22, 14, 13, 12, 19,
28, 37, 45, 46, 47, 40, 32, 23, 15, 14, 13, 20,
29, 38, 46, 47, 48, 41, 33, 24, 16, 15, 14, 21,
30, 39, 47, 48, 49, 42, 34, 25, 17, 16, 15, 22,
31, 40, 48, 49, -1, -1, -1, -1, -1, 17, 16, 23,
32, 41, 49, -1, -1, -1, -1, -1, -1, -1, 17, 24,
-1, -1, -1, -1, 50, 44, 37, 28, 19, 18, -1, -1,
-1, -1, -1, 50, 51, 45, 38, 29, 20, 19, 18, 26,
35, 43, 50, 51, 52, 46, 39, 30, 21, 20, 19, 27,
36, 44, 51, 52, 53, 47, 40, 31, 22, 21, 20, 28,
37, 45, 52, 53, 54, 48, 41, 32, 23, 22, 21, 29,
38, 46, 53, 54, 55, 49, 42, 33, 24, 23, 22, 30,
39, 47, 54, 55, -1, -1, -1, 34, 25, 24, 23, 31,
40, 48, 55, -1, -1, -1, -1, -1, -1, 25, 24, 32,
-1, -1, -1, -1, 56, 51, 45, 37, 28, 27, 26, -1,
-1, -1, -1, 56, 57, 52, 46, 38, 29, 28, 27, 35,
43, 50, 56, 57, 58, 53, 47, 39, 30, 29, 28, 36,
44, 51, 57, 58, 59, 54, 48, 40, 31, 30, 29, 37,
45, 52, 58, 59, 60, 55, 49, 41, 32, 31, 30, 38,
46, 53, 59, 60, -1, -1, -1, 42, 33, 32, 31, 39,
47, 54, 60, -1, -1, -1, -1, -1, 34, 33, 32, 40,
-1, -1, -1, -1, -1, 57, 52, 45, 37, 36, 35, -1,
-1, -1, -1, -1, -1, 58, 53, 46, 38, 37, 36, 43,
50, 56, -1, -1, -1, 59, 54, 47, 39, 38, 37, 44,
51, 57, -1, -1, -1, 60, 55, 48, 40, 39, 38, 45,
52, 58, -1, -1, -1, -1, -1, 49, 41, 40, 39, 46,
53, 59, -1, -1, -1, -1, -1, -1, 42, 41, 40, 47,
-1, -1, -1, -1, -1, -1, 58, 52, 45, 44, 43, -1,
-1, -1, -1, -1, -1, -1, 59, 53, 46, 45, 44, 50,
56, -1, -1, -1, -1, -1, 60, 54, 47, 46, 45, 51,
57, -1, -1, -1, -1, -1, -1, 55, 48, 47, 46, 52,
58, -1, -1, -1, -1, -1, -1, -1, 49, 48, 47, 53,
0, 0, 0, 0, 26, 18, 35, 11, 27, 43, 5, 19,
36, 50, 0, 12, 28, 44, 56, 6, 20, 37, 51, 1,
13, 29, 45, 57, 7, 21, 38, 52, 2, 14, 30, 46,
58, 8, 22, 39, 53, 3, 15, 31, 47, 59, 9, 23,
40, 54, 4, 16, 32, 48, 60, 10, 24, 41, 55, 17,
33, 49, 25, 42, 34, 0, 0, 0
};
} // End of anonymous namespace
void BeehiveGame::testGame(Common::Array<int> moves, bool playerWin) {
byte vars[1024];
memset(vars, 0, sizeof(vars));
int8 &hexDifference = ((int8 *)vars)[13];
byte &op = vars[14];// can't do the -1 with a reference
byte &counter = vars[16];
op = 1;
run(vars);
op = 2;
run(vars);
for (uint i = 0; i < moves.size(); i += 2) {
int from = moves[i];
int to = moves[i + 1];
op = 3;
vars[0] = from / 10;
vars[1] = from % 10;
run(vars);
op = 4;
vars[0] = from / 10;
vars[1] = from % 10;
vars[2] = to / 10;
vars[3] = to % 10;
run(vars);
while (counter) {
op = 6;
run(vars);
}
op = 6;
run(vars);
if (i + 2 < moves.size() && hexDifference == 6) {
error("early Stauf win");
} else if (i + 2 < moves.size() && hexDifference == 5) {
error("early player win");
}
op = 5;
run(vars);
while (counter) {
op = 6;
run(vars);
}
op = 6;
run(vars);
op = 2;
run(vars);
if (i + 2 < moves.size() && hexDifference == 6) {
error("early Stauf win");
} else if (i + 2 < moves.size() && hexDifference == 5) {
error("early player win");
}
}
if (playerWin && hexDifference != 5)
error("player didn't win");
if (playerWin == false && hexDifference != 6)
error("Stauf didn't win");
}
void BeehiveGame::tests() {
warning("starting BeehiveGame::tests()");
// 8 moves per line, in from and to pairs
// speedrun strat
testGame({
/**/ 34, 42, /**/ 56, 50, /**/ 50, 35, /**/ 42, 55, /**/ 34, 42, /**/ 42, 49, /**/ 35, 43, /**/ 43, 50,
/**/ 50, 51, /**/ 51, 52, /**/ 52, 53, /**/ 53, 54, /**/ 52, 57, /**/ 52, 46, /**/ 34, 25, /**/ 34, 24,
/**/ 25, 23, /**/ 46, 31, /**/ 31, 30, /**/ 52, 38, /**/ 29, 12, /**/ 31, 39, /**/ 35, 28, /**/ 49, 32,
/**/ 31, 40, /**/ 39, 47, /**/ 20, 19, /**/ 29, 37, /**/ 57, 58, /**/ 53, 46, /**/ 53, 52
}, true);
// losing game
testGame({
/**/ 34, 25, /**/ 25, 10, /**/ 34, 17, /**/ 0, 2, /**/ 56, 57, /**/ 57, 51, /**/ 51, 50, /**/ 51, 52,
/**/ 51, 44, /**/ 50, 43, /**/ 50, 35, /**/ 36, 38, /**/ 35, 37, /**/ 38, 39, /**/ 38, 29, /**/ 45, 58,
/**/ 58, 59, /**/ 57, 45, /**/ 44, 35, /**/ 35, 26, /**/ 46, 54, /**/ 59, 60, /**/ 59, 55, /**/ 55, 40,
/**/ 39, 23
}, false);
// copy the moveset from one of the tests to play it out yourself
overrideMoves = {};
overrideIndex = 0;
warning("finished BeehiveGame::tests()");
}
} // End of Groovie namespace

View File

@@ -0,0 +1,90 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_BEEHIVE_H
#define GROOVIE_LOGIC_BEEHIVE_H
#include "common/system.h"
namespace Groovie {
/*
* Beehive (Blood and Honey) puzzle (hs.grv)
*
* An infection-style game in which the player must cover more
* territory than the computer. It's similar to the microscope puzzle
* in the 7th Guest. The playfield is a honeycomb made of 61
* hexagons. The hexagons are numbered starting from the top-left
* corner, with a direction from bottom left to top right.
*/
class BeehiveGame {
public:
BeehiveGame(bool easierAi) {
#if 0
_easierAi = false;
tests();
#endif
_easierAi = easierAi;
}
~BeehiveGame() {}
void run(byte *scriptVariables);
private:
void sub02(int8 *a1, int8 *a2);
void sub04(int8 a1, int8 a2, int8 *scriptVariables);
void calcSamanthaMove(int8 *a1, int8 *a2, int8 *a3, int8 *a4, int8 *a5, int8 *a6);
void calcStaufMove(int8 *a1, int8 *a2, int8 *a3, int8 *a4, int8 *a5, int8 *a6);
int8 sub11(int8 *beehiveState, int8 *a2, int8 *a3, int8 *a4, int8 a5, int8 a6, int8 *a7);
int8 sub12(int8 *beehiveState, int8 a2, int8 a3, int8 a4);
int8 sub13(int8 *beehiveState, int8 a2, int8 a3);
void sub15(int8 *beehiveState, int8 a2, int8 *a3);
void sub16(int8 a1, int8 a2, int8 *a3, int8 *a4, int8 *a5);
void sub17(int8 *beehiveState, int8 a2, int8 *a3, int8 *a4, int8 *a5);
void selectSourceHexagon(int8 a1, int8 *a2, int8 *a3);
int8 sub19(int8 a1, int8 a2);
int8 getHexDifference();
int8 getTotal(int8 *hexagons);
int8 calcMove(int8 *beehiveState, int8 a2, int8 a3, int8 depth, int a5, int8 *a6);
int8 findCell(int8 *beehiveState, int8 *pos, int8 key);
void testGame(Common::Array<int> moves, bool playerWin);
void tests();
void overrideClick(byte *vars);
void overrideMove(byte *vars);
#define HEXCOUNT 61
int8 _beehiveState[HEXCOUNT];
Common::Array<int> overrideMoves;
uint overrideIndex;
bool _easierAi;
int8 _maxDepth;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_BEEHIVE_H

View File

@@ -0,0 +1,427 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/cake.h"
namespace Groovie {
/*
* T11hCake() constructor
* - Each spot on the board is part of multiple potential victory lines
* - The first x and y dimensions of the loops select the origin point of the line
* - The z is for the distance along that line
* - Then we push_back the id number of the line into the array at _map.indecies[x][y]
* - This is used in UpdateScores()
* .
* @see UpdateScores()
*/
CakeGame::CakeGame(bool easierAi) : _random("CakeGame") {
restart();
_map = {};
int numLines = 0;
// map all the lines with slope of (1, 0)
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x <= WIDTH - GOAL_LEN; x++) {
for (int z = 0; z < GOAL_LEN; z++) {
setLineNum(x + z, y, numLines);
}
numLines++;
}
}
// map all the lines with slope of (0, 1)
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y <= HEIGHT - GOAL_LEN; y++) {
for (int z = 0; z < GOAL_LEN; z++) {
setLineNum(x, y + z, numLines);
}
numLines++;
}
}
// map all the lines with slope of (1,1)
for (int y = 0; y <= HEIGHT - GOAL_LEN; y++) {
for (int x = 0; x <= WIDTH - GOAL_LEN; x++) {
for (int z = 0; z < GOAL_LEN; z++) {
setLineNum(x + z, y + z, numLines);
}
numLines++;
}
}
// map all the lines with slope of (1,-1)
for (int y = GOAL_LEN - 1; y < HEIGHT; y++) {
for (int x = 0; x <= WIDTH - GOAL_LEN; x++) {
for (int z = 0; z < GOAL_LEN; z++) {
setLineNum(x + z, y - z, numLines);
}
numLines++;
}
}
#if 0
_easierAi = false;
testCake();
#endif
_easierAi = easierAi;
}
void CakeGame::run(byte *scriptVariables) {
byte &lastMove = scriptVariables[1];
byte &winner = scriptVariables[3];
winner = 0;
if (lastMove == 8) {
restart();
return;
}
if (lastMove == 9) {
// samantha makes a move
lastMove = aiGetBestMove(6);
_hasCheated = true;
return;
}
if (isColumnFull(lastMove)) {
warning("player tried to place a bon bon in a full column, last_move: %d", (int)lastMove);
lastMove = 10;
return;
}
placeBonBon(lastMove);
winner = getWinner();
if (winner) {
return;
}
int depth = 4 + (_hasCheated == false);
if (_easierAi && _moveCount > 8)
depth = 3;
else if (_easierAi)
depth = 2;
lastMove = aiGetBestMove(depth);
placeBonBon(lastMove);
if (gameEnded()) {
winner = STAUF;
}
}
void CakeGame::restart() {
_playerProgress = {};
_staufProgress = {};
memset(_boardState, 0, sizeof(_boardState));
memset(_columnHeights, 0, sizeof(_columnHeights));
_moveCount = 0;
_hasCheated = false;
_playerProgress._score = NUM_LINES;
_staufProgress._score = NUM_LINES;
}
void CakeGame::setLineNum(uint x, uint y, uint index) {
assert(x < WIDTH);
assert(y < HEIGHT);
byte slot = _map.lengths[x][y]++;
assert(slot < GOAL_LEN * GOAL_LEN);
assert(index < NUM_LINES);
_map.indecies[x][y][slot] = index;
}
bool CakeGame::isColumnFull(byte column) {
return _columnHeights[column] >= HEIGHT;
}
CakeGame::PlayerProgress &CakeGame::getPlayerProgress(bool stauf) {
if (stauf)
return _staufProgress;
else
return _playerProgress;
}
/*
* UpdateScores()
* - Each PlayerProgress has an array of ints, _linesCounters[], where each entry maps to the ID of a line
* - When a bon bon is added to the board, we look up _map.lengths[x][y] and then loop through all the indecies for that point
* - Increment the PlayerProgress._linesCounters[id]
* - Calculate the scores proportional to the PlayerProgress._linesCounters[id]
* .
* .
*/
void CakeGame::updateScores(byte x, bool revert) {
bool stauf = _moveCount % 2;
PlayerProgress &pp = getPlayerProgress(stauf);
byte y = _columnHeights[x] - 1;
// get the number of potential victory lines that this spot exists in
int num_lines = _map.lengths[x][y];
for (int line = 0; line < num_lines; line++) {
// get the ID for this potential victory line
int index = _map.indecies[x][y][line];
int len = pp._linesCounters[index];
// add this new bon bon to the progress of this potential victory line, or remove in the case of revert
int mult = 1;// mult is used for multiplying the score gains, depends on revert
if (!revert)
pp._linesCounters[index]++;
else {
len = --pp._linesCounters[index];
mult = -1;
}
if (GOAL_LEN == len + 1) {
// that's a bingo
pp._score += WIN_SCORE * mult;
}
else {
PlayerProgress &pp2 = getPlayerProgress(!stauf);
int len2 = pp2._linesCounters[index];
if (len == 0) {
// we started a new line, take away the points the opponent had from this line since we ruined it for them
pp2._score -= (1 << (len2 & 31)) * mult;
}
if (len2 == 0) {
// the opponent doesn't have any spots in this line, so we get points for it
pp._score += (1 << (len & 31)) * mult;
}
}
}
}
void CakeGame::placeBonBon(byte x) {
byte y = _columnHeights[x]++;
if (_moveCount % 2)
_boardState[x][y] = STAUF;
else
_boardState[x][y] = PLAYER;
updateScores(x);
_moveCount++;
}
void CakeGame::revertMove(byte x) {
// PlaceBonBon in reverse, this is used for the AI's recursion rollback
_moveCount--;
updateScores(x, true);
byte y = --_columnHeights[x];
_boardState[x][y] = 0;
}
byte CakeGame::getWinner() {
if (_playerProgress._score >= WIN_SCORE)
return PLAYER;
if (_staufProgress._score >= WIN_SCORE)
return STAUF;
return 0;
}
bool CakeGame::gameEnded() {
if (getWinner())
return true;
if (_moveCount >= WIDTH * HEIGHT)
return true;
return false;
}
int CakeGame::getScoreDiff() {
if (_moveCount % 2)
return _staufProgress._score - _playerProgress._score;
else
return _playerProgress._score - _staufProgress._score;
}
int CakeGame::aiRecurse(int search_depth, int parent_score) {
int best_score = 0x7fffffff;
for (byte move = 0; move < WIDTH; move++) {
if (isColumnFull(move))
continue;
placeBonBon(move);
int score = getScoreDiff();
if (search_depth > 1 && !gameEnded())
score = aiRecurse(search_depth - 1, best_score);
revertMove(move);
if (score < best_score)
best_score = score;
if (-parent_score != best_score && parent_score <= -best_score)
break;
}
// we negate the score because from the perspective of our parent caller, this is his opponent's score
return -best_score;
}
byte CakeGame::aiGetBestMove(int search_depth) {
int best_move = 0xffff;
uint counter = 1;
for (int best_score = 0x7fffffff; best_score > 999999 && search_depth > 1; search_depth--) {
for (byte move = 0; move < WIDTH; move++) {
if (isColumnFull(move))
continue;
placeBonBon(move);
if (getWinner()) {
revertMove(move);
return move;
}
int score = aiRecurse(search_depth - 1, best_score);
revertMove(move);
if (score < best_score) {
counter = 1;
best_move = move;
best_score = score;
} else if (best_score == score) {
// rng is only used on moves with equal scores
counter++;
uint r = _random.getRandomNumber(1000000 - 1);
if (r * counter < 1000000) {
best_move = move;
}
}
}
}
return best_move;
}
void CakeGame::testCake() {
warning("starting CakeGame::testCake()");
uint32 oldSeed = _random.getSeed();
// test the draw condition, grouped by column
runCakeTestNoAi(/*move 1*/ "7777777" /*8*/ "6666666" /*15*/ "5555555" /*22*/ "34444444" /*30*/ "333333" /*36*/ "2222222" /*43*/ "01111111" /*51*/ "000000", false, true);
runCakeTest(9, "24223233041", true);
runCakeTest(1, "232232432445", false);
runCakeTest(123, "4453766355133466", false);
_random.setSeed(oldSeed);
warning("finished CakeGame::testCake()");
}
void CakeGame::runCakeTestNoAi(const char *moves, bool playerWin, bool draw = false) {
warning("starting runCakeTestNoAi(%s, %d)", moves, (int)playerWin);
restart();
for (int i = 0; moves[i]; i++) {
byte win = getWinner();
if (win) {
error("early win at %d, winner: %d", i, (int)win);
}
if (gameEnded()) {
error("early draw at %d", i);
}
byte move = moves[i] - '0';
placeBonBon(move);
}
byte winner = getWinner();
if (draw) {
if (winner != 0 || !gameEnded())
error("wasn't a draw! winner: %d, gameover: %d", (int)winner, (int)gameEnded());
} else if (playerWin && winner != PLAYER) {
error("player didn't win! winner: %d", (int)winner);
} else if (playerWin == false && winner != STAUF) {
error("Stauf didn't win! winner: %d", (int)winner);
}
warning("finished runCakeTestNoAi(%s, %d), winner: %d", moves, (int)playerWin, (int)winner);
}
void CakeGame::runCakeTest(uint seed, const char *moves, bool playerWin) {
warning("starting runCakeTest(%u, %s, %d)", seed, moves, (int)playerWin);
// first fill the board with the expected moves and test the win-detection function by itself without AI
runCakeTestNoAi(moves, playerWin);
restart();
byte vars[1024];
memset(vars, 0, sizeof(vars));
byte &lastMove = vars[1];
byte &winner = vars[3];
winner = 0;
lastMove = 8;
run(vars);
uint old_seed = _random.getSeed();
_random.setSeed(seed);
for (int i = 0; moves[i]; i += 2) {
if (winner != 0) {
error("early win at %d, winner: %d", i, (int)winner);
}
lastMove = moves[i] - '0';
byte stauf_move = moves[i + 1] - '0';
run(vars);
if (stauf_move < 8) {
if (winner == 2) {
error("early player win at %d", i);
}
if (stauf_move != lastMove) {
error("incorrect Stauf move, expected: %d, got: %d", (int)stauf_move, (int)lastMove);
}
} else if (winner != 2) {
error("missing Stauf move, last_move: %d", (int)lastMove);
} else
break;
}
if (playerWin && winner != 2) {
error("player didn't win! winner: %d", (int)winner);
} else if (playerWin == false && winner != 1) {
error("Stauf didn't win! winner: %d", (int)winner);
}
_random.setSeed(old_seed);
warning("finished runCakeTest(%u, %s, %d)", seed, moves, (int)playerWin);
}
} // End of Groovie namespace

View File

@@ -0,0 +1,98 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_CAKE_H
#define GROOVIE_LOGIC_CAKE_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Cake (Connect Four) puzzle in the dining room (tb.grv)
*/
class CakeGame {
public:
CakeGame(bool easierAi);
void run(byte *scriptVariables);
private:
static const int WIDTH = 8;
static const int HEIGHT = 7;
static const int GOAL_LEN = 4;
static const int WIN_SCORE = 1000000;//!< the number of points added for a connect four
static const byte STAUF = 1;
static const byte PLAYER = 2;
static const int NUM_LINES = 107;//!< how many potential victory lines there are
Common::RandomSource _random;
//! ID numbers for all of the potential victory lines for each spot on the board
struct LinesMappings {
byte lengths[WIDTH][HEIGHT];
byte indecies[WIDTH][HEIGHT][GOAL_LEN * GOAL_LEN];
};
//! how many points a player has, and their progress on potential victory lines
struct PlayerProgress {
int _score;
int _linesCounters[NUM_LINES];//!< how many pieces are claimed in each potential victory, links to LineMappings, an entry of 4 means that's a victory
};
PlayerProgress _playerProgress;
PlayerProgress _staufProgress;
byte _boardState[WIDTH][HEIGHT];//!< (0, 0) is the bottom left of the board
byte _columnHeights[WIDTH];
int _moveCount;
bool _hasCheated;
LinesMappings _map;//!< ID numbers for all of the potential victory lines for each spot on the board
bool _easierAi;
void restart();
void setLineNum(uint x, uint y, uint index);
bool isColumnFull(byte column);
PlayerProgress &getPlayerProgress(bool stauf);
void updateScores(byte x, bool revert = false);
void placeBonBon(byte x);
void revertMove(byte x);
byte getWinner();
bool gameEnded();
int getScoreDiff();
int aiRecurse(int search_depth, int parent_score);
byte aiGetBestMove(int search_depth);
void testCake();
void runCakeTest(uint seed, const char *moves, bool player_win);
void runCakeTestNoAi(const char *moves, bool player_win, bool draw);
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_CAKE_H

View File

@@ -0,0 +1,800 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/logic/cell.h"
#include "common/config-manager.h"
namespace Groovie {
CellGame::CellGame(bool easierAi) {
_startX = _startY = _endX = _endY = 255;
_stack_index = _boardStackPtr = 0;
_flag4 = false;
_flag2 = false;
_flag1 = false;
_coeff3 = 0;
_moveCount = 0;
_easierAi = easierAi;
}
byte CellGame::getStartX() {
if (_startX > BOARDSIZE) {
warning ("CellGame::getStartX: not calculated yet (%d)!", _startX);
return 0;
} else {
return _startX;
}
}
byte CellGame::getStartY() {
if (_startY > BOARDSIZE) {
warning ("CellGame::getStartY: not calculated yet (%d)!", _startY);
return 6;
} else {
return _startY;
}
}
byte CellGame::getEndX() {
if (_endX > BOARDSIZE) {
warning ("CellGame::getEndX: not calculated yet (%d)!", _endX);
return 1;
} else {
return _endX;
}
}
byte CellGame::getEndY() {
if (_endY > BOARDSIZE) {
warning ("CellGame::getEndY: not calculated yet (%d)!", _endY);
return 6;
} else {
return _endY;
}
}
CellGame::~CellGame() {
}
const int8 possibleMoves[][9] = {
{ 1, 7, 8, -1 },
{ 0, 2, 7, 8, 9, -1 },
{ 1, 3, 8, 9, 10, -1 },
{ 2, 4, 9, 10, 11, -1 },
{ 3, 5, 10, 11, 12, -1 },
{ 4, 6, 11, 12, 13, -1 }, // 5
{ 5, 12, 13, -1 },
{ 0, 1, 8, 14, 15, -1 },
{ 0, 1, 2, 7, 9, 14, 15, 16, -1 },
{ 1, 2, 3, 8, 10, 15, 16, 17, -1 },
{ 2, 3, 4, 9, 11, 16, 17, 18, -1 }, // 10
{ 3, 4, 5, 10, 12, 17, 18, 19, -1 },
{ 4, 5, 6, 11, 13, 18, 19, 20, -1 },
{ 5, 6, 12, 19, 20, -1 },
{ 7, 8, 15, 21, 22, -1 },
{ 7, 8, 9, 14, 16, 21, 22, 23, -1 }, // 15
{ 8, 9, 10, 15, 17, 22, 23, 24, -1 },
{ 9, 10, 11, 16, 18, 23, 24, 25, -1 },
{ 10, 11, 12, 17, 19, 24, 25, 26, -1 },
{ 11, 12, 13, 18, 20, 25, 26, 27, -1 },
{ 12, 13, 19, 26, 27, -1 }, // 20
{ 14, 15, 22, 28, 29, -1 },
{ 14, 15, 16, 21, 23, 28, 29, 30, -1 },
{ 15, 16, 17, 22, 24, 29, 30, 31, -1 },
{ 16, 17, 18, 23, 25, 30, 31, 32, -1 },
{ 17, 18, 19, 24, 26, 31, 32, 33, -1 }, // 25
{ 18, 19, 20, 25, 27, 32, 33, 34, -1 },
{ 19, 20, 26, 33, 34, -1 },
{ 21, 22, 29, 35, 36, -1 },
{ 21, 22, 23, 28, 30, 35, 36, 37, -1 },
{ 22, 23, 24, 29, 31, 36, 37, 38, -1 }, // 30
{ 23, 24, 25, 30, 32, 37, 38, 39, -1 },
{ 24, 25, 26, 31, 33, 38, 39, 40, -1 },
{ 25, 26, 27, 32, 34, 39, 40, 41, -1 },
{ 26, 27, 33, 40, 41, -1 },
{ 28, 29, 36, 42, 43, -1 }, // 35
{ 28, 29, 30, 35, 37, 42, 43, 44, -1 },
{ 29, 30, 31, 36, 38, 43, 44, 45, -1 },
{ 30, 31, 32, 37, 39, 44, 45, 46, -1 },
{ 31, 32, 33, 38, 40, 45, 46, 47, -1 },
{ 32, 33, 34, 39, 41, 46, 47, 48, -1 }, // 40
{ 33, 34, 40, 47, 48, -1 },
{ 35, 36, 43, -1 },
{ 35, 36, 37, 42, 44, -1 },
{ 36, 37, 38, 43, 45, -1 },
{ 37, 38, 39, 44, 46, -1 }, // 45
{ 38, 39, 40, 45, 47, -1 },
{ 39, 40, 41, 46, 48, -1 },
{ 40, 41, 47, -1 }
};
const int8 strategy2[][17] = {
{ 2, 9, 14, 15, 16, -1 },
{ 3, 10, 14, 15, 16, 17, -1 },
{ 0, 4, 7, 11, 14, 15, 16, 17, 18, -1 },
{ 1, 5, 8, 12, 15, 16, 17, 18, 19, -1 },
{ 2, 6, 9, 13, 16, 17, 18, 19, 20, -1 },
{ 3, 10, 17, 18, 19, 20, -1 }, // 5
{ 4, 11, 18, 19, 20, -1 },
{ 2, 9, 16, 21, 22, 23, -1 },
{ 3, 10, 17, 21, 22, 23, 24, -1 },
{ 0, 4, 7, 11, 14, 18, 21, 22, 23, 24, 25, -1 },
{ 1, 5, 8, 12, 15, 19, 22, 23, 24, 25, 26, -1 }, // 10
{ 2, 6, 9, 13, 16, 20, 23, 24, 25, 26, 27, -1 },
{ 3, 10, 17, 24, 25, 26, 27, -1 },
{ 4, 11, 18, 25, 26, 27, -1 },
{ 0, 1, 2, 9, 16, 23, 28, 29, 30, -1 },
{ 0, 1, 2, 3, 10, 17, 24, 28, 29, 30, 31, -1 }, // 15
{ 0, 1, 2, 3, 4, 7, 11, 14, 18, 21, 25, 28, 29, 30, 31, 32, -1 },
{ 1, 2, 3, 4, 5, 8, 12, 15, 19, 22, 26, 29, 30, 31, 32, 33, -1 },
{ 2, 3, 4, 5, 6, 9, 13, 16, 20, 23, 27, 30, 31, 32, 33, 34, -1 },
{ 3, 4, 5, 6, 10, 17, 24, 31, 32, 33, 34, -1 },
{ 4, 5, 6, 11, 18, 25, 32, 33, 34, -1 }, // 20
{ 7, 8, 9, 16, 23, 30, 35, 36, 37, -1 },
{ 7, 8, 9, 10, 17, 24, 31, 35, 36, 37, 38, -1 },
{ 7, 8, 9, 10, 11, 14, 18, 21, 25, 28, 32, 35, 36, 37, 38, 39, -1 },
{ 8, 9, 10, 11, 12, 15, 19, 22, 26, 29, 33, 36, 37, 38, 39, 40, -1 },
{ 9, 10, 11, 12, 13, 16, 20, 23, 27, 30, 34, 37, 38, 39, 40, 41, -1 }, // 25
{ 10, 11, 12, 13, 17, 24, 31, 38, 39, 40, 41, -1 },
{ 11, 12, 13, 18, 25, 32, 39, 40, 41, -1 },
{ 14, 15, 16, 23, 30, 37, 42, 43, 44, -1 },
{ 14, 15, 16, 17, 24, 31, 38, 42, 43, 44, 45, -1 },
{ 14, 15, 16, 17, 18, 21, 25, 28, 32, 35, 39, 42, 43, 44, 45, 46, -1 }, // 30
{ 15, 16, 17, 18, 19, 22, 26, 29, 33, 36, 40, 43, 44, 45, 46, 47, -1 },
{ 16, 17, 18, 19, 20, 23, 27, 30, 34, 37, 41, 44, 45, 46, 47, 48, -1 },
{ 17, 18, 19, 20, 24, 31, 38, 45, 46, 47, 48, -1 },
{ 18, 19, 20, 25, 32, 39, 46, 47, 48, -1 },
{ 21, 22, 23, 30, 37, 44, -1 }, // 35
{ 21, 22, 23, 24, 31, 38, 45, -1 },
{ 21, 22, 23, 24, 25, 28, 32, 35, 39, 42, 46, -1 },
{ 22, 23, 24, 25, 26, 29, 33, 36, 40, 43, 47, -1 },
{ 23, 24, 25, 26, 27, 30, 34, 37, 41, 44, 48, -1 },
{ 24, 25, 26, 27, 31, 38, 45, -1 }, // 40
{ 25, 26, 27, 32, 39, 46, -1 },
{ 28, 29, 30, 37, 44, -1 },
{ 28, 29, 30, 31, 38, 45, -1 },
{ 28, 29, 30, 31, 32, 35, 39, 42, 46, -1 },
{ 29, 30, 31, 32, 33, 36, 40, 43, 47, -1 }, // 45
{ 30, 31, 32, 33, 34, 37, 41, 44, 48, -1 },
{ 31, 32, 33, 34, 38, 45, -1 },
{ 32, 33, 34, 39, 46, -1 }
};
void CellGame::copyToTempBoard() {
for (int i = 0; i < 53; ++i) {
_tempBoard[i] = _board[i];
}
}
void CellGame::copyFromTempBoard() {
for (int i = 0; i < 53; ++i) {
_board[i] = _tempBoard[i];
}
}
void CellGame::copyToShadowBoard() {
_board[53] = 0;
_board[55] = 1;
_board[56] = 0;
for (int i = 0; i < 49; ++i) {
_shadowBoard[i] = _board[i];
}
}
void CellGame::pushBoard() {
assert(_boardStackPtr < 57 * 9);
for (int i = 0; i < 57; ++i)
_boardStack[_boardStackPtr + i] = _board[i];
_boardStackPtr += 57;
}
void CellGame::popBoard() {
assert(_boardStackPtr > 0);
_boardStackPtr -= 57;
for (int i = 0; i < 57; ++i) {
_board[i] = _boardStack[_boardStackPtr + i];
}
}
void CellGame::pushShadowBoard() {
assert(_boardStackPtr < 57 * 9);
for (int i = 0; i < 57; ++i)
_boardStack[_boardStackPtr + i] = _shadowBoard[i];
_boardStackPtr += 57;
}
void CellGame::popShadowBoard() {
assert(_boardStackPtr > 0);
_boardStackPtr -= 57;
for (int i = 0; i < 57; ++i) {
_shadowBoard[i] = _boardStack[_boardStackPtr + i];
}
}
void CellGame::clearMoves() {
_stack_startXY[0] = _board[53];
_stack_endXY[0] = _board[54];
_stack_pass[0] = _board[55];
_stack_index = 1;
}
void CellGame::pushMove() {
_stack_startXY[_stack_index] = _board[53];
_stack_endXY[_stack_index] = _board[54];
_stack_pass[_stack_index] = _board[55];
_stack_index++;
}
void CellGame::resetMove() {
_board[53] = 0;
_board[54] = 0;
_board[55] = 0;
}
void CellGame::takeCells(uint16 whereTo, int8 color) {
int cellN;
const int8 *str;
str = possibleMoves[whereTo];
while (1) {
cellN = *str++;
if (cellN < 0)
break;
if (_tempBoard[cellN] > 0) {
--_tempBoard[_tempBoard[cellN] + 48];
_tempBoard[cellN] = color;
++_tempBoard[color + 48];
}
}
}
void CellGame::countAllCells() {
_board[49] = 0;
_board[50] = 0;
_board[51] = 0;
_board[52] = 0;
for (int i = 0; i < 49; i++) {
switch (_board[i]) {
case 1: // CELL_BLUE
_board[49]++;
break;
case 2: // CELL_GREEN
_board[50]++;
break;
case 3:
_board[51]++;
break;
case 4:
_board[52]++;
break;
default:
break;
}
}
}
int CellGame::countCellsOnTempBoard(int8 color) {
const int8 *str;
int res = 0;
int i;
for (i = 0; i < 49; i++)
_boardSum[i] = 0;
for (i = 0; i < 49; i++) {
if (_tempBoard[i] == color) {
for (str = possibleMoves[i]; *str > 0; str++) {
if (!_tempBoard[*str])
++_boardSum[*str];
}
}
}
for (i = 0; i < 49; i++)
res += _boardSum[i];
return res;
}
bool CellGame::canMoveFunc1(int8 color) {
const int8 *str;
if (_board[55] == 1) {
for (; _board[53] < 49; _board[53]++) {
if (_shadowBoard[_board[53]] == color) {
str = &possibleMoves[_board[53]][_board[56]];
for (;_board[56] < 8; _board[56]++) {
_board[54] = *str++;
if (_board[54] < 0)
break;
if (!_shadowBoard[_board[54]]) {
_shadowBoard[_board[54]] = -1;
++_board[56];
return true;
}
}
_board[56] = 0;
}
}
_board[53] = 0;
_board[55] = 2;
_board[56] = 0;
}
if (_board[55] == 2) {
for (; _board[53] < 49; _board[53]++) {
if (_shadowBoard[_board[53]] == color) {
str = &strategy2[_board[53]][_board[56]];
for (;_board[56] < 16; _board[56]++) {
_board[54] = *str++;
if (_board[54] < 0)
break;
if (!_board[_board[54]]) {
++_board[56];
return true;
}
}
_board[56] = 0;
}
}
}
return false;
}
bool CellGame::canMoveFunc3(int8 color) {
const int8 *str;
if (_board[55] == 1) {
for (; _board[53] < 49; _board[53]++) {
if (_shadowBoard[_board[53]] == color) {
str = &possibleMoves[_board[53]][_board[56]];
for (;_board[56] < 8; _board[56]++) {
_board[54] = *str++;
if (_board[54] < 0)
break;
if (!_shadowBoard[_board[54]]) {
_shadowBoard[_board[54]] = -1;
++_board[56];
return true;
}
}
_board[56] = 0;
}
}
_board[53] = 0;
_board[55] = 2;
_board[56] = 0;
for (int i = 0; i < 49; ++i)
_shadowBoard[i] = _board[i];
}
if (_board[55] == 2) {
for (; _board[53] < 49; _board[53]++) {
if (_shadowBoard[_board[53]] == color) {
str = &strategy2[_board[53]][_board[56]];
for (;_board[56] < 16; _board[56]++) {
_board[54] = *str++;
if (_board[54] < 0)
break;
if (!_shadowBoard[_board[54]]) {
_shadowBoard[_board[54]] = -1;
++_board[56];
return true;
}
}
_board[56] = 0;
}
}
}
return false;
}
bool CellGame::canMoveFunc2(int8 color) {
const int8 *str;
while (1) {
while (_board[_board[54]]) {
++_board[54];
if (_board[54] >= 49)
return false;
}
if (!_board[55]) {
str = possibleMoves[_board[54]];
while (1) {
_board[53] = *str++;
if (_board[53] < 0)
break;
if (_board[_board[53]] == color) {
_board[55] = 1;
return true;
}
}
_board[55] = 1;
}
if (_board[55] == 1) {
_board[55] = 2;
_board[56] = 0;
}
if (_board[55] == 2) {
str = &strategy2[_board[54]][_board[56]];
for (; _board[56] < 16; _board[56]++) {
_board[53] = *str++;
if (_board[53] < 0)
break;
if (_board[_board[53]] == color) {
++_board[56];
return true;
}
}
++_board[54];
_board[55] = 0;
if (_board[54] >= 49)
break;
}
}
return false;
}
void CellGame::makeMove(int8 color) {
copyToTempBoard();
_tempBoard[_board[54]] = color;
++_tempBoard[color + 48];
if (_board[55] == 2) {
_tempBoard[_board[53]] = 0;
--_tempBoard[color + 48];
}
takeCells(_board[54], color);
}
int CellGame::getBoardWeight(int8 color1, int8 color2) {
int8 celln;
const int8 *str;
byte cellCnt[8];
str = possibleMoves[_board[54]];
cellCnt[1] = _board[49];
cellCnt[2] = _board[50];
cellCnt[3] = _board[51];
cellCnt[4] = _board[52];
if (_board[55] != 2)
++cellCnt[color2];
celln = *str++;
celln = _board[celln];
if (celln > 0) {
--cellCnt[celln];
++cellCnt[color2];
}
celln = *str++;
celln = _board[celln];
if (celln > 0) {
--cellCnt[celln];
++cellCnt[color2];
}
celln = *str++;
celln = _board[celln];
if (celln > 0) {
--cellCnt[celln];
++cellCnt[color2];
}
while (1) {
celln = *str++;
if (celln < 0)
break;
celln = _board[celln];
if (celln > 0) {
--cellCnt[celln];
++cellCnt[color2];
}
}
return _coeff3 + 2 * (2 * cellCnt[color1] - cellCnt[1] - cellCnt[2] - cellCnt[3] - cellCnt[4]);
}
void CellGame::chooseBestMove(int8 color) {
int moveIndex = 0;
if (_flag2) {
int bestWeight = 32767;
for (int i = 0; i < _stack_index; ++i) {
_board[53] = _stack_startXY[i];
_board[54] = _stack_endXY[i];
_board[55] = _stack_pass[i];
makeMove(color);
int curWeight = countCellsOnTempBoard(color);
if (curWeight <= bestWeight) {
if (curWeight < bestWeight)
moveIndex = 0;
bestWeight = curWeight;
_stack_startXY[moveIndex] = _board[53];
_stack_endXY[moveIndex] = _board[54];
_stack_pass[moveIndex++] = _board[55];
}
}
_stack_index = moveIndex;
}
_startX = _stack_startXY[0] % 7;
_startY = _stack_startXY[0] / 7;
_endX = _stack_endXY[0] % 7;
_endY = _stack_endXY[0] / 7;
}
int8 CellGame::calcBestWeight(int8 color1, int8 color2, uint16 depth, int bestWeight) {
int8 res;
int8 curColor;
bool canMove;
int type;
uint16 i;
int8 currBoardWeight;
int8 weight;
pushBoard();
copyFromTempBoard();
curColor = color2;
for (i = 0;; ++i) {
if (i >= 4) {
res = _coeff3 + 2 * (2 * _board[color1 + 48] - _board[49] - _board[50] - _board[51] - _board[52]);
popBoard();
return res;
}
++curColor;
if (curColor > 4)
curColor = 1;
if (_board[curColor + 48]) {
if (_board[curColor + 48] >= 49 - _board[49] - _board[50] - _board[51] - _board[52]) {
resetMove();
canMove = canMoveFunc2(curColor);
type = 1;
} else {
copyToShadowBoard();
if (depth == 1) {
canMove = canMoveFunc3(curColor);
type = 3;
} else {
canMove = canMoveFunc1(curColor);
type = 2;
}
}
if (canMove)
break;
}
}
if (_flag1) {
popBoard();
return bestWeight + 1;
}
depth -= 1;
if (depth) {
makeMove(curColor);
if (type == 1) {
res = calcBestWeight(color1, curColor, depth, bestWeight);
} else {
pushShadowBoard();
res = calcBestWeight(color1, curColor, depth, bestWeight);
popShadowBoard();
}
} else {
res = getBoardWeight(color1, curColor);
}
if ((res < bestWeight && color1 != curColor) || _flag4) {
popBoard();
return res;
}
currBoardWeight = _coeff3 + 2 * (2 * _board[color1 + 48] - _board[49] - _board[50] - _board[51] - _board[52]);
while (1) {
if (type == 1) {
canMove = canMoveFunc2(curColor);
} else if (type == 2) {
canMove = canMoveFunc1(curColor);
} else {
canMove = canMoveFunc3(curColor);
}
if (!canMove)
break;
if (_flag1) {
popBoard();
return bestWeight + 1;
}
if (_board[55] == 2) {
if (getBoardWeight(color1, curColor) == currBoardWeight)
continue;
}
if (!depth) {
weight = getBoardWeight(color1, curColor);
if (type == 1) {
if (_board[55] == 2)
_board[56] = 16;
}
} else {
makeMove(curColor);
if (type != 1) {
pushShadowBoard();
weight = calcBestWeight(color1, curColor, depth, bestWeight);
popShadowBoard();
} else {
weight = calcBestWeight(color1, curColor, depth, bestWeight);
}
}
if ((weight < res && color1 != curColor) || (weight > res && color1 == curColor))
res = weight;
if ((res < bestWeight && color1 != curColor) || _flag4)
break;
}
popBoard();
return res;
}
void CellGame::doGame(int8 color, int depth) {
bool canMove;
int type;
countAllCells();
if (_board[color + 48] >= 49 - _board[49] - _board[50] - _board[51] - _board[52]) {
resetMove();
canMove = canMoveFunc2(color);
type = true;
} else {
copyToShadowBoard();
canMove = canMoveFunc1(color);
type = false;
}
if (canMove) {
int8 w1, w2;
if (_board[color + 48] - _board[49] - _board[50] - _board[51] - _board[52] == 0)
depth = 0;
_coeff3 = 0;
if (_board[55] == 1)
_coeff3 = 1;
clearMoves();
if (depth) {
makeMove(color);
_flag4 = false;
if (type) {
w2 = calcBestWeight(color, color, depth, -127);
} else {
pushShadowBoard();
w2 = calcBestWeight(color, color, depth, -127);
popShadowBoard();
}
} else {
w2 = getBoardWeight(color, color);
}
int8 currBoardWeight = 2 * (2 * _board[color + 48] - _board[49] - _board[50] - _board[51] - _board[52]);
while (1) {
if (type)
canMove = canMoveFunc2(color);
else
canMove = canMoveFunc1(color);
if (!canMove)
break;
if (_flag1)
break;
_coeff3 = 0;
if (_board[55] == 2) {
if (getBoardWeight(color, color) == currBoardWeight)
continue;
}
if (_board[55] == 1)
_coeff3 = 1;
if (depth) {
makeMove(color);
_flag4 = false;
if (type) {
w1 = calcBestWeight(color, color, depth, w2);
} else {
pushShadowBoard();
w1 = calcBestWeight(color, color, depth, w2);
popShadowBoard();
}
} else {
w1 = getBoardWeight(color, color);
}
if (w1 == w2)
pushMove();
if (w1 > w2) {
clearMoves();
w2 = w1;
}
}
chooseBestMove(color);
}
}
const int8 depths[] = { 1, 1, 1, 2, 1, 1, 2, 2, 1, 2, 2, 2, 3, 2, 2, 3, 3, 2, 3, 3, 3 };
void CellGame::calcMove(int8 color, uint16 depth) {
_flag1 = false;
++_moveCount;
if (depth) {
if (depth == 1) {
_flag2 = true;
doGame(color, 0);
} else {
int newDepth;
newDepth = depths[3 * (depth - 2) + _moveCount % 3];
if (_easierAi && _moveCount > 7)
newDepth = 1;
_flag2 = true;
if (newDepth >= 20) {
assert(0); // This branch is not implemented
} else {
doGame(color, newDepth);
}
}
} else {
_flag2 = false;
doGame(color, depth);
}
}
void CellGame::run(uint16 depth, byte *scriptBoard) {
const byte color = 2;
int i;
for (i = 0; i < 49; i++, scriptBoard++) {
_board[i] = 0;
if (*scriptBoard == 50)
_board[i] = 1;
if (*scriptBoard == 66)
_board[i] = 2;
}
for (i = 49; i < 57; i++)
_board[i] = 0;
calcMove(color, depth);
}
} // End of Groovie namespace

View File

@@ -0,0 +1,101 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_CELL_H
#define GROOVIE_LOGIC_CELL_H
#include "common/textconsole.h"
#define BOARDSIZE 7
#define CELL_CLEAR 0
#define CELL_BLUE 1
#define CELL_GREEN 2
namespace Groovie {
class CellGame {
public:
CellGame(bool easierAi);
~CellGame();
void run(uint16 depth, byte *scriptBoard);
byte getStartX();
byte getStartY();
byte getEndX();
byte getEndY();
private:
void copyToTempBoard();
void copyFromTempBoard();
void copyToShadowBoard();
void pushBoard();
void popBoard();
void pushShadowBoard();
void popShadowBoard();
void clearMoves();
void pushMove();
void resetMove();
bool canMoveFunc1(int8 color);
bool canMoveFunc2(int8 color);
bool canMoveFunc3(int8 color);
void takeCells(uint16 whereTo, int8 color);
void countAllCells();
int countCellsOnTempBoard(int8 color);
void makeMove(int8 color);
int getBoardWeight(int8 color1, int8 color2);
void chooseBestMove(int8 color);
int8 calcBestWeight(int8 color1, int8 color2, uint16 depth, int bestWeight);
void doGame(int8 color, int depth);
void calcMove(int8 color, uint16 depth);
byte _startX;
byte _startY;
byte _endX;
byte _endY;
int8 _board[57];
int8 _tempBoard[58];
int8 _shadowBoard[64];
int8 _boardStack[570];
int _boardStackPtr;
int8 _boardSum[58];
int8 _stack_startXY[128];
int8 _stack_endXY[128];
int8 _stack_pass[128];
int _stack_index;
int _coeff3;
bool _flag1, _flag2, _flag4;
int _moveCount;
bool _easierAi;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_CELL_H

View File

@@ -0,0 +1,250 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/gallery.h"
namespace Groovie {
// Links between the pieces in the Gallery challenge
// For example, the first row signifies that piece 1
// is connected to pieces 2, 4 and 5
const byte GalleryGame::kGalleryLinks[21][10] = {
{ 2, 4, 5, 0, 0, 0, 0, 0, 0, 0 }, // 1
{ 1, 5, 3, 0, 0, 0, 0, 0, 0, 0 }, // 2
{ 2, 5, 9, 12, 0, 0, 0, 0, 0, 0 }, // 3
{ 1, 5, 6, 7, 8, 0, 0, 0, 0, 0 }, // 4
{ 1, 2, 3, 4, 7, 8, 9, 0, 0, 0 }, // 5
{ 4, 7, 10, 11, 13, 14, 15, 16, 18, 0 }, // 6
{ 4, 5, 6, 8, 9, 10, 0, 0, 0, 0 }, // 7
{ 4, 5, 7, 0, 0, 0, 0, 0, 0, 0 }, // 8
{ 3, 5, 7, 10, 11, 12, 18, 0, 0, 0 }, // 9
{ 6, 7, 9, 11, 0, 0, 0, 0, 0, 0 }, // 10
{ 6, 9, 10, 18, 0, 0, 0, 0, 0, 0 }, // 11
{ 3, 9, 18, 21, 0, 0, 0, 0, 0, 0 }, // 12
{ 6, 14, 17, 19, 0, 0, 0, 0, 0, 0 }, // 13
{ 6, 13, 15, 17, 19, 20, 21, 0, 0, 0 }, // 14
{ 6, 14, 16, 18, 21, 0, 0, 0, 0, 0 }, // 15
{ 6, 15, 0, 0, 0, 0, 0, 0, 0, 0 }, // 16
{13, 14, 19, 0, 0, 0, 0, 0, 0, 0 }, // 17
{ 6, 9, 11, 12, 15, 21, 0, 0, 0, 0 }, // 18
{13, 14, 17, 20, 0, 0, 0, 0, 0, 0 }, // 19
{14, 19, 21, 0, 0, 0, 0, 0, 0, 0 }, // 20
{12, 14, 15, 18, 20, 0, 0, 0, 0, 0 } // 21
};
enum kGalleryPieceStatus {
kPieceUnselected = 0,
kPieceSelected = 1
};
void GalleryGame::run(byte *scriptVariables) {
byte pieceStatus[kPieceCount];
byte status1[kPieceCount];
memcpy(pieceStatus, scriptVariables + 26, kPieceCount);
int selectedPieces = 0;
for (int i = 0; i < kPieceCount; i++) {
status1[i] = 0;
// in this context it seems like kPieceSelected means it's available for selection
if (pieceStatus[i] == kPieceSelected) {
byte status2[kPieceCount];
for (int j = 0; j < kPieceCount; j++)
status2[j] = pieceStatus[j];
status2[i] = 0;
byte curLink = kGalleryLinks[i][0];
int linkedPiece = 1;
while (curLink != 0) {
linkedPiece++;
status2[curLink - 1] = kPieceUnselected;
curLink = kGalleryLinks[i][linkedPiece - 1];
}
status1[i] = galleryAI(status2, 1);
// in this context, kPieceSelected means we think it's an optimal move
if (status1[i] == kPieceSelected) {
selectedPieces++;
}
}
}
if (selectedPieces == 0) {
// optimal move not found, choose a move with a high score?
int highestScore = 0;
for (int i = 0; i < kPieceCount; i++) {
if (highestScore < status1[i]) {
highestScore = status1[i];
}
}
if (highestScore == 2) {
highestScore = 1;
} else {
if (highestScore < kPieceCount) {
highestScore = 2;
} else {
highestScore -= 12;
}
}
for (int i = 0; i < kPieceCount; i++) {
if (highestScore < status1[i]) {
status1[i] = kPieceSelected;
selectedPieces++;
}
}
}
int selectedPiece = 0;
// var 49 is set by the script calling o_random
// choose one of our good moves
byte v12 = scriptVariables[49] % selectedPieces;
for (int i = 0; i < kPieceCount; i++) {
if (status1[selectedPiece] == 1 && !v12--)
break;
selectedPiece++;
}
scriptVariables[47] = (selectedPiece + 1) / 10;
scriptVariables[48] = (selectedPiece + 1) % 10;
}
byte GalleryGame::galleryAI(byte *pieceStatus, int depth) {
byte status1[kPieceCount];
byte status2[kPieceCount];
int selectedPieces = 0;
for (int i = 0; i < kPieceCount; i++) {
status1[i] = 0;
if (pieceStatus[i] == kPieceSelected) {
for (int j = 0; j < kPieceCount; j++)
status2[j] = pieceStatus[j];
status2[i] = 0;
selectedPieces = 1;
byte curLink = kGalleryLinks[i][0];
int linkedPiece = 1;
while (curLink != 0) {
linkedPiece++;
status2[curLink - 1] = kPieceUnselected;
curLink = kGalleryLinks[i][linkedPiece - 1];
}
status1[i] = galleryAI(status2, depth == 0 ? 1 : 0);
if (!depth && status1[i] == kPieceSelected) {
return 1;
}
}
}
if (selectedPieces) {
byte v8 = 0;
byte v9 = 0;
byte v10 = 0;
for (int j = 0; j < 21; ++j) {
byte v12 = status1[j];
if (v12) {
++v10;
if (v12 == 1)
++v9;
else
v8 += v12;
}
}
if (v9 == v10)
return 1; // I believe 1 means this is an optimal move
else if (_easierAi && (v9 + 1 == v10 || v9 - 1 == v10))
return 1; // close enough to an optimal move?
else
return (v8 + 102 * v9) / v10;// otherwise, higher numbers are better
}
return depth == 0 ? 2 : 1;
}
void GalleryGame::testsWriteMove(int move, byte pieceStatus[kPieceCount]) {
if (pieceStatus[move] != kPieceSelected)
error("illegal move to %d", move + 1);
pieceStatus[move] = kPieceUnselected;
for (int i = 0; i < 10; i++) {
byte curLink = kGalleryLinks[move][i];
if (!curLink)
break;
pieceStatus[curLink - 1] = kPieceUnselected;
}
}
void GalleryGame::ensureSamanthaWins(int seed) {
byte scriptVariables[1024];
byte goalPieceStatus[kPieceCount];
memset(goalPieceStatus, 0, sizeof(goalPieceStatus));
Common::RandomSource rng("ensureSamanthaWins");
rng.setSeed(seed);
warning("starting ensureSamanthaWins with seed %u", seed);
memset(scriptVariables, 1, sizeof(scriptVariables));
for (int i = 0; i < 100; i++) {
bool isStauf = i % 2;
scriptVariables[49] = rng.getRandomNumber(14);
run(scriptVariables);
int selectedMove = scriptVariables[47] * 10 + scriptVariables[48] - 1;
warning("Move %d: %s moved to %d", i, (isStauf ? "Stauf" : "Samantha"), selectedMove + 1);
testsWriteMove(selectedMove, scriptVariables + 26);
if (memcmp(scriptVariables + 26, goalPieceStatus, sizeof(goalPieceStatus)) == 0) {
if (isStauf)
error("Stauf won");
else
warning("Samantha won");
return;
}
}
error("game took too long");
}
void GalleryGame::test() {
warning("running gallery tests");
for (int i = 0; i < 20; i++) {
ensureSamanthaWins(i);
}
warning("finished running gallery tests");
}
} // End of Groovie namespace

View File

@@ -0,0 +1,105 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_GALLERY_H
#define GROOVIE_LOGIC_GALLERY_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Puzzle in the Gallery (bs.grv).
* The aim is to select the last piece of the image.
* There are 18 pieces in total.
* When selecting a piece, all surrounding pieces are also selected
*
* +--------------------+--------------------------------+--------+
* | 1/1A | 2/1B | |
* | +--------------+--+--------------------------+-----+ |
* | | | | |
* +--+ 4/1D | 5/1E | 3/1C |
* | | | |
* +-----+--------+--+--------+-----------------+--+--------+ |
* | | | | | | |
* | | | | | | |
* | | | 8/21 | | | |
* | | | | +-----------+ | |
* | | | | | | | |
* | | +-----------+ | 10/23 | 9/22 | |
* | | | | | |
* | | 7/20 +-----+-----+ +-----+
* | | | | | | |
* | +--------------------------+ | | | |
* | 6/1F | | | |
* +-----------+-----------+-----+--+ | 11 | | 12 |
* | 13/26 | | | | | / | | / |
* | +-----+-----+ | | | | 24 +-----------+ 25 |
* | | | | | | | | | |
* +-----+ 17/2A | | |16| | | | |
* | | | | |/ | | | | |
* | +-----+-----+ | |29| | | +-----+
* | | | | | | | | |
* | | | | | +-----+ 18/2B | |
* | 19/2C | 14/27 | | | | | |
* | | | | +-----------+ | |
* | | | | | | | |
* | | | +--+ 15/28 | | |
* | | | | | |
* | +--------+--+--------------------+-----------+ |
* | | 20/2D | 21/2E |
* +-----------+--------+-----------------------------------------+
*/
const int kPieceCount = 21;
class GalleryGame {
public:
GalleryGame(bool easierAi) {
#if 0
_easierAi = false;
test();
#endif
_easierAi = easierAi;
}
void run(byte *scriptVariables);
private:
byte galleryAI(byte *pieceStatus, int depth);
static const byte kGalleryLinks[21][10];
bool _easierAi;
void test();
void ensureSamanthaWins(int seed);
void testsWriteMove(int move, byte pieceStatus[kPieceCount]);
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_GALLERY_H

View File

@@ -0,0 +1,677 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/mousetrap.h"
namespace Groovie {
MouseTrapGame::MouseTrapGame(bool easierAi) : _random("MouseTrapGame") {
_mouseTrapCounter = _mouseTrapCounter1 = 0;
_mouseTrapX = _mouseTrapY = 0;
memset(_mouseTrapRoute, 0, 75);
memset(_mouseTrapRouteCopy, 0, 76);
_mouseTrapPosX = _mouseTrapPosY = 0;
memset(_mouseTrapCells, 0, 31);
_mouseTrapNumSteps = 0;
_easierAi = easierAi;
}
void MouseTrapGame::run(byte *scriptVariables) {
byte op = scriptVariables[2];
// variable 24 is the mouse?
//scriptVariables[24] = 2;
// player wins: scriptVariables[22] = 1;
// stauf wins: scriptVariables[22] = 2;
// allows the player to click to place the mouse somewhere? scriptVariables[5] = 0;
switch (op) {
case 0:
sub01(scriptVariables);
break;
case 1: // init board
// value of 0 is V, 1 is <, 2 is ^, 3 is >
// variable 23 is the outside piece
//scriptVariables[23] = _random.getRandomNumber(3);
// variable slot is the space number + 25, the left corner
// (Stauf's goal) is space 1, above that is space 2, the
// center is 13, and the right corner (goal) is space 25
init();
sub03(scriptVariables);
break;
case 2: // before player chooses the floor to move, set the banned move
{
int clicked = xyToPos(_mouseTrapX, _mouseTrapY);
scriptVariables[clicked + 50] = 0;
break;
}
case 3: // after player moving floor
// a bunch of hardcoded conditionals to copy variables and
// set the banned move
// this probably also sets a variable to allow the player to
// move the mouse, and checks for win/lose
sub05(scriptVariables);
break;
case 5: // maybe player moving mouse
sub06(scriptVariables);
break;
case 6: // Stauf moving floor?
sub07(scriptVariables);
break;
case 7: // maybe Stauf moving mouse
sub08(scriptVariables);
break;
case 8: // Samantha making a move
sub09(scriptVariables);
break;
default:
warning("Unknown mousetrap op %d", op);
break;
}
}
static const int8 mouseTrapStates[] = {
6, 12, 9, 3
};
static const int8 mouseTrapLookup[] = {
1, 0, 3, 0, 0, 1, 0, 3, 1, 4, 3, 4, 4, 1, 4, 3
};
void MouseTrapGame::init() {
int8 initState[8], initX[8], initY[8];
initState[0] = 0;
initState[1] = 1;
initState[3] = 3;
initState[2] = 2;
initState[4] = 4;
initState[5] = 5;
initState[6] = 6;
initState[7] = 7;
initY[0] = 1;
initY[1] = 3;
initY[2] = 0;
initY[3] = 4;
initY[4] = 0;
initY[5] = 4;
initY[6] = 1;
initY[7] = 3;
initX[0] = 0;
initX[1] = 0;
initX[2] = 1;
initX[3] = 1;
initX[4] = 3;
initX[5] = 3;
initX[6] = 4;
initX[7] = 4;
// easier AI gives a fixed board state, because the random configurations can give you some bad ones
if(_easierAi)
_random.setSeed(711);
for (int i = 7; i >= 0; i--) {
int8 j = _random.getRandomNumber(i);
_mouseTrapCells[5 * initY[i] + 5 + initX[i]] = mouseTrapStates[initState[j] >> 1];
for (; j < i; j++) {
initState[j] = initState[j + 1];
}
}
_mouseTrapCells[11] = mouseTrapStates[3];
_mouseTrapCells[16] = mouseTrapStates[0];
_mouseTrapCells[5] = 12;
_mouseTrapCells[21] = mouseTrapStates[0];
_mouseTrapCells[12] = mouseTrapStates[3];
_mouseTrapCells[15] = 13;
_mouseTrapCells[25] = 9;
_mouseTrapCells[22] = mouseTrapStates[1];
_mouseTrapCells[13] = mouseTrapStates[2];
_mouseTrapCells[18] = mouseTrapStates[2];
_mouseTrapCells[23] = mouseTrapStates[1];
_mouseTrapCells[7] = 14;
_mouseTrapCells[17] = 15;
_mouseTrapCells[27] = 11;
_mouseTrapCells[9] = 6;
_mouseTrapCells[19] = 7;
_mouseTrapCells[29] = 3;
_mouseTrapCells[30] = mouseTrapStates[_random.getRandomNumber(3)];
_mouseTrapPosY = 2;
_mouseTrapPosX = 2;
_mouseTrapY = 0;
_mouseTrapX = 0;
_mouseTrapNumSteps = 0;
_mouseTrapCounter = 0;
}
void MouseTrapGame::sub01(byte *scriptVariables) {
int8 x, y;
findMaxPointInRoute(&x, &y);
scriptVariables[5] = (_mouseTrapPosX == x && _mouseTrapPosY == y) ? 1 : 0;
if (havePosInRoute(4, 4)) {
copyRoute(4, 4);
scriptVariables[22] = 1;
} else if (havePosInRoute(0, 0)) {
copyRoute(0, 0);
scriptVariables[22] = 2;
} else {
scriptVariables[22] = 0;
if (!scriptVariables[5])
copyRoute(x, y);
}
}
void MouseTrapGame::sub03(byte *scriptVariables) {
int cnt = 1;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
scriptVariables[cnt + 25] = findState(_mouseTrapCells[5 * j + 5 + i]);
cnt++;
}
}
scriptVariables[23] = findState(_mouseTrapCells[30]);
}
void MouseTrapGame::sub05(byte *scriptVariables) {
int8 x, y;
posToXY(scriptVariables[1] + 10 * scriptVariables[0], &x, &y);
flipField(x, y);
if (calcSolution()) {
scriptVariables[5] = 0;
updateRoute();
if (havePosInRoute(4, 4)) {
copyRoute(4, 4);
scriptVariables[22] = 1;
} else if (havePosInRoute(0, 0)) {
copyRoute(0, 0);
scriptVariables[22] = 2;
} else {
copyStateToVars(scriptVariables);
scriptVariables[22] = 0;
}
} else {
scriptVariables[5] = 1;
scriptVariables[22] = 0;
}
}
void MouseTrapGame::sub06(byte *scriptVariables) {
int8 x, y;
posToXY(10 * scriptVariables[0] + scriptVariables[1], &x, &y);
copyRoute(x, y);
}
void MouseTrapGame::sub07(byte *scriptVariables) {
int8 x1, y1, x2, y2;
goFarthest(&x1, &y1);
flipField(x1, y1);
if (!calcSolution()) {
scriptVariables[5] = 1;
scriptVariables[22] = 0;
} else {
scriptVariables[5] = 0;
updateRoute();
if (!havePosInRoute(0, 0)) {
if (havePosInRoute(4, 4)) {
copyRoute(4, 4);
scriptVariables[22] = 1;
} else {
findMinPointInRoute(&x2, &y2);
if (_mouseTrapPosX != x2 || _mouseTrapPosY != y2) {
copyRoute(x2, y2);
scriptVariables[22] = 0;
} else {
scriptVariables[5] = 1;
scriptVariables[22] = 0;
}
}
} else {
copyRoute(0, 0);
scriptVariables[22] = 2;
}
}
int8 pos = xyToPos(x1, y1);
scriptVariables[0] = pos / 10;
scriptVariables[1] = pos % 10;
}
void MouseTrapGame::sub08(byte *scriptVariables) {
int8 x1, y1, x, y;
popLastStep(&x1, &y1);
int8 pos = xyToPos(x1, y1);
_mouseTrapPosX = x1;
_mouseTrapPosY = y1;
scriptVariables[0] = scriptVariables[11];
scriptVariables[1] = scriptVariables[12];
scriptVariables[11] = pos / 10;
scriptVariables[12] = pos % 10;
posToXY(scriptVariables[1] + 10 * scriptVariables[0], &x, &y);
if (y > y1) {
scriptVariables[15] = 0;
} else if (y < y1) {
scriptVariables[15] = 2;
} else if (x > x1) {
scriptVariables[15] = 3;
} else if (x < x1) {
scriptVariables[15] = 1;
}
if (!_mouseTrapCounter1)
scriptVariables[2] = 0;
}
void MouseTrapGame::sub09(byte *scriptVariables) {
int8 x1, y1, x2, y2;
getBestDirection(&x1, &y1);
flipField(x1, y1);
if (!calcSolution()) {
scriptVariables[5] = 1;
scriptVariables[22] = 0;
} else {
scriptVariables[5] = 0;
updateRoute();
if (!havePosInRoute(4, 4)) {
if (havePosInRoute(0, 0)) {
copyRoute(0, 0);
scriptVariables[22] = 2;
} else {
findMaxPointInRoute(&x2, &y2);
copyRoute(x2, y2);
scriptVariables[22] = 0;
}
} else {
copyRoute(4, 4);
scriptVariables[22] = 1;
}
}
int8 pos = xyToPos(x1, y1);
scriptVariables[0] = pos / 10;
scriptVariables[1] = pos % 10;
}
void MouseTrapGame::copyRoute(int8 x, int8 y) {
int i;
for (i = 0; i < _mouseTrapCounter; i++) {
if (_mouseTrapRoute[3 * i] == x && _mouseTrapRoute[3 * i + 1] == y)
break;
}
_mouseTrapCounter1 = 0;
do {
_mouseTrapRouteCopy[3 * _mouseTrapCounter1 + 0] = _mouseTrapRoute[3 * i + 0];
_mouseTrapRouteCopy[3 * _mouseTrapCounter1 + 1] = _mouseTrapRoute[3 * i + 1];
_mouseTrapRouteCopy[3 * _mouseTrapCounter1 + 2] = _mouseTrapRoute[3 * i + 2];
_mouseTrapCounter1++;
i = _mouseTrapRoute[3 * i + 2];
} while (i);
}
int8 MouseTrapGame::xyToPos(int8 x, int8 y) {
return 5 * y + x + 1;
}
void MouseTrapGame::posToXY(int8 pos, int8 *x, int8 *y) {
*y = (pos - 1) / 5;
*x = (pos - 1) % 5;
}
void MouseTrapGame::copyStateToVars(byte *scriptVariables) {
memset(scriptVariables + 51, 0, 24);
scriptVariables[75] = 0;
for (int i = 0; i < _mouseTrapCounter; i++)
scriptVariables[xyToPos(_mouseTrapRoute[3 * i], _mouseTrapRoute[3 * i + 1]) + 50] = 1;
}
int8 MouseTrapGame::findState(int8 val) {
int8 result = 0;
while (mouseTrapStates[result] != val) {
if (++result >= 4)
return -1;
}
return result;
}
void MouseTrapGame::flipField(int8 x, int8 y) {
int8 tmp;
if (y) {
if (y == 4) {
if (x == 1) {
tmp = _mouseTrapCells[10];
_mouseTrapCells[10] = _mouseTrapCells[11];
_mouseTrapCells[11] = _mouseTrapCells[12];
_mouseTrapCells[12] = _mouseTrapCells[13];
_mouseTrapCells[13] = _mouseTrapCells[14];
_mouseTrapCells[14] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 1;
_mouseTrapY = 0;
} else if (x == 3) {
tmp = _mouseTrapCells[20];
_mouseTrapCells[20] = _mouseTrapCells[21];
_mouseTrapCells[21] = _mouseTrapCells[22];
_mouseTrapCells[22] = _mouseTrapCells[23];
_mouseTrapCells[23] = _mouseTrapCells[24];
_mouseTrapCells[24] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 3;
_mouseTrapY = 0;
}
} else if (x) {
if (x == 4) {
if (y == 1) {
tmp = _mouseTrapCells[6];
_mouseTrapCells[6] = _mouseTrapCells[11];
_mouseTrapCells[11] = _mouseTrapCells[16];
_mouseTrapCells[16] = _mouseTrapCells[21];
_mouseTrapCells[21] = _mouseTrapCells[26];
_mouseTrapCells[26] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 0;
_mouseTrapY = 1;
} else if (y == 3) {
tmp = _mouseTrapCells[8];
_mouseTrapCells[8] = _mouseTrapCells[13];
_mouseTrapCells[13] = _mouseTrapCells[18];
_mouseTrapCells[18] = _mouseTrapCells[23];
_mouseTrapCells[23] = _mouseTrapCells[28];
_mouseTrapCells[28] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 0;
_mouseTrapY = 3;
}
}
} else if (y == 1) {
tmp = _mouseTrapCells[26];
_mouseTrapCells[26] = _mouseTrapCells[21];
_mouseTrapCells[21] = _mouseTrapCells[16];
_mouseTrapCells[16] = _mouseTrapCells[11];
_mouseTrapCells[11] = _mouseTrapCells[6];
_mouseTrapCells[6] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 4;
_mouseTrapY = 1;
} else if (y == 3) {
tmp = _mouseTrapCells[28];
_mouseTrapCells[28] = _mouseTrapCells[23];
_mouseTrapCells[23] = _mouseTrapCells[18];
_mouseTrapCells[18] = _mouseTrapCells[13];
_mouseTrapCells[13] = _mouseTrapCells[8];
_mouseTrapCells[8] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 4;
_mouseTrapY = 3;
}
} else if (x == 1) {
tmp = _mouseTrapCells[14];
_mouseTrapCells[14] = _mouseTrapCells[13];
_mouseTrapCells[13] = _mouseTrapCells[12];
_mouseTrapCells[12] = _mouseTrapCells[11];
_mouseTrapCells[11] = _mouseTrapCells[10];
_mouseTrapCells[10] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 1;
_mouseTrapY = 4;
} else if (x == 3) {
tmp = _mouseTrapCells[24];
_mouseTrapCells[24] = _mouseTrapCells[23];
_mouseTrapCells[23] = _mouseTrapCells[22];
_mouseTrapCells[22] = _mouseTrapCells[21];
_mouseTrapCells[21] = _mouseTrapCells[20];
_mouseTrapCells[20] = _mouseTrapCells[30];
_mouseTrapCells[30] = tmp;
_mouseTrapX = 3;
_mouseTrapY = 4;
}
}
bool MouseTrapGame::calcSolution() {
int8 pos = _mouseTrapPosY + 5 * _mouseTrapPosX; // coordinates swapped?
int8 val = _mouseTrapCells[pos + 5];
return ((val & 1) != 0 && _mouseTrapPosX && (_mouseTrapCells[pos] & 4) != 0)
|| ((val & 4) != 0 && _mouseTrapPosX < 4 && (_mouseTrapCells[pos + 10] & 1) != 0)
|| ((val & 8) != 0 && _mouseTrapPosY < 4 && (_mouseTrapCells[pos + 6] & 2) != 0)
|| ((val & 2) != 0 && _mouseTrapPosY && (_mouseTrapCells[pos + 4] & 8) != 0);
}
bool MouseTrapGame::havePosInRoute(int8 x, int8 y) {
for (int i = 0; i < _mouseTrapCounter; i++) {
if (_mouseTrapRoute[3 * i] == x && _mouseTrapRoute[3 * i + 1] == y)
return true;
}
return false;
}
void MouseTrapGame::addToRoute(int8 x, int8 y, int8 num) {
if (!havePosInRoute(x, y)) {
_mouseTrapRoute[3 * _mouseTrapCounter] = x;
_mouseTrapRoute[3 * _mouseTrapCounter + 1] = y;
_mouseTrapRoute[3 * _mouseTrapCounter + 2] = num;
_mouseTrapCounter++;
}
}
void MouseTrapGame::updateRoute() {
_mouseTrapCounter = 0;
addToRoute(_mouseTrapPosX, _mouseTrapPosY, 0);
int prevCounter = 0;
do {
for (int i = prevCounter; i < _mouseTrapCounter; i++) {
int8 y1 = _mouseTrapRoute[3 * i + 1];
int8 x1 = _mouseTrapRoute[3 * i];
int8 pos = 5 * x1 + y1;
int8 mask = _mouseTrapCells[pos + 5];
if ((mask & 1) != 0 && x1 && (_mouseTrapCells[pos] & 4) != 0)
addToRoute(x1 - 1, y1, i);
if ((mask & 4) != 0 && x1 < 4 && (_mouseTrapCells[pos + 10] & 1) != 0)
addToRoute(x1 + 1, y1, i);
if ((mask & 8) != 0 && y1 < 4 && (_mouseTrapCells[pos + 6] & 2) != 0)
addToRoute(x1, y1 + 1, i);
if ((mask & 2) != 0 && y1 && (_mouseTrapCells[pos + 4] & 8) != 0)
addToRoute(x1, y1 - 1, i);
}
prevCounter = _mouseTrapCounter;
} while (_mouseTrapCounter != prevCounter);
}
void MouseTrapGame::popLastStep(int8 *x, int8 *y) {
_mouseTrapCounter1--;
*x = _mouseTrapRouteCopy[3 * _mouseTrapCounter1];
*y = _mouseTrapRouteCopy[3 * _mouseTrapCounter1 + 1];
}
void MouseTrapGame::goFarthest(int8 *x, int8 *y) {
int8 origX = _mouseTrapX;
int8 origY = _mouseTrapY;
int8 maxVal = 0;
int8 maxX = 0, maxY = 0;
if (_mouseTrapNumSteps)
--_mouseTrapNumSteps;
for (int8 i = 0; i < 8; i++) {
int8 x1 = mouseTrapLookup[2 * i];
int8 y1 = mouseTrapLookup[2 * i + 1];
if (x1 != origX || y1 != origY) {
flipField(x1, y1);
int8 dist = calcDistanceToExit();
if (_easierAi)
dist += _random.getRandomNumber(2);
if (_mouseTrapNumSteps && _random.getRandomNumber(1) != 0)
dist += 3;
if (dist >= maxVal) {
maxVal = dist;
maxX = x1;
maxY = y1;
}
flipField(mouseTrapLookup[2 * ((i + 4) & 7)], mouseTrapLookup[2 * ((i + 4) & 7) + 1]);
}
}
*x = maxX;
*y = maxY;
}
void MouseTrapGame::findMinPointInRoute(int8 *x, int8 *y) {
int8 maxVal = 0;
int8 x1 = _mouseTrapPosX;
int8 y1 = _mouseTrapPosY;
for (int i = 0; i < _mouseTrapCounter; i++) {
if (8 - _mouseTrapRoute[3 * i + 1] - _mouseTrapRoute[3 * i] > maxVal) {
maxVal = 8 - _mouseTrapRoute[3 * i + 1] - _mouseTrapRoute[3 * i];
x1 = _mouseTrapRoute[3 * i];
y1 = _mouseTrapRoute[3 * i + 1];
}
}
*x = x1;
*y = y1;
}
int8 MouseTrapGame::calcDistanceToExit() {
int8 maxDist = 0;
updateRoute();
if (havePosInRoute(4, 4))
return 0;
for (int i = 0; i < _mouseTrapCounter; i++) {
if (8 - _mouseTrapRoute[3 * i + 1] - _mouseTrapRoute[3 * i] > maxDist)
maxDist = 8 - _mouseTrapRoute[3 * i + 1] - _mouseTrapRoute[3 * i];
}
return maxDist;
}
void MouseTrapGame::getBestDirection(int8 *x, int8 *y) {
int8 maxVal = 0;
int8 origX = _mouseTrapX;
int8 origY = _mouseTrapY;
_mouseTrapNumSteps = 8;
int8 maxX = 0, maxY = 0;
for (int i = 0; i < 8; i++) {
int x1 = mouseTrapLookup[2 * i];
int y1 = mouseTrapLookup[2 * i + 1];
if (origX != x1 || origY != y1) {
flipField(x1, y1);
int8 maxInRoute = findMaxInRoute();
if (maxInRoute >= maxVal) {
maxVal = maxInRoute;
maxX = x1;
maxY = y1;
}
flipField(mouseTrapLookup[2 * ((i + 4) & 7)], mouseTrapLookup[2 * ((i + 4) & 7) + 1]);
}
}
*x = maxX;
*y = maxY;
}
void MouseTrapGame::findMaxPointInRoute(int8 *x, int8 *y) {
int8 maxVal = 0;
int8 x1 = _mouseTrapPosX;
int8 y1 = _mouseTrapPosY;
updateRoute();
for (int i = 0; i < _mouseTrapCounter; i++) {
if (_mouseTrapRoute[3 * i] + _mouseTrapRoute[3 * i + 1] > maxVal) {
maxVal = _mouseTrapRoute[3 * i] + _mouseTrapRoute[3 * i + 1];
x1 = _mouseTrapRoute[3 * i];
y1 = _mouseTrapRoute[3 * i + 1];
}
}
*x = x1;
*y = y1;
}
int8 MouseTrapGame::findMaxInRoute() {
updateRoute();
if (havePosInRoute(0, 0))
return 0;
int8 maxCoords = 0;
for (int i = 0; i < _mouseTrapCounter; i++) {
if (_mouseTrapRoute[3 * i] + _mouseTrapRoute[3 * i + 1] > maxCoords)
maxCoords = _mouseTrapRoute[3 * i] + _mouseTrapRoute[3 * i + 1];
}
return maxCoords;
}
} // End of Groovie namespace

View File

@@ -0,0 +1,101 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_MOUSETRAP_H
#define GROOVIE_LOGIC_MOUSETRAP_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Mouse Trap puzzle in the Lab.
*
* Stauf's Goal is space 1, counting up as you go north east
* towards the north corner which is space 5 and the moveable
* space to the left of that is space 4.
* South east from Stauf's goal is the next line starting with
* space 6, counting up as you go north east where the moveable
* space to the right of the north corner is space 10
*
* Next line is 11 (unmovable) to 15 (unmoveable), this line
* contains the center space which is space 13
* Next line is 16 (moveable) to 20 (moveable)
* Next line is 21 (unmovable) to 25 (unmovable), with 25 being
* the player's goal door
*
* Space -2 is the next piece, outside of the box
*/
class MouseTrapGame {
public:
MouseTrapGame(bool easierAi);
void run(byte *scriptVariables);
private:
void init();
void sub01(byte *scriptVariables);
void sub03(byte *scriptVariables);
void sub05(byte *scriptVariables);
void sub06(byte *scriptVariables);
void sub07(byte *scriptVariables);
void sub08(byte *scriptVariables);
void sub09(byte *scriptVariables);
void copyRoute(int8 x, int8 y);
int8 xyToPos(int8 x, int8 y);
void posToXY(int8 pos, int8 *x, int8 *y);
void copyStateToVars(byte *scriptVariables);
int8 findState(int8 val);
void flipField(int8 x, int8 y);
bool calcSolution();
bool havePosInRoute(int8 y, int8 x);
void addToRoute(int8 y, int8 x, int8 num);
void updateRoute();
void popLastStep(int8 *x, int8 *y);
void goFarthest(int8 *x, int8 *y);
void findMinPointInRoute(int8 *y, int8 *x);
int8 calcDistanceToExit();
void getBestDirection(int8 *x, int8 *y);
void findMaxPointInRoute(int8 *x, int8 *y);
int8 findMaxInRoute();
private:
Common::RandomSource _random;
int8 _mouseTrapX, _mouseTrapY;
int8 _mouseTrapCounter, _mouseTrapCounter1;
int8 _mouseTrapRoute[75];
int8 _mouseTrapRouteCopy[76];
int8 _mouseTrapCells[31];
int8 _mouseTrapPosX, _mouseTrapPosY;
int8 _mouseTrapNumSteps;
bool _easierAi;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_MOUSETRAP_H

View File

@@ -0,0 +1,721 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/logic/othello.h"
#include "groovie/groovie.h"
namespace Groovie {
const int EMPTY_PIECE = 0;
const int AI_PIECE = 1;
const int PLAYER_PIECE = 2;
int xyToVar(int x, int y) {
return x * 10 + y + 25;
}
void sortPossibleMoves(Freeboard (&boards)[30], int numPossibleMoves) {
if (numPossibleMoves < 2)
return;
Common::sort(&boards[0], &boards[numPossibleMoves]);
}
int OthelloGame::scoreEdge(byte (&board)[8][8], int x, int y, int slopeX, int slopeY) {
const int8 *scores = &_edgesScores[0];
const int8 *ptr = &scores[board[x][y]];
// we don't score either corner in this function
x += slopeX;
y += slopeY;
int endX = x + slopeX * 5;
int endY = y + slopeY * 5;
while (x <= endX && y <= endY) {
ptr = &scores[*ptr + board[x][y]];
x += slopeX;
y += slopeY;
}
return _cornersScores[*ptr];
}
int OthelloGame::scoreEarlyGame(Freeboard *freeboard) {
// in the early game the AI's search depth can't see far enough
// so instead of the score simply counting the pieces, we use some heuristics
int scores[3];
scores[0] = 0;
scores[1] = 0;
scores[2] = 0;
byte(&b)[8][8] = freeboard->_boardstate;
int scoreRightEdge = scoreEdge(b, 7, 0, 0, 1);
int scoreBottomEdge = scoreEdge(b, 0, 7, 1, 0);
int scoreTopEdge = scoreEdge(b, 0, 0, 1, 0);
int scoreLeftEdge = scoreEdge(b, 0, 0, 0, 1);
scores[AI_PIECE] = scoreRightEdge + scoreBottomEdge + scoreTopEdge + scoreLeftEdge;
int topLeft = b[0][0];
int bottomLeft = b[0][7];
int topRight = b[7][0];
int bottomRight = b[7][7];
//subtract points for bad spots relative to the opponent
//diagonal from the corners
const int8 *diagFromCorners = &_scores[0][0];
scores[b[1][1]] -= diagFromCorners[topLeft];
scores[b[1][6]] -= diagFromCorners[bottomLeft];
scores[b[6][1]] -= diagFromCorners[topRight];
scores[b[6][6]] -= diagFromCorners[bottomRight];
// 2 away from the edge
const int8 *twoAwayFromEdge = &_scores[1][0];
scores[b[1][2]] -= twoAwayFromEdge[b[0][2]];
scores[b[1][5]] -= twoAwayFromEdge[b[0][5]];
scores[b[2][1]] -= twoAwayFromEdge[b[2][0]];
scores[b[2][6]] -= twoAwayFromEdge[b[2][7]];
scores[b[5][1]] -= twoAwayFromEdge[b[5][0]];
scores[b[5][6]] -= twoAwayFromEdge[b[5][7]];
scores[b[6][2]] -= twoAwayFromEdge[b[7][2]];
scores[b[6][5]] -= twoAwayFromEdge[b[7][5]];
// 3 away from the edge
const int8 *threeAwayFromEdge = &_scores[2][0];
scores[b[1][3]] -= threeAwayFromEdge[b[0][3]];
scores[b[1][4]] -= threeAwayFromEdge[b[0][4]];
scores[b[3][1]] -= threeAwayFromEdge[b[3][0]];
scores[b[3][6]] -= threeAwayFromEdge[b[3][7]];
scores[b[4][1]] -= threeAwayFromEdge[b[4][0]];
scores[b[4][6]] -= threeAwayFromEdge[b[4][7]];
scores[b[6][3]] -= threeAwayFromEdge[b[7][3]];
scores[b[6][4]] -= threeAwayFromEdge[b[7][4]];
// corners
scores[topLeft] += 0x32;
scores[bottomLeft] += 0x32;
scores[topRight] += 0x32;
scores[bottomRight] += 0x32;
// left column
scores[b[0][1]] += 4;
scores[b[0][2]] += 0x10;
scores[b[0][3]] += 0xc;
scores[b[0][4]] += 0xc;
scores[b[0][5]] += 0x10;
scores[b[0][6]] += 4;
// top row
scores[b[1][0]] += 4;
scores[b[2][0]] += 0x10;
scores[b[3][0]] += 0xc;
scores[b[4][0]] += 0xc;
scores[b[5][0]] += 0x10;
scores[b[6][0]] += 4;
// bottom row
scores[b[1][7]] += 4;
scores[b[2][7]] += 0x10;
scores[b[3][7]] += 0xc;
scores[b[4][7]] += 0xc;
scores[b[5][7]] += 0x10;
scores[b[6][7]] += 4;
// away from the edges (interesting we don't score the center/starting spots?)
scores[b[2][2]] += 1;
scores[b[2][5]] += 1;
scores[b[5][2]] += 1;
scores[b[5][5]] += 1;
// right column
scores[b[7][1]] += 4;
scores[b[7][2]] += 0x10;
scores[b[7][3]] += 0xc;
scores[b[7][4]] += 0xc;
scores[b[7][5]] += 0x10;
scores[b[7][6]] += 4;
return scores[AI_PIECE] - scores[PLAYER_PIECE];
}
int OthelloGame::scoreLateGame(Freeboard *freeboard) {
byte *board = &freeboard->_boardstate[0][0];
// in the late game, we simply score the same way we determine the winner, because the AI's search depth can see to the end of the game
int scores[3];
scores[0] = 0;
scores[1] = 0;
scores[2] = 0;
for (int i = 0; i < 64; i++) {
scores[board[i]]++;
}
return (scores[AI_PIECE] - scores[PLAYER_PIECE]) * 4;
}
int OthelloGame::scoreBoard(Freeboard *board) {
if (_isLateGame || _easierAi)
return scoreLateGame(board);
else
return scoreEarlyGame(board);
}
void OthelloGame::restart(void) {
_counter = 0;
_isLateGame = false;
_board._score = 0;
// clear the board
memset(_board._boardstate, EMPTY_PIECE, sizeof(_board._boardstate));
// set the starting pieces
_board._boardstate[4][4] = AI_PIECE;
_board._boardstate[3][3] = _board._boardstate[4][4];
_board._boardstate[4][3] = PLAYER_PIECE;
_board._boardstate[3][4] = _board._boardstate[4][3];
}
void OthelloGame::writeBoardToVars(Freeboard *board, byte *vars) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
byte b = _lookupPlayer[board->_boardstate[x][y]];
vars[xyToVar(x, y)] = b;
}
}
return;
}
void OthelloGame::readBoardStateFromVars(byte *vars) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
byte b = vars[xyToVar(x, y)];
if (b == _lookupPlayer[0]) {
_board._boardstate[x][y] = EMPTY_PIECE;
}
if (b == _lookupPlayer[1]) {
_board._boardstate[x][y] = AI_PIECE;
}
if (b == _lookupPlayer[2]) {
_board._boardstate[x][y] = PLAYER_PIECE;
}
}
}
}
Freeboard OthelloGame::getPossibleMove(Freeboard *freeboard, int moveSpot) {
// we make a new board with the piece placed and captures completed
int player = _isAiTurn ? AI_PIECE : PLAYER_PIECE;
int opponent = _isAiTurn ? PLAYER_PIECE : AI_PIECE;
// copy the board
Freeboard newboard;
memcpy(newboard._boardstate, freeboard->_boardstate, sizeof(newboard._boardstate));
byte *board = &newboard._boardstate[0][0];
int8 **line = _lines[moveSpot];
// check every line until we hit the null-terminating pointer
for (line = _lines[moveSpot]; *line != NULL; line++) {
int8 *lineSpot = *line;
int piece = board[*lineSpot];
int8 *_lineSpot;
// we already know the current moveSpot is the player's piece
// if these 2 loops were a regex replacement, they would be something like s/(O+)P/(P+)P/
for (_lineSpot = lineSpot; piece == opponent; _lineSpot++) {
piece = board[*_lineSpot];
}
// if _lineSpot was advanced (meaning at least 1 opponent piece), and now we're at a player piece
if (_lineSpot != lineSpot && piece == player) {
// apply the captures
piece = board[*lineSpot];
while (piece == opponent) {
board[*lineSpot] = player;
lineSpot++;
piece = board[*lineSpot];
}
}
}
// add the new piece
board[moveSpot] = player;
return newboard;
}
void OthelloGame::checkPossibleMove(Freeboard *board, Freeboard (&boards)[30], int8 **lineSpot, int &numPossibleMoves, int moveSpot, byte player, byte opponent) {
int8 *testSpot;
// loop through a list of slots in line with piece moveSpot, looping away from moveSpot
do {
do {
// skip all spots that aren't the opponent
testSpot = *lineSpot;
lineSpot++;
if (testSpot == NULL) // end of the null terminated line?
return;
} while (board->_boardstate[*testSpot / 8][*testSpot % 8] != opponent);
// we found the opponent, skip to the first piece that doesn't belong to the opponent
for (; board->_boardstate[*testSpot / 8][*testSpot % 8] == opponent; testSpot++) {
}
// start over again if didn't find a piece of our own on the other side
} while (board->_boardstate[*testSpot / 8][*testSpot % 8] != player);
// so we found (empty space)(opponent+)(our own piece)
// add this to the list of possible moves
boards[numPossibleMoves] = getPossibleMove(board, moveSpot);
boards[numPossibleMoves]._score = scoreBoard(&boards[numPossibleMoves]);
numPossibleMoves++;
}
int OthelloGame::getAllPossibleMoves(Freeboard *board, Freeboard (&boards)[30]) {
int moveSpot = 0;
byte player = _isAiTurn ? AI_PIECE : PLAYER_PIECE;
byte opponent = _isAiTurn ? PLAYER_PIECE : AI_PIECE;
int numPossibleMoves = 0;
int8 ***line = &_lines[0];
do {
if (board->_boardstate[moveSpot / 8][moveSpot % 8] == 0) {
checkPossibleMove(board, boards, *line, numPossibleMoves, moveSpot, player, opponent);
}
line++;
moveSpot++;
if (moveSpot > 63) {
sortPossibleMoves(boards, numPossibleMoves);
return numPossibleMoves;
}
} while (true);
}
int OthelloGame::aiRecurse(Freeboard *board, int depth, int parentScore, int opponentBestScore) {
Freeboard possibleMoves[30];
int numPossibleMoves = getAllPossibleMoves(board, possibleMoves);
if (numPossibleMoves == 0) {
_isAiTurn = !_isAiTurn;
numPossibleMoves = getAllPossibleMoves(board, possibleMoves);
if (numPossibleMoves == 0) {
return scoreLateGame(board);
}
}
int _depth = depth - 1;
bool isPlayerTurn = !_isAiTurn;
int bestScore = isPlayerTurn ? 100 : -100;
Freeboard *boardsIter = &possibleMoves[0];
for (int i = 0; i < numPossibleMoves; i++, boardsIter++) {
Freeboard *tBoard = boardsIter;
_isAiTurn = isPlayerTurn; // reset and flip the global for whose turn it is before recursing
int score;
if (_depth == 0) {
score = (int)tBoard->_score;
} else {
if (isPlayerTurn) {
score = aiRecurse(tBoard, _depth, parentScore, bestScore);
} else {
score = aiRecurse(tBoard, _depth, bestScore, opponentBestScore);
}
}
if ((bestScore < score) != isPlayerTurn) {
bool done = true;
if (isPlayerTurn) {
if (parentScore < score)
done = false;
} else {
if (score < opponentBestScore)
done = false;
}
bestScore = score;
if (done) {
return score;
}
}
}
return bestScore;
}
byte OthelloGame::aiDoBestMove(Freeboard *pBoard) {
Freeboard possibleMoves[30];
int bestScore = -101;
int bestMove = 0;
int parentScore = -100;
if (_flag1 == 0) {
_isAiTurn = 1;
}
Freeboard *board = pBoard;
int numPossibleMoves = getAllPossibleMoves(board, possibleMoves);
if (numPossibleMoves == 0) {
return 0;
}
for (int move = 0; move < numPossibleMoves; move++) {
_isAiTurn = !_isAiTurn; // flip before recursing
int depth = _depths[_counter];
if (_easierAi)
depth = 1;
int score = aiRecurse(&possibleMoves[move], depth, parentScore, 100);
if (bestScore < score) {
parentScore = score;
bestMove = move;
bestScore = score;
}
}
*pBoard = possibleMoves[bestMove];
if (_flag1 == 0) {
_counter += 1;
}
return 1;
}
void OthelloGame::initLines(void) {
// allocate an array of strings, the lines are null-terminated
int8 **lines = &_linesStorage[0];
int8 *line = &_lineStorage[0];
for (int baseX = 0; baseX < 8; baseX++) {
for (int baseY = 0; baseY < 8; baseY++) {
// assign the array of strings to the current spot
_lines[(baseX * 8 + baseY)] = lines;
for (int slopeX = -1; slopeX < 2; slopeX++) {
for (int slopeY = -1; slopeY < 2; slopeY++) {
// don't include current spot in its own line
if (slopeX == 0 && slopeY == 0)
continue;
// assign the current line to the current spot in the lines array, uint saves us from bounds checking for below 0
*lines = line;
uint x = baseX + slopeX;
uint y;
for (y = baseY + slopeY; x < 8 && y < 8; y += slopeY) {
*line = x * 8 + y;
line++;
x += slopeX;
}
if (baseX + slopeX != (int)x || baseY + slopeY != (int)y) {
*line = baseX * 8 + baseY;
line++;
lines++;
}
}
}
// append a 0 to the lines array to terminate that set of lines
*lines = NULL;
lines++;
}
}
}
uint OthelloGame::makeMove(Freeboard *freeboard, uint8 x, uint8 y) {
Freeboard possibleMoves[30];
Freeboard *board = freeboard;
_isAiTurn = 0;
uint numPossibleMoves = getAllPossibleMoves(board, possibleMoves);
if (numPossibleMoves == 0)
return 0;
if (x == '*') {
_flag1 = 1;
aiDoBestMove(freeboard);
_flag1 = 0;
_counter += 1;
return 1;
}
// uint saves us from bounds checking below 0, not yet sure why this function uses y, x instead of x, y but it works
if (y < 8 && x < 8 && board->_boardstate[y][x] == 0) {
// find the pre-made board the represents this move
uint newBoardSlot = 0;
for (; newBoardSlot < numPossibleMoves && possibleMoves[newBoardSlot]._boardstate[y][x] == 0; newBoardSlot++) {
}
if (newBoardSlot == numPossibleMoves)
return 0;
*freeboard = possibleMoves[newBoardSlot];
_counter += 1;
return 1;
}
return 0;
}
byte OthelloGame::getLeader(Freeboard *f) {
byte counters[3] = {};
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
byte t = f->_boardstate[x][y];
counters[t]++;
}
}
if (counters[2] < counters[1])
return 1;
if (counters[2] > counters[1])
return 2;
return 3;
}
void OthelloGame::opInit(byte *vars) {
vars[0] = 0;
restart();
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
vars[xyToVar(x, y)] = _lookupPlayer[_board._boardstate[x][y]];
}
}
vars[4] = 1;
}
void OthelloGame::tickBoard() {
if (_counter < 60) {
if (_movesLateGame < _counter) {
_isLateGame = true;
}
}
}
void OthelloGame::opPlayerMove(byte *vars) {
tickBoard();
if (_counter < 60) {
_flag2 = 0;
byte x = vars[3];
byte y = vars[2];
// top left spot is 0, 0
debugC(1, kDebugLogic, "OthelloGame player moved to %d, %d", (int)x, (int)y);
vars[4] = makeMove(&_board, x, y);
} else {
vars[0] = getLeader(&_board);
vars[4] = 1;
}
writeBoardToVars(&_board, vars);
}
// this might be for a hint move? maybe on easy mode?
void OthelloGame::op3(byte *vars) {
tickBoard();
if (_counter < 60) {
vars[3] = '*';
uint move = makeMove(&_board, '*', vars[2]);
vars[4] = move;
if (move == 0) {
_flag2 = 1;
} else {
_flag2 = 0;
}
} else {
vars[0] = getLeader(&_board);
vars[4] = 1;
}
writeBoardToVars(&_board, vars);
}
void OthelloGame::opAiMove(byte *vars) {
tickBoard();
if (_counter < 60) {
uint move = aiDoBestMove(&_board);
vars[4] = move;
if (move == 0 && _flag2 != 0) {
vars[0] = getLeader(&_board);
}
} else {
vars[0] = getLeader(&_board);
vars[4] = 0;
}
writeBoardToVars(&_board, vars);
}
void OthelloGame::op5(byte *vars) {
_counter = vars[2];
readBoardStateFromVars(vars);
initLines();
vars[4] = 1;
}
OthelloGame::OthelloGame(bool easierAi)
: _random("OthelloGame"),
_depths {
1, 4, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 4, 7, 6, 5, 4, 3, 2, 1, 1
},
_lookupPlayer { 21, 40, 31 },
_scores {
{ 30, 0, 0, 0 },
{ 4, 0, 0, 0 },
{ 5, 0, 0, 0 }
},
_edgesScores{
0, 3, 6, 9, 3, 15, 12, 18,
6, 0, 45, 6, 0, 3, 27, 12,
60, 15, 9, 18, 36, 21, 24, 27,
30, 24, 36, 33, 39, 27, 21, 3,
27, 21, 24, 69, 33, 18, 36, 30,
39, 78, 42, 45, 48, 51, 45, 57,
54, 60, 48, 42, 87, 48, 42, 45,
6, 54, 102, 57, 51, 60, 15, 63,
66, 69, 72, 66, 78, 75, 81, 69,
63, 24, 69, 63, 66, 69, 75, 39,
78, 72, 81, 78, 84, 87, 90, 93,
87, 99, 96, 102, 90, 84, 87, 90,
84, 87, 48, 96, 102, 99, 93, 102,
57, 0, 0, 0, 0, 0, 0, 0
},
_cornersScores{
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
-20, 0, 0, 0, 20,
0, -20, 0, 0, 0,
20, 20, 20, 20, 20,
20, 20, 20, 20, 20,
20, 0, 20, 20, 20,
40, 20, 0, 20, 20,
20, 40, -20, -20, -20,
-20, -20, -20, -20, -20,
-20, -20, -40, -20, -20,
-20, 0, -20, -40, -20,
-20, -20, 0, 40, 40,
40, 40, 40, 40, 40,
40, 40, 40, 20, 40,
40, 40, 40, 40, 20,
40, 40, 40, 40, -40,
-40, -40, -40, -40, -40,
-40, -40, -40, -40, -40,
-40, -40, -40, -20, -40,
-40, -40, -40, -40, -20
},
_movesLateGame(52)
{
_isLateGame = false;
_counter = 0;
_isAiTurn = 0;
_flag1 = 0;
_flag2 = 0;
initLines();
#if 0
_easierAi = false;
test();
#endif
_easierAi = easierAi;
}
void OthelloGame::run(byte *vars) {
byte op = vars[1];
debugC(1, kDebugLogic, "OthelloGame op %d", (int)op);
switch (op) {
case 0: // init/restart
opInit(vars);
break;
case 1: // win/lose?
_flag2 = 1;
break;
case 2: // player move
opPlayerMove(vars);
break;
case 3: // ???
op3(vars);
break;
case 4: // ai move
opAiMove(vars);
break;
case 5: // ???
op5(vars);
break;
}
}
void OthelloGame::test() {
warning("OthelloGame::test() starting");
// pairs of x, y, 3 moves per line
testMatch({
// x1,y1,x2,y2,x3,y3
5, 4, 5, 2, 3, 2,
6, 6, 1, 2, 1, 0
}, true);
testMatch({
// x1,y1,x2,y2,x3,y3
5, 4, 6, 2, 4, 2,
5, 1, 5, 5, 3, 5,
1, 5, 2, 4, 6, 1,
6, 4, 6, 3, 7, 4,
7, 1, 6, 0, 1, 4,
2, 2, 1, 3, 6, 6,
6, 7, 0, 6, 2, 6,
4, 6, 3, 6, 5, 6,
1, 6, 1, 1, 2, 1,
3, 1, 3, 0, 0, 2,
2, 7
// x1,y1,x2,y2,x3,y3
}, false);
warning("OthelloGame::test() finished");
}
void OthelloGame::testMatch(Common::Array<int> moves, bool playerWin) {
byte vars[1024];
memset(vars, 0, sizeof(vars));
byte &op = vars[1];
byte &x = vars[3];
byte &y = vars[2];
byte &winner = vars[4];
byte &winner2 = vars[0];
warning("OthelloGame::testMatch(%u, %d) starting", moves.size(), (int)playerWin);
op = 0;
run(vars);
for (uint i = 0; i < moves.size(); i += 2) {
if (winner2 != 0)
error("early winner? %d, %d", (int)winner, (int)winner2);
x = moves[i];
y = moves[i + 1];
op = 2;
run(vars);
if (winner != 1)
error("early winner? %d, %d", (int)winner, (int)winner2);
op = 4;
run(vars);
}
if (playerWin && winner2 != 0)
error("player didn't win, %d", (int)winner2);
else if (playerWin == false && winner2 != 1)
error("ai didn't win? %d", (int)winner2);
warning("OthelloGame::testMatch(%u, %d) finished", moves.size(), (int)playerWin);
}
} // namespace Groovie

View File

@@ -0,0 +1,101 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_OTHELLO_H
#define GROOVIE_LOGIC_OTHELLO_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Othello/Reversi Cursed Coins puzzle in Clandestiny and UHP.
*/
struct Freeboard {
int _score;
byte _boardstate[8][8]; // 0 is empty, 1 is player, 2 is AI
// for sorting an array of pointers
friend bool operator<(const Freeboard &a, const Freeboard &b) {
return a._score > b._score;
}
};
class OthelloGame {
public:
OthelloGame(bool easierAi);
void run(byte *scriptVariables);
private:
int scoreEdge(byte (&board)[8][8], int x, int y, int slopeX, int slopeY);
int scoreEarlyGame(Freeboard *freeboard);
int scoreLateGame(Freeboard *freeboard);
int scoreBoard(Freeboard *board);
void restart(void);
void writeBoardToVars(Freeboard *board, byte *vars);
void readBoardStateFromVars(byte *vars);
Freeboard getPossibleMove(Freeboard *freeboard, int moveSpot);
void checkPossibleMove(Freeboard *board, Freeboard (&boards)[30], int8 **lineSpot, int &numPossibleMoves, int moveSpot, byte player, byte opponent);
int getAllPossibleMoves(Freeboard *board, Freeboard (&boards)[30]);
int aiRecurse(Freeboard *board, int depth, int parentScore, int opponentBestScore);
byte aiDoBestMove(Freeboard *pBoard);
void initLines(void);
uint makeMove(Freeboard *freeboard, uint8 x, uint8 y);
byte getLeader(Freeboard *f);
void opInit(byte *vars);
void tickBoard();
void opPlayerMove(byte *vars);
void op3(byte *vars);
void opAiMove(byte *vars);
void op5(byte *vars);
void test();
void testMatch(Common::Array<int> moves, bool playerWin);
Common::RandomSource _random;
byte _flag1;
int8 _flag2;
const int _depths[60];
int _counter;
const int _movesLateGame; // this is 52, seems to be a marker of when to change the function pointer to an aleternate scoring algorithm for the late game
bool _isLateGame; // used to choose the scoring function, true means scoreLateGame
const int8 _lookupPlayer[3]; // used to convert from internal values that represent piece colors to what the script uses in vars, {21, 40, 31}
const int8 _scores[3][4];
const int8 _edgesScores[112];
const int _cornersScores[105];
int _isAiTurn;
int8 **_lines[64];
int8 *_linesStorage[484];
int8 _lineStorage[2016];
Freeboard _board;
bool _easierAi;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_OTHELLO_H

View File

@@ -0,0 +1,891 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/logic/pente.h"
#include "common/stack.h"
#include "common/algorithm.h"
#include "groovie/groovie.h"
namespace Groovie {
#ifdef UINT_MAX
#undef UINT_MAX
#endif
const uint UINT_MAX = (uint)-1;
const int WIN_SCORE = 100000000;
const int CAPTURE_SCORE = 1000000;
const uint PLAYER = 79;
const uint STAUF = 88;
struct pentePlayerTable {
Common::FixedStack<int, 813> lines;
};
struct penteTable {
pentePlayerTable player;
pentePlayerTable stauf;
int playerScore;
int staufScore;
byte playerLines;
byte staufLines;
byte width;
byte height;
uint16 boardSize;
byte lineLength;
uint16 moveCounter;
byte boardState[20][15];
uint16 linesCounter;
uint16 linesTable[20][15][21];
byte numAdjacentPieces[20][15];
byte calcTouchingPieces; // the deepest level of AI recursion sets this to 0, and then sets it back to 1 when returning
};
void PenteGame::addLine(int x, int y, int linesCounter) {
int i = ++_table->linesTable[x][y][0];
_table->linesTable[x][y][i] = linesCounter;
}
void PenteGame::buildLookupTable() {
int width = _table->width;
int height = _table->height;
uint16 linesCounter = 0;
int lineLength = _table->lineLength;
// slope of (1,0)
for (int x = 0; x <= width - lineLength; x++) {
for (int y = 0; y < height; y++) {
for (int z = 0; z < lineLength; z++) {
addLine(x + z, y, linesCounter);
}
linesCounter++;
}
}
// slope of (0,1)
for (int x = 0; x < width; x++) {
for (int y = 0; y <= height - lineLength; y++) {
for (int z = 0; z < lineLength; z++) {
addLine(x, y + z, linesCounter);
}
linesCounter++;
}
}
// slope of (1,1)
for (int x = 0; x <= width - lineLength; x++) {
for (int y = 0; y <= height - lineLength; y++) {
for (int z = 0; z < lineLength; z++) {
addLine(x + z, y + z, linesCounter);
}
linesCounter++;
}
}
// slope of (1,-1)
for (int x = 0; x <= width - lineLength; x++) {
for (int y = lineLength - 1; y < height; y++) {
for (int z = 0; z < lineLength; z++) {
addLine(x + z, y - z, linesCounter);
}
linesCounter++;
}
}
_table->linesCounter = linesCounter;
}
void PenteGame::penteDeInit() {
delete _table;
_table = nullptr;
}
void PenteGame::penteInit(uint width, uint height, uint length) {
_table = new penteTable();
_table->width = width;
_table->height = height;
_table->boardSize = height * width;
_table->lineLength = length;
memset(_table->boardState, 0, sizeof(_table->boardState));
buildLookupTable();
assert(_table->linesCounter == 812);
_table->staufScore = (uint)_table->linesCounter;
_table->playerScore = (uint)_table->linesCounter;
memset(_table->numAdjacentPieces, 0, sizeof(_table->numAdjacentPieces));
_table->calcTouchingPieces = 1;
_nextCapturedSpot = -1;
_animateCapturesBitMask = 0;
_previousMove = 0;
}
int &PenteGame::getPlayerTable(bool staufTurn, pentePlayerTable *&pt) {
pt = staufTurn ? &_table->stauf : &_table->player;
return staufTurn ? _table->staufScore : _table->playerScore;
}
void PenteGame::scoreLine(uint16 lineIndex, bool isStaufTurn, bool revert) {
pentePlayerTable *playerTable;
int &score = getPlayerTable(isStaufTurn, playerTable);
int lineLength, mult;
if (revert) {
lineLength = --playerTable->lines[lineIndex];
mult = -1;
} else {
lineLength = playerTable->lines[lineIndex]++;
mult = 1;
}
if (_table->lineLength - lineLength == 1) {
score = (int)score + (int)WIN_SCORE * mult;
} else {
pentePlayerTable *opponentTable;
int &opponentScore = getPlayerTable(!isStaufTurn, opponentTable);
int opponentLineLength = opponentTable->lines[lineIndex];
if (lineLength == 0) {
opponentScore += (-(1 << ((byte)opponentLineLength & 0x1f))) * mult;
if (_table->lineLength - opponentLineLength == 1) {
if (isStaufTurn)
_table->playerLines -= mult;
else
_table->staufLines -= mult;
}
}
if (opponentLineLength == 0) {
score += (1 << ((byte)lineLength & 0x1f)) * mult;
if (_table->lineLength - lineLength == 2) {
byte b;
if (isStaufTurn)
b = (_table->staufLines += mult);
else
b = (_table->playerLines += mult);
if (revert)
b -= mult;
if (1 < b) {
score = (int)score + (int)CAPTURE_SCORE * mult;
}
}
}
}
}
void PenteGame::calcTouchingPieces(byte moveX, byte moveY, bool revert) {
byte endX, endY;
if (moveX + 1 < _table->width) {
endX = moveX + 1;
} else {
endX = _table->width - 1;
}
if (moveY + 1 < _table->height) {
endY = moveY + 1;
} else {
endY = _table->height - 1;
}
byte x = 0;
if (1 < moveX) {
x = moveX - 1;
}
for (; x <= endX; x++) {
byte y = 0;
if (1 < moveY) {
y = moveY - 1;
}
for (; y <= endY; y++) {
if (revert)
_table->numAdjacentPieces[x][y]--;
else
_table->numAdjacentPieces[x][y]++;
}
}
}
void PenteGame::updateScore(byte x, byte y, bool isStauf) {
assert(_table->boardState[x][y] == 0);
_table->boardState[x][y] = isStauf ? STAUF : PLAYER;
uint16 lines = _table->linesTable[x][y][0];
for (int i = 1; i <= lines; i++) {
uint16 lineIndex = _table->linesTable[x][y][i];
scoreLine(lineIndex, isStauf, false);
}
if (_table->calcTouchingPieces != 0) {
calcTouchingPieces(x, y, false);
}
_table->moveCounter++;
}
void PenteGame::revertScore(byte x, byte y) {
assert(_table->boardState[x][y] != 0);
bool stauf_turn = _table->boardState[x][y] == STAUF;
_table->boardState[x][y] = 0;
_table->moveCounter--;
uint lines = _table->linesTable[x][y][0];
for (uint i = 1; i <= lines; i++) {
uint16 lineIndex = _table->linesTable[x][y][i];
scoreLine(lineIndex, stauf_turn, true);
}
if (_table->calcTouchingPieces != 0) {
calcTouchingPieces(x, y, true);
}
}
byte PenteGame::scoreCaptureSingle(byte x, byte y, int slopeX, int slopeY) {
byte x1 = x + slopeX;
byte y1 = y + slopeY;
byte x2 = x + slopeX * 2;
byte y2 = y + slopeY * 2;
byte endX = x + slopeX * 3;
byte endY = y + slopeY * 3;
// we don't need to check for below 0 when we have unsigned types
if (x >= _table->width || y >= _table->height)
return 0;
if (endX >= _table->width || endY >= _table->height)
return 0;
auto &boardState = _table->boardState;
byte captor = boardState[x][y];
byte captive = captor == STAUF ? PLAYER : STAUF;
// make sure the captor is at the start and end of the line
if (boardState[endX][endY] != captor)
return 0;
// check that the captive is both of the middle pieces
if (boardState[x1][y1] != captive || boardState[x2][y2] != captive)
return 0;
// now we take away the points the captor had for these pieces
revertScore(x1, y1);
revertScore(x2, y2);
return 1;
}
struct Slope {
int x, y;
};
// the order of these is important because we return the bitMask
Slope slopes[] = {{1, 0},
{1, 1},
{0, 1},
{-1, 1},
{-1, 0},
{-1, -1},
{0, -1},
{1, -1}};
uint PenteGame::scoreCapture(byte x, byte y) {
byte bitMask = 0;
bool isStauf = _table->boardState[x][y] == STAUF;
for (const Slope &slope : slopes) {
bitMask <<= 1;
bitMask |= scoreCaptureSingle(x, y, slope.x, slope.y);
}
for (int i = bitMask; i; i >>= 1) {
if ((i & 1) == 0)
continue;
pentePlayerTable *playerTable;
int &score = getPlayerTable(isStauf, playerTable);
int lineLength = ++playerTable->lines[_table->linesCounter];
if (_table->lineLength == lineLength) {
score += WIN_SCORE;
} else {
score += 1 << ((lineLength - 1U) & 0x1f);
}
}
return bitMask;
}
void PenteGame::animateCapture(int16 move, byte *bitMaskG, int16 *outCapture1, int16 *outCapture2) {
int x = move / 15;
int y = 0xe - (move % 15);
byte &bitMask = *bitMaskG;
byte bit = 0;
for (bit = 0; bit < 8; bit++) {
if (bitMask >> bit & 1) {
bitMask = 1 << bit ^ bitMask;
break;
}
}
int16 baseSpot;
switch (bit) {
case 0:
*outCapture1 = (x + 2) * 15 - y;
*outCapture2 = x * 15 - y + 46;
return;
case 1:
*outCapture1 = (x + 1) * 15 - y;
*outCapture2 = x * 15 - y + 16;
return;
case 2:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot;
*outCapture2 = baseSpot - 14;
return;
case 3:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot - 1;
*outCapture2 = baseSpot - 16;
return;
case 4:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot - 2;
*outCapture2 = baseSpot - 18;
return;
case 5:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot + 13;
*outCapture2 = baseSpot + 12;
return;
case 6:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot + 28;
*outCapture2 = baseSpot + 42;
return;
case 7:
baseSpot = x * 15 - y;
*outCapture1 = baseSpot + 29;
*outCapture2 = baseSpot + 44;
}
return;
}
void PenteGame::revertCapture(byte x, byte y, byte bitMask) {
bool isPlayer = _table->boardState[x][y] == PLAYER;
for (int i = bitMask; i; i >>= 1) {
if ((i & 1) == 0)
continue;
pentePlayerTable *playerTable;
int &score = getPlayerTable(!isPlayer, playerTable);
int linesCounter = --playerTable->lines[_table->linesCounter];
if (_table->lineLength - linesCounter == 1) {
score -= WIN_SCORE;
} else {
score -= 1 << linesCounter;
}
}
for (int i = 0; i < 8; i++) {
if ((bitMask >> i & 1) == 0)
continue;
Slope &slope = slopes[7 - i];
updateScore(x + slope.x * 2, y + slope.y * 2, isPlayer);
updateScore(x + slope.x, y + slope.y, isPlayer);
}
}
int PenteGame::scoreMoveAndRevert(byte x, byte y, char depth, int parentScore, bool &gameOver) {
updateScore(x, y, _table->moveCounter % 2);
uint capturesMask = scoreCapture(x, y);
if (_table->playerScore >= WIN_SCORE || _table->staufScore >= WIN_SCORE)
gameOver = true;
else
gameOver = false;
int scoreDiff;
if (depth > 0 && gameOver==false && _table->boardSize != _table->moveCounter) {
scoreDiff = aiRecurse(depth, parentScore);
} else {
if (_table->moveCounter % 2 == 0) {
scoreDiff = _table->playerScore - _table->staufScore;
} else {
scoreDiff = _table->staufScore - _table->playerScore;
}
}
if (capturesMask != 0) {
revertCapture(x, y, capturesMask);
}
revertScore(x, y);
return scoreDiff;
}
int PenteGame::scoreMoveAndRevert(byte x, byte y, char depth, int parentScore) {
// same thing, but don't need to provide the reference to the gameOverBool
bool gameOver;
return scoreMoveAndRevert(x, y, depth, parentScore, gameOver);
}
int PenteGame::aiRecurseTail(int parentScore) {
int bestScore = 0x7fffffff;
_table->calcTouchingPieces = 0;
for (byte x = 0; x < _table->width; x++) {
for (byte y = 0; y < _table->height; y++) {
if ((_table->boardState[x][y] != 0) ||
(_table->numAdjacentPieces[x][y] == 0)) {
continue;
}
int scoreDiff = scoreMoveAndRevert(x, y, 0, 0);
if (scoreDiff < bestScore) {
bestScore = scoreDiff;
}
if (-parentScore != bestScore && parentScore <= -bestScore) {
_table->calcTouchingPieces = 1;
return -bestScore;
}
}
}
_table->calcTouchingPieces = 1;
return -bestScore;
}
int PenteGame::aiRecurse(char depth, int parentScore) {
if (depth == 1) {
// don't do more recursion
return aiRecurseTail(parentScore);
}
// do more recursion after finding some good moves
struct GoodMove {
int scoreDiff;
byte x, y;
bool operator()(GoodMove a, GoodMove b) {
return a.scoreDiff < b.scoreDiff;
}
};
Common::FixedStack<GoodMove, 300> goodMoves; // 300 slots because the board is 20x15, but we rarely need many since the search excludes spots with no adjacenet pieces
int bestScore = 0x7fffffff;
for (byte x = 0; x < _table->width; x++) {
for (byte y = 0; y < _table->height; y++) {
if ((_table->boardState[x][y] != 0) ||
(_table->numAdjacentPieces[x][y] == 0)) {
continue;
}
int scoreDiff = scoreMoveAndRevert(x, y, 0, 0);
goodMoves.push({scoreDiff, x, y});
}
}
// sort ascending by scoreDiff, most of the time you'll see scores like -40 at the top and -34 at the end
Common::sort(&goodMoves[0], &goodMoves.top(), goodMoves[0]);
for (uint i = 0; i < goodMoves.size(); i++) {
byte x = goodMoves[i].x;
byte y = goodMoves[i].y;
int scoreDiff = scoreMoveAndRevert(x, y, depth - 1, bestScore);
if (scoreDiff < bestScore) {
bestScore = scoreDiff;
}
if (-parentScore != bestScore && parentScore <= -bestScore)
break;
}
return -bestScore;
}
uint16 PenteGame::aiGetBestMove(byte depth) {
for (int x = 0; x < _table->width; x++) {
for (int y = 0; y < _table->height; y++) {
if (_table->boardState[x][y] != 0 || _table->numAdjacentPieces[x][y] == 0)
continue;
bool gameOver;
scoreMoveAndRevert(x, y, 0, 0, gameOver);
if (gameOver) {
return y + x * 100;
}
}
}
byte counter = 1;
int bestScore = 0x7fffffff;
uint16 bestMove = 0xffff;
for (; bestScore > 99999999 && depth > 1; depth--) {
for (int x = 0; x < _table->width; x++) {
for (int y = 0; y < _table->height; y++) {
if (_table->boardState[x][y] != 0 || _table->numAdjacentPieces[x][y] == 0)
continue;
int scoreRecurse = scoreMoveAndRevert(x, y, depth - 1, bestScore);
if (scoreRecurse < bestScore) {
counter = 1;
bestMove = x * 100 + y;
bestScore = scoreRecurse;
} else {
if (scoreRecurse == bestScore) {
counter += 1;
uint rng = _random.getRandomNumber(UINT_MAX);
if ((rng % CAPTURE_SCORE) * counter < CAPTURE_SCORE) {
bestMove = x * 100 + y;
}
}
}
}
}
}
return bestMove;
}
int varsMoveToXY(byte hundreds, byte tens, byte ones, byte &x, byte &y) {
int move = hundreds * 100 + tens * 10 + ones;
x = move / 15;
y = 14 - move % 15;
return move;
}
void aiMoveToXY(int move, byte &x, byte &y) {
x = move / 100;
y = move % 100;
}
void moveToVars(int move, byte &hundreds, byte &tens, byte &ones) {
hundreds = move / 100;
tens = move % 100 / 10;
ones = move % 10;
}
int xyToMove(uint x, uint y) {
return x * 15 - y + 14;
}
void moveXYToVars(uint x, uint y, byte &hundreds, byte &tens, byte &ones) {
int move = xyToMove(x, y);
moveToVars(move, hundreds, tens, ones);
}
void PenteGame::animateCapturesCheckWinner(byte *vars) {
if (_animateCapturesBitMask != 0 && _nextCapturedSpot < 0) {
int16 capturedSpot;
animateCapture(_previousMove, &_animateCapturesBitMask, &capturedSpot, &_nextCapturedSpot);
vars[5] = 1;
moveToVars(capturedSpot, vars[0], vars[1], vars[2]);
return;
}
if (_animateCapturesBitMask != 0 || _nextCapturedSpot > -1) {
moveToVars(_nextCapturedSpot, vars[0], vars[1], vars[2]);
_nextCapturedSpot = -1;
vars[5] = 1;
return;
}
if (_table->playerScore >= WIN_SCORE || _table->moveCounter >= _table->boardSize)
vars[5] = 3; // player wins
else if (_table->staufScore >= WIN_SCORE)
vars[5] = 2; // Stauf wins
else {
// the match continues
vars[5] = 0;
return;
}
penteDeInit();
}
void PenteGame::opQueryPiece(byte *vars) {
// this runs multiple times to check if pieces belong to stauf or the player?
// this happens when you close the gamebook
byte x, y;
varsMoveToXY(vars[0], vars[1], vars[2], x, y);
byte piece = _table->boardState[x][y];
if (piece == 0) {
vars[3] = 0;
return;
}
if (piece == 0x4f) {
vars[3] = 2;
return;
}
if (piece != 0x58) {
return;
}
vars[3] = 1;
}
void PenteGame::run(byte *vars) {
byte op = vars[4];
if (_table == nullptr && op != 0) {
debugC(kDebugLogic, "pente Init, seed %u", _random.getSeed());
penteInit(20, 15, 5);
}
debugC(kDebugLogic, "penteOp vars[4]: %d", (int)op);
switch (op) {
case 0:
penteDeInit();
return;
case 1:
byte x, y;
_previousMove = varsMoveToXY(vars[0], vars[1], vars[2], x, y);
debugC(kDebugLogic, "player moved to %d, %d", (int)x, (int)y);
updateScore(x, y, _table->moveCounter % 2);
_animateCapturesBitMask = scoreCapture(x, y);
return;
case 2:
case 4:
animateCapturesCheckWinner(vars);
return;
case 3:
break;
case 5:
opQueryPiece(vars);
default:
return;
}
byte aiDepth = vars[6];
if (aiDepth == 0) {
aiDepth = 3;
} else if (aiDepth == 1) {
aiDepth = 4;
} else {
aiDepth = 5;
}
if (aiDepth != 2) {
if (_easierAi && aiDepth > 2)
aiDepth = 2;
_previousMove = aiGetBestMove(aiDepth);
}
else
warning("pente unknown else");
byte x, y;
aiMoveToXY(_previousMove, x, y);
debugC(kDebugLogic, "Stauf moved to %d, %d", (int)x, (int)y);
updateScore(x, y, _table->moveCounter % 2);
_animateCapturesBitMask = scoreCapture(x, y);
_previousMove = xyToMove(x, y);
moveXYToVars(x, y, vars[0], vars[1], vars[2]);
}
PenteGame::PenteGame(bool easierAi) : _random("PenteGame") {
_table = nullptr;
_nextCapturedSpot = -1;
_animateCapturesBitMask = 0;
_previousMove = 0;
#if 0
_easierAi = false;
test();
#endif
_easierAi = easierAi;
}
void PenteGame::test() {
warning("starting PenteGame::test()");
uint32 oldSeed = _random.getSeed();
// 6 moves per line
testGame(3,
{
/*x=*/10, /*y=*/6, /*x=*/9, /*y=*/6, /*x=*/10, /*y=*/7, /*x=*/10, /*y=*/5, /*x=*/10, /*y=*/8, /*x=*/9, /*y=*/9,
/*x=*/10, /*y=*/9, /*x=*/10, /*y=*/10, /*x=*/9, /*y=*/8, /*x=*/8, /*y=*/7, /*x=*/8, /*y=*/8, /*x=*/7, /*y=*/8,
/*x=*/6, /*y=*/9, /*x=*/11, /*y=*/4,
}, false);
testGame(10,
{
/*x=*/10, /*y=*/6, /*x=*/11, /*y=*/7, /*x=*/10, /*y=*/5, /*x=*/10, /*y=*/7, /*x=*/9, /*y=*/7, /*x=*/12, /*y=*/7,
/*x=*/10, /*y=*/4, /*x=*/8, /*y=*/8, /*x=*/10, /*y=*/3, /*x=*/11, /*y=*/5, /*x=*/10, /*y=*/2, /*x=*/9, /*y=*/7,
/*x=*/10, /*y=*/6,
}, true);
// test bottom left corner
testGame(1993,
{
/*x=*/0, /*y=*/0, /*x=*/1, /*y=*/1, /*x=*/1, /*y=*/0, /*x=*/2, /*y=*/0, /*x=*/0, /*y=*/1, /*x=*/0, /*y=*/2,
/*x=*/2, /*y=*/1, /*x=*/3, /*y=*/2, /*x=*/1, /*y=*/2, /*x=*/2, /*y=*/3, /*x=*/4, /*y=*/1, /*x=*/1, /*y=*/4,
/*x=*/5, /*y=*/1, /*x=*/6, /*y=*/1, /*x=*/3, /*y=*/0, /*x=*/5, /*y=*/2, /*x=*/4, /*y=*/3, /*x=*/3, /*y=*/1,
/*x=*/3, /*y=*/3, /*x=*/5, /*y=*/3, /*x=*/4, /*y=*/1, /*x=*/4, /*y=*/3, /*x=*/3, /*y=*/3, /*x=*/3, /*y=*/4,
/*x=*/2, /*y=*/5, /*x=*/7, /*y=*/0
}, false);
// test bottom right corner
testGame(1995,
{
/*x=*/19, /*y=*/0, /*x=*/18, /*y=*/1, /*x=*/19, /*y=*/1, /*x=*/18, /*y=*/2, /*x=*/18, /*y=*/0, /*x=*/18, /*y=*/3,
/*x=*/18, /*y=*/4, /*x=*/17, /*y=*/5, /*x=*/17, /*y=*/0, /*x=*/16, /*y=*/5, /*x=*/17, /*y=*/4, /*x=*/16, /*y=*/4,
/*x=*/18, /*y=*/5, /*x=*/18, /*y=*/6, /*x=*/18, /*y=*/5, /*x=*/15, /*y=*/3, /*x=*/18, /*y=*/4, /*x=*/14, /*y=*/2,
}, false);
// test top left corner
testGame(1996,
{
/*x=*/0, /*y=*/14, /*x=*/1, /*y=*/13, /*x=*/1, /*y=*/14, /*x=*/2, /*y=*/14, /*x=*/0, /*y=*/13, /*x=*/0, /*y=*/12,
/*x=*/1, /*y=*/12, /*x=*/2, /*y=*/11, /*x=*/2, /*y=*/12, /*x=*/3, /*y=*/12, /*x=*/4, /*y=*/13, /*x=*/1, /*y=*/10,
/*x=*/0, /*y=*/9, /*x=*/3, /*y=*/10, /*x=*/1, /*y=*/12, /*x=*/4, /*y=*/9, /*x=*/5, /*y=*/8, /*x=*/6, /*y=*/9,
/*x=*/3, /*y=*/11, /*x=*/6, /*y=*/10, /*x=*/6, /*y=*/11, /*x=*/4, /*y=*/8, /*x=*/3, /*y=*/9, /*x=*/4, /*y=*/10,
/*x=*/4, /*y=*/11, /*x=*/2, /*y=*/10, /*x=*/0, /*y=*/10, /*x=*/5, /*y=*/10
}, false);
// test top right corner
testGame(2019,
{
/*x=*/19, /*y=*/14, /*x=*/18, /*y=*/13, /*x=*/19, /*y=*/12, /*x=*/18, /*y=*/12, /*x=*/18, /*y=*/11, /*x=*/17, /*y=*/10,
/*x=*/18, /*y=*/14, /*x=*/16, /*y=*/11, /*x=*/18, /*y=*/9, /*x=*/15, /*y=*/12, /*x=*/14, /*y=*/13, /*x=*/15, /*y=*/10,
/*x=*/15, /*y=*/11, /*x=*/14, /*y=*/10, /*x=*/17, /*y=*/12, /*x=*/16, /*y=*/10, /*x=*/13, /*y=*/10, /*x=*/18, /*y=*/10
}, false);
for (uint32 i = 0; i < 10; i++)
testRandomGame(i);
_easierAi = true;
for (uint32 i = 10; i < 20; i++)
testRandomGame(i);
_random.setSeed(oldSeed);
warning("finished PenteGame::test()");
}
bool PenteGame::testGame(uint32 seed, Common::Array<int> moves, bool playerWin) {
byte vars[1024];
byte &winner = vars[5];
byte &op = vars[4];
warning("starting PenteGame::testGame(%u, %u, %d)", seed, moves.size(), (int)playerWin);
memset(vars, 0, sizeof(vars));
_random.setSeed(seed);
op = 0;
run(vars);
for (uint i = 0; i < moves.size(); i += 2) {
if (winner)
error("%u: early winner: %d", i, (int)winner);
int x = moves[i];
int y = moves[i + 1];
if (i % 4) {
// check Stauf's move
op = 3;
run(vars);
byte sX, sY;
varsMoveToXY(vars[0], vars[1], vars[2], sX, sY);
if (sX != x || sY != y)
error("%u: Stauf, expected (%d, %d), got (%d, %d)", i, (int)x, (int)y, (int)sX, (int)sY);
do {
op = 4;
run(vars);
} while (winner == 1);
continue;
}
moveXYToVars(x, y, vars[0], vars[1], vars[2]);
op = 1;
run(vars);
do {
op = 2;
run(vars);
} while (winner == 1);
}
if (playerWin && winner != 3)
error("player didn't win, winner: %d", (int)winner);
else if (playerWin == false && winner != 2)
error("Stauf didn't win, winner: %d", (int)winner);
warning("finished PenteGame::testGame(%u, %u, %d)", seed, moves.size(), (int)playerWin);
return true;
}
void PenteGame::testRandomGame(uint32 seed) {
byte vars[1024];
byte &winner = vars[5];
byte &op = vars[4];
warning("starting PenteGame::testRandomGame(%u)", seed);
memset(vars, 0, sizeof(vars));
_random.setSeed(seed);
op = 0;
run(vars);
while (1) {
// Player makes a random move
int x, y;
do {
x = _random.getRandomNumber(19);
y = _random.getRandomNumber(14);
} while (_table != nullptr && _table->boardState[x][y]);
moveXYToVars(x, y, vars[0], vars[1], vars[2]);
op = 1;
run(vars);
do {
op = 2;
run(vars);
} while (winner == 1);
if (winner)
break;
// Stauf's move
op = 3;
run(vars);
do {
op = 4;
run(vars);
} while (winner == 1);
if (winner)
break;
}
if (winner != 2)
error("Stauf didn't win, winner: %d", (int)winner);
warning("finished PenteGame::testRandomGame(%u)", seed);
}
} // namespace Groovie

View File

@@ -0,0 +1,85 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_PENTE_H
#define GROOVIE_LOGIC_PENTE_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Pente puzzle at the end of the game.
*/
struct pentePlayerTable;
struct penteTable;
class PenteGame {
public:
PenteGame(bool easierAi);
void run(byte *vars);
private:
void animateCapturesCheckWinner(byte *vars);
void opQueryPiece(byte *vars);
void addLine(int x, int y, int linesCounter);
void buildLookupTable();
void penteDeInit();
void penteInit(uint width, uint height, uint length);
int &getPlayerTable(bool staufTurn, pentePlayerTable *&pt);
void scoreLine(uint16 lineIndex, bool isStaufTurn, bool revert);
void calcTouchingPieces(byte moveX, byte moveY, bool revert);
void updateScore(byte x, byte y, bool whose_turn);
void revertScore(byte x, byte y);
byte scoreCaptureSingle(byte x, byte y, int slopeX, int slopeY);
uint scoreCapture(byte x, byte y);
void animateCapture(short move, byte *bitMaskG, short *outCapture1, short *outCapture2);
void revertCapture(byte x, byte y, byte y2);
int scoreMoveAndRevert(byte x, byte y, char depth, int parentScore, bool &gameOver);
int scoreMoveAndRevert(byte x, byte y, char depth, int parentScore);
int aiRecurseTail(int parentScore);
int aiRecurse(char depth, int parentScore);
uint16 aiGetBestMove(byte depth);
void test();
bool testGame(uint32 seed, Common::Array<int> moves, bool playerWin);
void testRandomGame(uint32 seed);
Common::RandomSource _random;
byte _animateCapturesBitMask;
short _previousMove;
short _nextCapturedSpot;
penteTable *_table;
bool _easierAi;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_PENTE_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_TLCGAME_H
#define GROOVIE_LOGIC_TLCGAME_H
#include "common/textconsole.h"
#include "common/random.h"
#define GROOVIE_TLC_MAX_EPSIODES (15)
#define GROOVIE_TLC_MAX_QUEST_EP (50)
namespace Groovie {
class GroovieEngine;
// The regions.rle contains 898 entries. Round about 18 kByte in memory.
struct TlcRegionsHeader {
char name[12];
int numAnswers;
uint32 offset;
};
struct TlcRegion {
uint16 left;
uint16 top;
uint16 right;
uint16 bottom;
};
struct TlcEpQuestionData {
bool questionUsed;
uint32 questionScore;
};
struct TlcTatHeader {
uint32 questionsNum;
uint32 questionsOffset;
uint8 binDividends[16];
};
struct TlcTatAnswer {
uint8 binScore[16];
};
struct TlcTatQuestions {
char name[6];
int answerCount;
TlcTatAnswer answerData[8];
};
class TlcGame
{
public:
#ifdef ENABLE_GROOVIE2
TlcGame(byte *scriptVariables);
~TlcGame();
static const char *getTlcMusicFilename(int musicId);
void handleOp(uint8 op);
/**
* Handle region commands. A region describes the coordinates of
* a rectangle as clickable area in the question dialogs. These regions
* are provided by a the extra file.
* screen coordinates.
*/
void opRegions();
/**
* Get the coordiantes of the region for the next answer. There are
* up to 8 answers possible for each question. In the script the
* coordinates are (0,0,0,0) which will be replace by the new ones.
* @param left Left value of the rectangle
* @param top Top value of the rectangle
* @param right Right value of the rectangle
* @param bottom Bottom value of the rectangle
* @return 0 if anwer was found. -1 in case no more answer
* available for this question
*/
int getRegionNext(uint16 &left, uint16 &top, uint16 &right, uint16 &bottom);
/**
* Rewinds the internal answer counter for the function
* getRegionNext()
*/
void getRegionRewind();
/**
* Handles some flags which are used during a TAT. The game seems to
* use this flags to skip some questions during a TAT.
* OpCode_0x42(2)
*/
void opFlags();
/**
* Handles all Exit Poll commands. The exit poll (EP) questions are
* described in detail in the file EPAIDB.RLE.
* OpCode_0x42(1)
*/
void opExitPoll();
/**
* Handles all TAT commands. The TAT questions are described in detail
* in the file TATAIDB.RLE
*/
void opTat();
private:
Common::RandomSource _random;
void inline setScriptVar(uint16 var, byte value);
void inline setScriptVar16(uint16 var, uint16 value);
uint16 inline getScriptVar16(uint16 var);
byte *_scriptVariables;
/**
* Loads the description part of the regions.rle file into memory
* This makes it faster to search for the correct quesion.
*/
void regionsInit();
void regionsLoad();
// Variables for region handling
int _numRegionHeaders;
int _curAnswerIndex;
int _curQuestNumAnswers;
TlcRegion _curQuestRegions[8];
TlcRegionsHeader *_regionHeader;
/**
* Functions for Exit Poll Commands
*/
void epInit();
void epSelectNextQuestion();
void epResultQuestion();
void epResultEpisode();
// Variables for Exit Poll handling
int16 _epScoreBin[6];
int _epEpisodeIdx; // 15 Episodes: 0..14
int _epQuestionIdx; // 1..X (questions in current episode. counted up for every question)
int _epQuestionNumOfPool; // 1..X (question number in the data base. The questions are played in random order)
int _epQuestionsInEpisode;
TlcEpQuestionData *_epQuestionsData;
// Variables for flag handling
byte _tatFlags[0x0E][0x09];
/**
* Functions for TAT Commands
*/
void tatInitRegs();
void tatLoadDB();
void tatLoadDBHeaders();
void tatResultQuest();
void tatResultEpisode();
void tatGetProfile();
void debugTatFlags(int y, int y2);
// Variables for TAT handling
int _tatEpisodes;
int _tatQuestCount;
TlcTatHeader *_tatHeaders;
TlcTatQuestions *_tatQuestions;
// uint8 _tatCoeffs[15][16];
#endif
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_TLCGAME_H

View File

@@ -0,0 +1,987 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/triangle.h"
namespace Groovie {
namespace {
extern const int8 triangleLookup1[12];
extern const int8 triangleLookup2[12];
extern const int8 triangleLookup3[12];
extern const int8 triangleLogicTable[924];
}
TriangleGame::TriangleGame() : _random("TriangleGame") {
init();
#if 0
test();
#endif
}
void TriangleGame::run(byte *scriptVariables) {
byte op = scriptVariables[3];
uint8 move;
switch (op) {
case 3:
init();
scriptVariables[3] = 0;
return;
case 4:
// Samantha AI
move = sub03(2);
break;
case 5:
// Stauf AI (only called after Samantha)
move = sub03(1);
break;
default:
// Player and then Stauf
debugC(kDebugLogic, "player chose spot %d", (int)(scriptVariables[1]) + (10 * (int)scriptVariables[0]));
setCell(scriptVariables[1] + 10 * scriptVariables[0], 2);
scriptVariables[3] = sub02();
if (scriptVariables[3] == 0) {
move = sub03(1);
} else {
debugC(kDebugLogic, "winner: %d", (int)scriptVariables[3]);
return;
}
}
scriptVariables[0] = move / 10;
scriptVariables[1] = move % 10;
scriptVariables[3] = sub02();
debugC(kDebugLogic, "stauf chose spot %d, winner: %d", (int)move, (int)scriptVariables[3]);
}
void TriangleGame::init() {
debugC(kDebugLogic, "TriangleGame::init(), seed: %u", _random.getSeed());
_triangleCellCount = 0;
memset(_triangleCells, 0, 66);
}
int8 TriangleGame::sub02() {
int8 v6[132];
int8 v7[68];
sub05(_triangleCells, v6, v7);
for (int i = 0; v6[i] != 66; i++) {
bool v1 = false;
bool v2 = false;
bool pl = false;
// There could be several sections, each one
// ends with 66. And the overall list ends with 66 too
// Hence, two loops
for (; v6[i] != 66; i++) {
if (!triangleLogicTable[14 * v6[i] + 6])
pl = true;
if (!triangleLogicTable[14 * v6[i] + 7])
v2 = true;
if (!triangleLogicTable[14 * v6[i] + 8])
v1 = true;
}
if (pl && v2 && v1)
return _triangleCells[v6[i - 1]];
}
return 0;
}
int8 TriangleGame::sub03(int8 player) {
int8 pickedMoves[4];
int8 tempTriangle1[68];
int8 tempTriangle2[68];
int8 a6a[132];
int8 tempTriangle3[68];
int8 tempMoves[132];
int8 pos;
if (_triangleCellCount >= 2) {
sub05(_triangleCells, tempMoves, tempTriangle3);
sub07(tempMoves, _triangleCells, tempTriangle3, tempTriangle2, tempTriangle1, a6a);
// Find move until valid one
(pos = sub09(player, tempTriangle2, tempTriangle1, a6a, _triangleCells)) != 66 ||
(pos = sub10(player, tempTriangle1, _triangleCells)) != 66 ||
(pos = sub12(player, a6a, _triangleCells, tempTriangle1)) != 66 ||
(pos = sub09(3 - player, tempTriangle2, tempTriangle1, a6a, _triangleCells));
if (pos == 66) {
pos = _random.getRandomNumber(65);
int8 oldPos = pos;
while (_triangleCells[pos]) {
if (++pos > 65)
pos = 0;
if (oldPos == pos) {
pos = 66;
break;
}
}
}
} else {
int8 max = 0;
if (!_triangleCells[24]) {
pickedMoves[0] = 24;
max = 1;
}
if (!_triangleCells[31])
pickedMoves[max++] = 31;
if (!_triangleCells[32])
pickedMoves[max++] = 32;
if (max)
pos = pickedMoves[_random.getRandomNumber(max - 1)];
else {
warning("TriangleGame: Undefined behaviour");
pos = 0; // tempMoves is uninitalized in this branch, so just return 0
}
}
if (pos != 66)
setCell(pos, player);
return pos;
}
void TriangleGame::sub05(int8 *triangleCells, int8 *tempMoves, int8 *tempTriangle) {
int8 dest[8];
for (int i = 0; i < 66; i++)
tempTriangle[i] = triangleCells[i];
int v16 = 3;
for (int j = 0; j < 66; ++j) {
if (triangleCells[j]) {
bool flag = false;
copyLogicRow(j, triangleCells[j], dest);
for (int8 *k = dest; *k != 66; k++) {
if (j > *k) {
if (flag) {
if (tempTriangle[j] != tempTriangle[*k])
replaceCells(tempTriangle, j, tempTriangle[j], tempTriangle[*k]);
} else {
flag = true;
tempTriangle[j] = tempTriangle[*k];
}
}
}
if (!flag)
tempTriangle[j] = v16++;
}
}
int v11 = 0;
if (v16 > 3) {
for (int v12 = 3; v12 < v16; v12++) {
int v14 = v11;
for (int m = 0; m < 66; m++) {
if (tempTriangle[m] == v12)
tempMoves[v11++] = m;
}
if (v11 != v14)
tempMoves[v11++] = 66;
}
}
tempMoves[v11] = 66;
}
void TriangleGame::sub07(int8 *tempMoves, int8 *triangleCells, int8 *tempTriangle3, int8 *tempTriangle2, int8 *tempTriangle1, int8 *tempMoves2) {
int8 singleRow[8];
int8 tempTriangle[68];
int8 routes[4356];
for (int i = 0; i < 66; i++) {
tempTriangle2[i] = tempTriangle3[i];
tempTriangle1[i] = 0;
}
if (*tempMoves != 66) {
for (int8 *ptr1 = tempMoves; *ptr1 != 66; ptr1++) {
int8 *v45 = &routes[66 * tempTriangle3[*ptr1]];
*v45 = 66;
while (*ptr1 != 66) {
copyLogicRow(*ptr1++, 0, singleRow);
collapseLoops(v45, singleRow);
}
}
for (int8 *ptr2 = tempMoves; *ptr2 != 66; ptr2++) {
int8 *j;
for (j = ptr2; *j != 66; j++)
;
for (int8 *k = j + 1; *k != 66; k++) {
if (triangleCells[*k] == triangleCells[*ptr2]) {
int8 val1 = tempTriangle2[*k];
int8 val2 = tempTriangle2[*ptr2];
if (val1 != val2) {
int8 lookupLen = copyLookup(&routes[66 * val2], &routes[66 * val1], tempTriangle);
if (lookupLen == 1) {
if (triangleCells[*ptr2] == 1) {
tempTriangle1[tempTriangle[0]] |= 1u;
} else if (triangleCells[*ptr2] == 2) {
tempTriangle1[tempTriangle[0]] |= 0x40u;
}
} else if (lookupLen > 1) {
int8 v16 = lookupLen - 1;
do {
if (triangleCells[*ptr2] == 1) {
tempTriangle1[tempTriangle[v16]] |= 0x10u;
} else if (triangleCells[*ptr2] == 2) {
tempTriangle1[tempTriangle[v16]] |= 0x20u;
}
} while (v16--);
collapseLoops(&routes[66 * tempTriangle2[*k]], &routes[66 * tempTriangle2[*ptr2]]);
int8 from = tempTriangle2[*ptr2];
int8 to = tempTriangle2[*k];
for (int m = 0; m < 66; m++) {
if (tempTriangle2[m] == from)
tempTriangle2[m] = to;
}
}
}
}
for (; *k != 66; k++)
;
}
for (; *ptr2 != 66; ptr2++)
;
}
}
int8 maxVal = 0;
for (int i = 0; i < 66; i++) {
if (tempTriangle2[i] > maxVal)
maxVal = tempTriangle2[i];
}
int8 len2 = 0;
for (int i = 3; i <= maxVal; i++) {
int8 prevLen2 = len2;
for (int j = 0; j < 66; j++) {
if (tempTriangle2[j] == i)
tempMoves2[len2++] = j;
}
if (prevLen2 != len2)
tempMoves2[len2++] = 66;
}
tempMoves2[len2] = 66;
for (int8 *ptr3 = tempMoves2; *ptr3 != 66; ptr3++) {
bool flag1 = false, flag2 = false, flag3 = false;
int8 row = tempTriangle2[*ptr3];
byte mask1 = 0, mask2 = 0;
for (int i = 0; i < 66; i++) {
if (tempTriangle2[i] == row && triangleCells[i]) {
if (triangleCells[i] == 1) {
mask1 = 1;
mask2 = 16;
} else {
mask1 = 64;
mask2 = 32;
}
break;
}
}
while (*ptr3 != 66) {
if (!triangleLogicTable[14 * *ptr3 + 6])
flag1 = 1;
if (!triangleLogicTable[14 * *ptr3 + 7])
flag2 = 1;
if (!triangleLogicTable[14 * *ptr3 + 8])
flag3 = 1;
++ptr3;
}
if (!flag1) {
int8 lookup1 = copyLookup(triangleLookup1, &routes[66 * row], tempTriangle);
if (lookup1 == 1) {
tempTriangle1[tempTriangle[0]] |= mask1;
} else if (lookup1 > 1) {
int k = lookup1 - 1;
do
tempTriangle1[tempTriangle[k]] |= mask2;
while (k--);
flag1 = 1;
}
}
if (!flag2) {
int8 lookup2 = copyLookup(triangleLookup2, &routes[66 * row], tempTriangle);
if (lookup2 == 1) {
tempTriangle1[tempTriangle[0]] |= mask1;
} else if (lookup2 > 1) {
int k = lookup2 - 1;
do
tempTriangle1[tempTriangle[k]] |= mask2;
while (k--);
flag2 = 1;
}
}
if (!flag3) {
int8 lookup3 = copyLookup(triangleLookup3, &routes[66 * row], tempTriangle);
if (lookup3 == 1) {
tempTriangle1[tempTriangle[0]] |= mask1;
} else if (lookup3 > 1) {
int k = lookup3 - 1;
do
tempTriangle1[tempTriangle[k]] |= mask2;
while (k--);
flag3 = 1;
}
}
byte mask3 = 0;
if (flag1)
mask3 = 4;
if (flag2)
mask3 |= 8u;
if (flag3)
mask3 |= 2u;
for (int i = 0; i < 66; i++) {
if (tempTriangle2[i] == row)
tempTriangle1[i] |= mask3;
}
}
}
int8 TriangleGame::sub09(int8 player, int8 *tempTriangle2, int8 *tempTriangle1, int8 *a4, int8 *triangleCells) {
int8 movesTable[280];
int numDir1 = 0;
int numDir2 = 0;
int numDir3 = 0;
int numDir4 = 0;
int row = 0;
for (const int8 *tPtr = &triangleLogicTable[6]; tPtr < &triangleLogicTable[924]; tPtr += 14, row++) {
if (!triangleCells[row] && (tempTriangle1[row] & (player == 1 ? 1 : 64)) != 0) {
int c1 = 0, c2 = 0, c3 = 0;
int mask = 0;
copyLogicRow(row, player, movesTable);
for (int8 *movPtr = movesTable; *movPtr != 66; ++movPtr) {
int row2 = 0;
mask |= tempTriangle1[*movPtr];
for (const int8 *tPtr2 = &triangleLogicTable[6]; tPtr2 < &triangleLogicTable[924]; tPtr2 += 14, row2++) {
if (tempTriangle2[row2] == tempTriangle2[*movPtr]) {
c1 += (tPtr2[0] == 0) ? 1 : 0;
c2 += (tPtr2[1] == 0) ? 1 : 0;
c3 += (tPtr2[2] == 0) ? 1 : 0;
}
}
}
if (c1)
mask &= ~4u;
if (c2)
mask &= ~8u;
if (c3)
mask &= ~2u;
if (tPtr[0] || c1) {
if (tPtr[1] || c2) {
if (tPtr[2] || c3) {
if (mask) {
if (mask == 0xe) {
movesTable[numDir2 + 76] = row;
numDir2++;
} else if (mask == 2 || mask == 8 || mask == 4) {
movesTable[numDir4 + 212] = row;
numDir4++;
} else {
movesTable[numDir3 + 144] = row;
numDir3++;
}
}
} else {
movesTable[numDir1 + 8] = row;
numDir1++;
}
} else {
movesTable[numDir1 + 8] = row;
numDir1++;
}
} else {
movesTable[numDir1 + 8] = row;
numDir1++;
}
}
}
if (numDir1)
return movesTable[_random.getRandomNumber(numDir1 - 1) + 8];
if (numDir2)
return movesTable[_random.getRandomNumber(numDir2 - 1) + 76];
if (numDir3)
return movesTable[_random.getRandomNumber(numDir3 - 1) + 144];
if (numDir4)
return movesTable[_random.getRandomNumber(numDir4 - 1) + 212];
return 66;
}
int8 TriangleGame::sub10(int8 player, int8 *a2, int8 *triangleCells) {
int8 *destPtr;
byte mask;
int counter;
int8 dest1[8];
int8 dest2[68];
mask = 0;
counter = 0;
if (player == 1)
mask = 16;
else if (player == 2)
mask = 32;
for (int i = 0; i < 66; ++i) {
if (!triangleCells[i] && (mask & (byte)a2[i]) != 0) {
copyLogicRow(i, player, dest1);
destPtr = dest1;
while (*destPtr != 66) {
if ((a2[*destPtr] & 0xE) == 0xE) {
dest2[counter] = i;
counter++;
break;
}
destPtr++;
}
}
}
if (counter)
return dest2[_random.getRandomNumber(counter - 1)];
return 66;
}
int8 TriangleGame::sub12(int8 player, int8 *tempMoves, int8 *triangleCells, int8 *tempTriangle) {
int8 moves[8];
int8 tempTriangle1[68];
int8 tempTriangle2[68];
int8 tempTriangle3[68];
int8 tempTriangle4[68];
int8 result = 66;
int maxlen = -1;
int8 *startPtr = triangleCells;
for (int8 *ptr = tempMoves; *ptr != 66; ptr++) {
int len = 0;
int8 *beg = ptr;
int8 p0 = *ptr;
for (; *ptr != 66; ++ptr)
++len;
if (len > maxlen && triangleCells[p0] == player) {
maxlen = len;
startPtr = beg;
}
}
tempTriangle4[0] = 66;
for (int8 *ptr = startPtr; *ptr != 66; ++ptr) {
if (sub13(*ptr, triangleCells, moves))
collapseLoops(tempTriangle4, moves);
}
int len1 = 0, len2 = 0, len3 = 0;
tempTriangle1[0] = 66;
tempTriangle2[0] = 66;
tempTriangle3[0] = 66;
for (int8 *ptr = tempTriangle4; *ptr != 66; ++ptr) {
int8 v13 = 100;
int8 v15 = triangleLogicTable[14 * *ptr + 11];
if (v15 < 100)
v13 = triangleLogicTable[14 * *ptr + 11];
int8 v16 = triangleLogicTable[14 * *ptr + 13];
if (v13 > v16)
v13 = triangleLogicTable[14 * *ptr + 13];
int8 v17 = triangleLogicTable[14 * *ptr + 12];
if (v13 > v17)
v13 = triangleLogicTable[14 * *ptr + 12];
if (v13 == v15) {
tempTriangle1[len1++] = *ptr;
} else if (v13 == v17) {
tempTriangle2[len2++] = *ptr;
} else if (v13 == v16) {
tempTriangle3[len3++] = *ptr;
}
}
bool flag1 = false, flag2 = false, flag3 = false;
tempTriangle3[len3] = 66;
tempTriangle2[len2] = 66;
tempTriangle1[len1] = 66;
int8 startVal = tempTriangle[*startPtr];
switch (startVal) {
case 8:
flag3 = true;
break;
case 4:
flag2 = true;
break;
case 2:
flag1 = true;
break;
case 12:
flag2 = true;
flag3 = flag2;
break;
case 6:
flag1 = true;
flag2 = true;
break;
case 10:
flag1 = true;
flag3 = true;
break;
case 14:
flag2 = false;
flag1 = false;
flag3 = flag2;
break;
default:
flag2 = true;
flag1 = true;
flag3 = flag2;
break;
}
int minLen = 101;
if (flag1) {
for (int8 *ptr = tempTriangle1; *ptr != 66; ++ptr) {
int8 part1 = 0;
if ((startVal & 8) == 0)
part1 = triangleLogicTable[14 * *ptr + 7];
int8 part2 = 0;
if ((startVal & 4) == 0)
part2 = triangleLogicTable[14 * *ptr + 6];
if (minLen > part1 + part2) {
minLen = part1 + part2;
result = *ptr;
}
}
}
if (flag2) {
for (int8 *ptr = tempTriangle2; *ptr != 66; ++ptr) {
int8 part1 = 0;
if ((startVal & 8) == 0)
part1 = triangleLogicTable[14 * *ptr + 7];
int8 part2 = 0;
if ((startVal & 2) == 0)
part2 = triangleLogicTable[14 * *ptr + 8];
if (minLen > part1 + part2) {
minLen = part1 + part2;
result = *ptr;
}
}
}
if (flag3) {
for (int8 *ptr = tempTriangle3; *ptr != 66; ++ptr) {
int8 part1 = 0;
if ((startVal & 2) == 0)
part1 = triangleLogicTable[14 * *ptr + 8];
int8 part2 = 0;
if ((startVal & 4) == 0)
part2 = triangleLogicTable[14 * *ptr + 6];
if (minLen > part1 + part2) {
minLen = part1 + part2;
result = *ptr;
}
}
}
return result;
}
int TriangleGame::sub13(int8 row, int8 *triangleCells, int8 *moves) {
int pos = 0;
for (int i = 0; i < 6; i++) {
int8 v6 = triangleLogicTable[14 * row + i];
if (v6 != -1 && !triangleCells[v6]) {
int v7 = (i + 1) % 6;
int8 v8 = triangleLogicTable[14 * row + v7];
if (v8 != -1 && !triangleCells[v8]) {
int8 v9 = triangleLogicTable[14 * v6 + v7];
if (v9 != -1 && !triangleCells[v9])
moves[pos++] = v9;
}
}
}
moves[pos] = 66;
return pos;
}
void TriangleGame::setCell(int8 cellnum, int8 val) {
assert(cellnum >= 0);
assert(cellnum < 66);
if (cellnum >= 0 && cellnum < 66) {
++_triangleCellCount;
assert(_triangleCells[cellnum] == 0);
_triangleCells[cellnum] = val;
}
}
void TriangleGame::copyLogicRow(int row, int8 key, int8 *dest) {
int pos = 0;
for (int i = 0; i < 6; i++) {
int8 val = triangleLogicTable[14 * row + i];
if (val != -1 && _triangleCells[val] == key)
dest[pos++] = val;
}
dest[pos] = 66;
}
void TriangleGame::replaceCells(int8 *tempTriangle, int limit, int8 from, int8 to) {
for (int i = 0; i <= limit; ++i) {
if (tempTriangle[i] == from)
tempTriangle[i] = to;
}
}
int TriangleGame::copyLookup(const int8 *lookup, int8 *start, int8 *dest){
int counter = 0;
if (*lookup == 66) {
*dest = 66;
return counter;
}
for (; *lookup != 66; lookup++) {
for (int8 *ptr = start; *ptr != 66; ptr++) {
if (*ptr == *lookup)
dest[counter++] = *lookup;
}
}
dest[counter] = 66;
return counter;
}
void TriangleGame::collapseLoops(int8 *route, int8 *singleRow) {
int len = 0;
for (int8 *i = route; *i != 66; i++)
len++;
int origlen = len;
for (int8 *i = singleRow; *i != 66; i++) {
int j;
for (j = 0; j < len; j++) {
if (route[j] == *i)
break;
}
if (j == len)
route[len++] = *i;
}
if (len != origlen)
route[len] = 66;
}
bool TriangleGame::testGame(uint32 seed, const Common::Array<uint8> moves, bool playerWin) {
byte vars[1024];
byte &op = vars[3];
byte &move10s = vars[0];
byte &move1s = vars[1];
byte &winner = vars[3];
memset(vars, 0, sizeof(vars));
op = 3;
run(vars);
warning("starting TriangleGame::testGame(%u, %u, %d)", seed, moves.size(), (int)playerWin);
_random.setSeed(seed);
for (uint i = 0; i < moves.size(); i++) {
if (i % 2) {
// check Stauf's move
uint8 move = ((uint)move10s * 10) + (uint)move1s;
if (move != moves[i]) {
error("%u: bad Stauf move: %d", (int)i, (int)move);
// return false here is useful for finding the right seed to test
return false;
}
continue;
}
// else, input player's move
if (winner != 0)
error("%u: early winner: %d", (int)i, (int)winner);
uint8 move = moves[i];
move10s = move / 10;
move1s = move % 10;
op = 0;
run(vars);
}
if (playerWin && winner != 2)
error("player didn't win, winner: %d", (int)winner);
if (playerWin == false && winner != 1)
error("Stauf didn't win, winner: %d", (int)winner);
warning("finished TriangleGame::testGame(%u, %u, %d)", seed, moves.size(), (int)playerWin);
return true;
}
void TriangleGame::ensureSamanthaWin(uint32 seed) {
byte vars[1024];
byte &op = vars[3];
byte &winner = vars[3];
op = 3;
run(vars);
warning("starting TriangleGame::ensureSamanthaWin(%u)", seed);
_random.setSeed(seed);
for (int i = 0; i < 100; i++) {
// Samantha
op = 4;
run(vars);
if (winner)
break;
// Stauf
op = 5;
run(vars);
if (winner)
break;
}
if (winner != 2)
error("Samantha didn't win, winner: %d", (int)winner);
warning("finished TriangleGame::ensureSamanthaWin(%u)", seed);
}
void TriangleGame::testPlayRandomly(uint32 seed) {
byte vars[1024];
byte &op = vars[3];
byte &move10s = vars[0];
byte &move1s = vars[1];
byte &winner = vars[3];
op = 3;
run(vars);
warning("starting TriangleGame::testPlayRandomly(%u)", seed);
_random.setSeed(seed);
for (int i = 0; i < 100; i++) {
// Player make random move
uint8 move = 0;
do {
move = _random.getRandomNumber(65);
} while (_triangleCells[move]);
move10s = move / 10;
move1s = move % 10;
op = 0;
run(vars);
if (winner)
break;
// Stauf
op = 5;
run(vars);
if (winner)
break;
}
if (winner != 1)
error("Stauf didn't win, winner: %d", (int)winner);
warning("finished TriangleGame::testPlayRandomly(%u)", seed);
}
void TriangleGame::test() {
warning("starting TriangleGame::test");
uint32 oldSeed = _random.getSeed();
// Samantha appears to not always win, but she usually does, and she wins these seeds
// haven't verified if she always wins in the original game
for (uint32 i = 100; i < 105; i++)
ensureSamanthaWin(i);
// Similar thing here, technically there might be a seed where the player wins, but Stauf should win the vast majority of them
for (uint32 i = 200; i < 205; i++)
testPlayRandomly(i);
testGame(1, {24, 32, 30, 42, 37, 53, 45, 39, 19, 47, 20, 56, 55, 59, 36, 49, 29, 46, 23, 38, 18}, true);
testGame(1, {24, 32, 30, 42, 37, 53, 19, 39, 45, 47, 46, 59, 56, 49, 38, 48, 31, 40, 25, 50, 20}, true);
testGame(2, {24, 31, 33, 38, 43, 46, 16, 41, 54, 52, 64, 61, 53, 37, 42, 51, 32, 40, 23, 60, 15}, true);
testGame(2, {24, 31, 33, 38, 43, 46, 16, 41, 53, 52, 64, 61, 54, 37, 34, 50, 25, 36, 17, 0, 10}, true);
testGame(40680, {0, 24, 1, 12, 2, 4, 3, 5, 6, 30, 7, 9, 8, 29, 10, 13, 11, 47, 14, 18, 20, 37, 19, 36, 27, 57, 26, 31}, false);
_random.setSeed(oldSeed);
warning("finished TriangleGame::test");
}
namespace {
const int8 triangleLookup1[12] = {
0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66
};
const int8 triangleLookup2[12] = {
0, 2, 5, 9, 14, 20, 27, 35, 44, 54, 65, 66
};
const int8 triangleLookup3[12] = {
55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66
};
const int8 triangleLogicTable[924] = {
-1, -1, 2, 1, -1, -1, 0, 0, 10, 10, 6, 0, 10, 10,
0, 2, 4, 3, -1, -1, 0, 1, 9, 9, 5, 1, 9, 9,
-1, -1, 5, 4, 1, 0, 1, 0, 9, 11, 5, 1, 9, 9,
1, 4, 7, 6, -1, -1, 0, 2, 8, 8, 4, 2, 8, 8,
2, 5, 8, 7, 3, 1, 1, 1, 8, 10, 4, 1, 8, 8,
-1, -1, 9, 8, 4, 2, 2, 0, 8, 12, 4, 2, 8, 8,
3, 7, 11, 10, -1, -1, 0, 3, 7, 7, 3, 3, 7, 7,
4, 8, 12, 11, 6, 3, 1, 2, 7, 9, 3, 2, 7, 7,
5, 9, 13, 12, 7, 4, 2, 1, 7, 11, 3, 2, 7, 7,
-1, -1, 14, 13, 8, 5, 3, 0, 7, 13, 3, 3, 7, 7,
6, 11, 16, 15, -1, -1, 0, 4, 6, 6, 3, 4, 6, 6,
7, 12, 17, 16, 10, 6, 1, 3, 6, 8, 2, 3, 6, 6,
8, 13, 18, 17, 11, 7, 2, 2, 6, 10, 2, 2, 6, 6,
9, 14, 19, 18, 12, 8, 3, 1, 6, 12, 2, 3, 6, 6,
-1, -1, 20, 19, 13, 9, 4, 0, 6, 14, 3, 4, 6, 6,
10, 16, 22, 21, -1, -1, 0, 5, 5, 5, 3, 5, 5, 5,
11, 17, 23, 22, 15, 10, 1, 4, 5, 7, 2, 4, 5, 5,
12, 18, 24, 23, 16, 11, 2, 3, 5, 9, 1, 3, 5, 5,
13, 19, 25, 24, 17, 12, 3, 2, 5, 11, 1, 3, 5, 5,
14, 20, 26, 25, 18, 13, 4, 1, 5, 13, 2, 4, 5, 5,
-1, -1, 27, 26, 19, 14, 5, 0, 5, 15, 3, 5, 5, 5,
15, 22, 29, 28, -1, -1, 0, 6, 4, 4, 3, 6, 6, 4,
16, 23, 30, 29, 21, 15, 1, 5, 4, 6, 2, 5, 5, 4,
17, 24, 31, 30, 22, 16, 2, 4, 4, 8, 1, 4, 4, 4,
18, 25, 32, 31, 23, 17, 3, 3, 4, 10, 0, 3, 4, 4,
19, 26, 33, 32, 24, 18, 4, 2, 4, 12, 1, 4, 4, 4,
20, 27, 34, 33, 25, 19, 5, 1, 4, 14, 2, 5, 4, 5,
-1, -1, 35, 34, 26, 20, 6, 0, 4, 16, 3, 6, 4, 6,
21, 29, 37, 36, -1, -1, 0, 7, 3, 3, 3, 7, 7, 3,
22, 30, 38, 37, 28, 21, 1, 6, 3, 5, 2, 6, 6, 3,
23, 31, 39, 38, 29, 22, 2, 5, 3, 7, 1, 5, 5, 3,
24, 32, 40, 39, 30, 23, 3, 4, 3, 9, 0, 4, 4, 3,
25, 33, 41, 40, 31, 24, 4, 3, 3, 11, 0, 4, 3, 4,
26, 34, 42, 41, 32, 25, 5, 2, 3, 13, 1, 5, 3, 5,
27, 35, 43, 42, 33, 26, 6, 1, 3, 15, 2, 6, 3, 6,
-1, -1, 44, 43, 34, 27, 7, 0, 3, 17, 3, 7, 3, 7,
28, 37, 46, 45, -1, -1, 0, 8, 2, 2, 4, 8, 8, 2,
29, 38, 47, 46, 36, 28, 1, 7, 2, 4, 3, 7, 7, 2,
30, 39, 48, 47, 37, 29, 2, 6, 2, 6, 2, 6, 6, 2,
31, 40, 49, 48, 38, 30, 3, 5, 2, 8, 1, 5, 5, 3,
32, 41, 50, 49, 39, 31, 4, 4, 2, 10, 1, 4, 4, 4,
33, 42, 51, 50, 40, 32, 5, 3, 2, 12, 1, 5, 3, 5,
34, 43, 52, 51, 41, 33, 6, 2, 2, 14, 2, 6, 2, 6,
35, 44, 53, 52, 42, 34, 7, 1, 2, 16, 3, 7, 2, 7,
-1, -1, 54, 53, 43, 35, 8, 0, 2, 18, 4, 8, 2, 8,
36, 46, 56, 55, -1, -1, 0, 9, 1, 1, 5, 9, 9, 1,
37, 47, 57, 56, 45, 36, 1, 8, 1, 3, 4, 8, 8, 1,
38, 48, 58, 57, 46, 37, 2, 7, 1, 5, 3, 7, 7, 2,
39, 49, 59, 58, 47, 38, 3, 6, 1, 7, 2, 6, 6, 3,
40, 50, 60, 59, 48, 39, 4, 5, 1, 9, 2, 5, 5, 4,
41, 51, 61, 60, 49, 40, 5, 4, 1, 11, 2, 5, 4, 5,
42, 52, 62, 61, 50, 41, 6, 3, 1, 13, 2, 6, 3, 6,
43, 53, 63, 62, 51, 42, 7, 2, 1, 15, 3, 7, 2, 7,
44, 54, 64, 63, 52, 43, 8, 1, 1, 17, 4, 8, 1, 8,
-1, -1, 65, 64, 53, 44, 9, 0, 1, 19, 5, 9, 1, 9,
45, 56, -1, -1, -1, -1, 0, 10, 0, 0, 6, 10, 10, 0,
46, 57, -1, -1, 55, 45, 1, 9, 0, 2, 5, 9, 9, 1,
47, 58, -1, -1, 56, 46, 2, 8, 0, 4, 4, 8, 8, 2,
48, 59, -1, -1, 57, 47, 3, 7, 0, 6, 3, 7, 7, 3,
49, 60, -1, -1, 58, 48, 4, 6, 0, 8, 3, 6, 6, 4,
50, 61, -1, -1, 59, 49, 5, 5, 0, 10, 3, 5, 5, 5,
51, 62, -1, -1, 60, 50, 6, 4, 0, 12, 3, 6, 4, 6,
52, 63, -1, -1, 61, 51, 7, 3, 0, 14, 3, 7, 3, 7,
53, 64, -1, -1, 62, 52, 8, 2, 0, 16, 4, 8, 2, 8,
54, 65, -1, -1, 63, 53, 9, 1, 0, 18, 5, 9, 1, 9,
-1, -1, -1, -1, 64, 54, 10, 0, 0, 20, 6, 10, 0, 10
};
} // End of anonymous namespace
} // End of Groovie namespace

View File

@@ -0,0 +1,71 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_TRIANGLE_H
#define GROOVIE_LOGIC_TRIANGLE_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
class TriangleGame {
public:
TriangleGame();
void run(byte *scriptVariables);
private:
void init();
int8 sub02();
int8 sub03(int8 player);
void sub05(int8 *triangleCells, int8 *a2, int8 *a3);
void sub07(int8 *tempMoves, int8 *triangleCells, int8 *tempTriangle3, int8 *tempTriangle2, int8 *tempTriangle1, int8 *tempMoves2);
int8 sub09(int8 key, int8 *a2, int8 *a3, int8 *a4, int8 *triangleCells);
int8 sub10(int8 key, int8 *a2, int8 *triangleCells);
int8 sub12(int8 a1, int8 *a2, int8 *triangleCells, int8 *a4);
int sub13(int8 row, int8 *triangleCells, int8 *moves);
void setCell(int8 cellnum, int8 val);
void copyLogicRow(int row, int8 key, int8 *dest);
void replaceCells(int8 *tempTriangle, int limit, int8 from, int8 to);
int copyLookup(const int8 *lookup, int8 *start, int8 *dest);
void collapseLoops(int8 *route, int8 *singleRow);
bool testGame(uint32 seed, Common::Array<uint8> moves, bool player_win);
void ensureSamanthaWin(uint32 seed);
void testPlayRandomly(uint32 seed);
void test();
private:
int _triangleCellCount;
int8 _triangleCells[66];
Common::RandomSource _random;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_TRIANGLE_H

View File

@@ -0,0 +1,642 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/winerack.h"
namespace Groovie {
enum WineBottle {
kWineBottleOpponent = 1,
kWineBottlePlayer = 2
};
namespace {
extern const int8 wineRackLogicTable[1200];
}
WineRackGame::WineRackGame() : _random("WineRackGame"), _totalBottles(0) {
memset(_wineRackGrid, 0, 100);
#if 0
runTests();
#endif
}
void WineRackGame::run(byte *scriptVariables) {
char op = scriptVariables[3];
byte pos = 0;
/* positions on the board
* north = 9 (0, 9)
south = 90 (9, 0)
east = 99 (9, 9)
west = 0 (0, 0)
*/
switch (op) {
case 3:
debugC(kDebugLogic, "WineRackGame initGrid, seed: %u", _random.getSeed());
initGrid(scriptVariables[4]);
break;
case 4: // Player's move
pos = calculateNextMove(kWineBottlePlayer);
placeBottle(pos, kWineBottlePlayer);
scriptVariables[0] = pos / 10;
scriptVariables[1] = pos % 10;
scriptVariables[3] = didPlayerWin();
break;
case 5: // Opponent's move
scriptVariables[3] = 0;
pos = calculateNextMove(kWineBottleOpponent);
placeBottle(pos, kWineBottleOpponent);
scriptVariables[0] = pos / 10;
scriptVariables[1] = pos % 10;
scriptVariables[3] = didAiWin() != 0 ? 1 : 0;
break;
default:
scriptVariables[3] = 0;
placeBottle(scriptVariables[0] * 10 + scriptVariables[1], 2);
if (didPlayerWin()) {
scriptVariables[3] = 2;
} else {
pos = calculateNextMove(kWineBottleOpponent);
placeBottle(pos, kWineBottleOpponent);
scriptVariables[0] = pos / 10;
scriptVariables[1] = pos % 10;
scriptVariables[3] = didAiWin() != 0 ? 1 : 0;
}
break;
}
}
void WineRackGame::initGrid(byte difficulty) {
memset(_wineRackGrid, 0, 100);
switch (difficulty) {
case 0:
_totalBottles = 24;
_wineRackGrid[15] = kWineBottlePlayer;
_wineRackGrid[18] = kWineBottleOpponent;
_wineRackGrid[19] = kWineBottleOpponent;
_wineRackGrid[20] = kWineBottleOpponent;
_wineRackGrid[21] = kWineBottleOpponent;
_wineRackGrid[22] = kWineBottleOpponent;
_wineRackGrid[23] = kWineBottleOpponent;
_wineRackGrid[25] = kWineBottlePlayer;
_wineRackGrid[26] = kWineBottleOpponent;
_wineRackGrid[27] = kWineBottleOpponent;
_wineRackGrid[28] = kWineBottleOpponent;
_wineRackGrid[33] = kWineBottleOpponent;
_wineRackGrid[34] = kWineBottleOpponent;
_wineRackGrid[35] = kWineBottlePlayer;
_wineRackGrid[36] = kWineBottleOpponent;
_wineRackGrid[44] = kWineBottlePlayer;
_wineRackGrid[45] = kWineBottlePlayer;
_wineRackGrid[54] = kWineBottlePlayer;
_wineRackGrid[62] = kWineBottlePlayer;
_wineRackGrid[63] = kWineBottlePlayer;
_wineRackGrid[64] = kWineBottlePlayer;
_wineRackGrid[72] = kWineBottlePlayer;
_wineRackGrid[82] = kWineBottlePlayer;
_wineRackGrid[91] = kWineBottlePlayer;
break;
case 1:
_totalBottles = 12;
_wineRackGrid[75] = kWineBottlePlayer;
_wineRackGrid[56] = kWineBottlePlayer;
_wineRackGrid[45] = kWineBottlePlayer;
_wineRackGrid[27] = kWineBottlePlayer;
_wineRackGrid[24] = kWineBottlePlayer;
_wineRackGrid[15] = kWineBottlePlayer;
_wineRackGrid[64] = kWineBottleOpponent;
_wineRackGrid[34] = kWineBottleOpponent;
_wineRackGrid[33] = kWineBottleOpponent;
_wineRackGrid[18] = kWineBottleOpponent;
_wineRackGrid[16] = kWineBottleOpponent;
_wineRackGrid[14] = kWineBottleOpponent;
break;
default:
_totalBottles = 0;
break;
}
}
void WineRackGame::placeBottle(byte pos, byte val) {
debugC(kDebugLogic, "placeBottle(%d, %d)", (int)pos, (int)val);
_totalBottles++;
assert(_wineRackGrid[pos] == 0);
_wineRackGrid[pos] = val;
}
int8 WineRackGame::calculateNextMove(byte player) {
int8 moves1[24];
int8 moves2[24];
byte playerIndex = (player == 1) + 1;
if (!_totalBottles)
return randomMoveStart();
if (_totalBottles == 1)
return randomMoveStart2();
sub05(player, moves1);
sub05(playerIndex, moves2);
int8 result = sub06(moves1, moves2);
if (result == -1)
return findEmptySpot();
assert(_wineRackGrid[result] == 0);
return result;
}
int8 WineRackGame::findEmptySpot() {
int8 result = 0;
while (_wineRackGrid[result]) {
if (result == 99)
return 100;
else
++result;
}
return result;
}
void WineRackGame::sub05(int8 player, int8 *moves) {
int8 moves1[23];
int8 moves2[23];
int8 to, delta, playerIndex;
if (player == 1) {
to = 90;
delta = 10;
playerIndex = 2;
} else {
to = 9;
delta = 1;
playerIndex = 3;
}
memset(moves, 0, 23);
for (int i = 0; i < to; i += delta) {
if (!_wineRackGrid[i] || _wineRackGrid[i] == player) {
memset(moves1, 0, sizeof(moves1));
memset(moves2, 0, sizeof(moves2));
sub13(i, playerIndex, moves1, moves2);
if (moves[0] < moves1[0]) {
memcpy(moves, moves1, 23);
}
}
}
}
int8 WineRackGame::sub06(int8 *moves1, int8 *moves2) {
for (int i = 0; i < moves1[2]; i++) {
int8 result = moves1[i + 3];
if (!_wineRackGrid[result]) {
for (int j = 0; j < moves2[2]; j++)
if (moves2[j + 3] == result)
return result;
}
}
for (int i = 0; i < moves1[2]; i++) {
if (!_wineRackGrid[moves1[i + 3]])
return moves1[i + 3];
}
return -1;
}
uint32 WineRackGame::didPlayerWin() {
memset(_wineRackGrid2, 0, 100);
for (int i = 0; i < 10; i++) {
if (_wineRackGrid[i] == kWineBottlePlayer) {
int var = 0;
sub10(100, i, 2, 3, &var);
if (var == 1)
return 1;
}
}
return 0;
}
void WineRackGame::sub10(int8 endPos, int8 pos, int unused, int player, int *val) {
int8 candidates[8];
if (*val)
return;
if (wineRackLogicTable[12 * pos + player] == -1) {
*val = 1;
return;
}
sub11(pos, candidates);
for (int i = 0; candidates[i] != 100; i++) {
int8 nextPos = candidates[i];
if (endPos != nextPos)
sub10(pos, nextPos, unused, player, val);
}
}
void WineRackGame::sub11(int8 pos, int8 *candidates) {
int cnt = 0;
_wineRackGrid2[pos] = 1;
for (int i = 0; i < 6; i++) {
int8 val = wineRackLogicTable[12 * pos + i];
if (!_wineRackGrid2[val] && _wineRackGrid[pos] == _wineRackGrid[val])
candidates[cnt++] = val;
}
candidates[cnt] = 100;
}
uint32 WineRackGame::didAiWin() {
memset(_wineRackGrid2, 0, 100);
for (int i = 0; i < 100; i += 10) {
if (_wineRackGrid[i] == kWineBottleOpponent) {
int var = 0;
sub10(100, i, 1, 2, &var);
if (var == 1)
return 1;
}
}
return 0;
}
void WineRackGame::sub13(int8 cell, int8 player, int8 *moves1, int8 *moves2) {
int8 candidates[4];
if (cell == -1)
return;
moves2[moves2[2] + 3] = cell;
if (wineRackLogicTable[12 * cell + player] < 0) {
++moves2[2];
moves2[0] = countEmtpy(moves2);
if (moves2[0] > moves1[0])
memcpy(moves1, moves2, 23);
--moves2[2];
} else {
++moves2[2];
if (player == 2)
sub15(cell, candidates);
else
sub16(cell, candidates);
for (int i = 0; candidates[i] != -1; i++)
sub13(candidates[i], player, moves1, moves2);
--moves2[2];
}
}
void WineRackGame::sub15(int8 cell, int8 *candidates) {
int8 depth = 0;
int8 pos2 = wineRackLogicTable[12 * cell + 2];
int8 pos1 = wineRackLogicTable[12 * cell + 1];
if (_wineRackGrid[pos2] == 2) {
if (pos1 < 0 || _wineRackGrid[pos1] == 2) {
if (cell >= 20) {
int8 val1 = _wineRackGrid[cell - 10];
if (val1 != 2) {
int8 val2 = _wineRackGrid[cell - 10];
if (val2 != 2 && (val1 == 1 || val2 == 1)) {
depth = 1;
candidates[0] = cell - 10;
}
}
}
if (cell < 80) {
int8 val1 = _wineRackGrid[cell + 10];
if (val1 != 2) {
int8 val2 = _wineRackGrid[cell + 11];
if (val2 != 2 && (val1 == 1 || val2 == 1)) {
candidates[depth] = cell + 10;
depth++;
}
}
}
} else if (_wineRackGrid[cell] == 1 || _wineRackGrid[pos1] == 1) {
depth = 1;
candidates[0] = pos1;
}
} else if (pos1 < 0 || _wineRackGrid[pos1] == 2) {
if (_wineRackGrid[cell] == 1 || _wineRackGrid[pos2] == 1) {
depth = 1;
candidates[0] = pos2;
}
} else {
depth = 2;
candidates[0] = pos2;
candidates[1] = pos1;
}
candidates[depth] = -1;
}
void WineRackGame::sub16(int8 cell, int8 *candidates) {
int8 depth = 0;
int8 pos3 = wineRackLogicTable[12 * cell + 3];
int8 pos4 = wineRackLogicTable[12 * cell + 4];
if (_wineRackGrid[pos3] == 1) {
if (pos4 < 0 || _wineRackGrid[pos4] == 1) {
if (cell % 10 >= 2) {
int8 val1 = _wineRackGrid[cell - 1];
if (val1 != 1) {
int8 val2 = _wineRackGrid[cell + 8];
if (val2 != 1 && (val1 == 2 || val2 == 2)) {
depth = 1;
candidates[0] = cell - 1;
}
}
}
if (cell < 80 && _wineRackGrid[cell + 1] != 1) {
int8 val1 = _wineRackGrid[cell + 11];
if (val1 != 1 && (_wineRackGrid[cell + 1] == 2 || val1 == 2)) {
candidates[depth] = cell + 1;
depth++;
}
}
} else if (_wineRackGrid[cell] == 2 || _wineRackGrid[pos4] == 2) {
depth = 1;
candidates[0] = pos4;
}
} else if (pos4 < 0 || _wineRackGrid[pos4] == 1) {
if (_wineRackGrid[cell] == 2 || _wineRackGrid[pos3] == 2) {
depth = 1;
candidates[0] = pos3;
}
} else {
depth = 2;
candidates[0] = pos3;
candidates[1] = pos4;
}
candidates[depth] = -1;
}
int8 WineRackGame::countEmtpy(int8 *moves) {
int8 cnt = 0;
for (int i = 0; i < moves[2]; i++) {
if (!_wineRackGrid[moves[i + 3]])
++cnt;
}
return 20 - cnt;
}
int8 WineRackGame::randomMoveStart() {
const int8 moves[] = { 44, 45, 54, 55 };
return moves[_random.getRandomNumber(3)];
}
int8 WineRackGame::randomMoveStart2() {
const int8 moves[] = { 25, 26, 63, 64 };
// the original game doesn't ensure the spot isn't taken
int8 res = 0;
do {
res = moves[_random.getRandomNumber(3)];
} while (_wineRackGrid[res] != 0);
return res;
}
void WineRackGame::testWinCondition(byte player, int baseX, int baseY) {
initGrid(2);
int basePos = baseX * 10 + baseY;
for (int i = 0; i < 10; i++) {
if (player == kWineBottlePlayer)
placeBottle(i * 10 + basePos, player);
else
placeBottle(i + basePos, player);
}
if (player == kWineBottlePlayer && !didPlayerWin()) {
error("WineRackGame::testWinCondition(%d, %d, %d) failed", (int)player, baseX, baseY);
} else if (player == kWineBottleOpponent && !didAiWin()) {
error("WineRackGame::testWinCondition(%d, %d, %d) failed", (int)player, baseX, baseY);
}
}
void WineRackGame::testGame(uint32 seed, Common::Array<int> moves, bool playerWin) {
byte vars[1024] = {};
byte &x = vars[0];
byte &y = vars[1];
byte &op = vars[3];
byte &winner = vars[3];
byte &difficulty = vars[4];
_random.setSeed(seed);
difficulty = 2;
op = 3;
run(vars);
winner = 0;
for (uint i = 0; i < moves.size(); i += 2) {
if (winner)
error("early winner");
x = moves[i];
y = moves[i + 1];
op = 1;
run(vars);
}
if (playerWin && winner != 2)
error("WineRackGame::testGame(%u, %u, %d) player didn't win", seed, moves.size(), (int)playerWin);
else if (playerWin == false && winner != 1)
error("WineRackGame::testGame(%u, %u, %d) ai didn't win", seed, moves.size(), (int)playerWin);
}
void WineRackGame::runTests() {
warning("WineRackGame::runTests() starting");
uint32 oldSeed = _random.getSeed();
for (int i = 0; i < 10; i++) {
testWinCondition(kWineBottlePlayer, 0, i);
testWinCondition(kWineBottleOpponent, i, 0);
}
// pairs of x,y for the player's moves
testGame(1, {9,0, 9,1, 9,2, 9,3, 9,4, 9,5, 9,6, 9,7, 9,8, 9,9}, false);
testGame(2, {5,5, 3,5, 7,4, 1,6, 9,3, 0,7, 2,6, 4,5, 6,5, 8,4}, true);
// in the original game, the AI had a 25% chance to move to 2,6 as its first move, even if your first move was to 2,6 already
testGame(147160395, {2,6, 3,6, 4,6, 5,6, 7,6, 8,6, 9,6, 2,7, 3,7, 4,7}, false);
_random.setSeed(oldSeed);
warning("WineRackGame::runTests() finished");
}
namespace {
const int8 wineRackLogicTable[1200] = {
-1, -1, 1, 10, -1, -1, -1, -1, 11, -1, -1, -1,
-1, -1, 2, 11, 10, 0, -1, -1, 12, 20, -1, -1,
-1, -1, 3, 12, 11, 1, -1, -1, 13, 21, 10, -1,
-1, -1, 4, 13, 12, 2, -1, -1, 14, 22, 11, -1,
-1, -1, 5, 14, 13, 3, -1, -1, 15, 23, 12, -1,
-1, -1, 6, 15, 14, 4, -1, -1, 16, 24, 13, -1,
-1, -1, 7, 16, 15, 5, -1, -1, 17, 25, 14, -1,
-1, -1, 8, 17, 16, 6, -1, -1, 18, 26, 15, -1,
-1, -1, 9, 18, 17, 7, -1, -1, 19, 27, 16, -1,
-1, -1, -1, 19, 18, 8, -1, -1, -1, 28, 17, -1,
0, 1, 11, 20, -1, -1, -1, 2, 21, -1, -1, -1,
1, 2, 12, 21, 20, 10, -1, 3, 22, 30, -1, 0,
2, 3, 13, 22, 21, 11, -1, 4, 23, 31, 20, 1,
3, 4, 14, 23, 22, 12, -1, 5, 24, 32, 21, 2,
4, 5, 15, 24, 23, 13, -1, 6, 25, 33, 22, 3,
5, 6, 16, 25, 24, 14, -1, 7, 26, 34, 23, 4,
6, 7, 17, 26, 25, 15, -1, 8, 27, 35, 24, 5,
7, 8, 18, 27, 26, 16, -1, 9, 28, 36, 25, 6,
8, 9, 19, 28, 27, 17, -1, -1, 29, 37, 26, 7,
9, -1, -1, 29, 28, 18, -1, -1, -1, 38, 27, 8,
10, 11, 21, 30, -1, -1, 1, 12, 31, -1, -1, -1,
11, 12, 22, 31, 30, 20, 2, 13, 32, 40, -1, 10,
12, 13, 23, 32, 31, 21, 3, 14, 33, 41, 30, 11,
13, 14, 24, 33, 32, 22, 4, 15, 34, 42, 31, 12,
14, 15, 25, 34, 33, 23, 5, 16, 35, 43, 32, 13,
15, 16, 26, 35, 34, 24, 6, 17, 36, 44, 33, 14,
16, 17, 27, 36, 35, 25, 7, 18, 37, 45, 34, 15,
17, 18, 28, 37, 36, 26, 8, 19, 38, 46, 35, 16,
18, 19, 29, 38, 37, 27, 9, -1, 39, 47, 36, 17,
19, -1, -1, 39, 38, 28, -1, -1, -1, 48, 37, 18,
20, 21, 31, 40, -1, -1, 11, 22, 41, -1, -1, -1,
21, 22, 32, 41, 40, 30, 12, 23, 42, 50, -1, 20,
22, 23, 33, 42, 41, 31, 13, 24, 43, 51, 40, 21,
23, 24, 34, 43, 42, 32, 14, 25, 44, 52, 41, 22,
24, 25, 35, 44, 43, 33, 15, 26, 45, 53, 42, 23,
25, 26, 36, 45, 44, 34, 16, 27, 46, 54, 43, 24,
26, 27, 37, 46, 45, 35, 17, 28, 47, 55, 44, 25,
27, 28, 38, 47, 46, 36, 18, 29, 48, 56, 45, 26,
28, 29, 39, 48, 47, 37, 19, -1, 49, 57, 46, 27,
29, -1, -1, 49, 48, 38, -1, -1, -1, 58, 47, 28,
30, 31, 41, 50, -1, -1, 21, 32, 51, -1, -1, -1,
31, 32, 42, 51, 50, 40, 22, 33, 52, 60, -1, 30,
32, 33, 43, 52, 51, 41, 23, 34, 53, 61, 50, 31,
33, 34, 44, 53, 52, 42, 24, 35, 54, 62, 51, 32,
34, 35, 45, 54, 53, 43, 25, 36, 55, 63, 52, 33,
35, 36, 46, 55, 54, 44, 26, 37, 56, 64, 53, 34,
36, 37, 47, 56, 55, 45, 27, 38, 57, 65, 54, 35,
37, 38, 48, 57, 56, 46, 28, 39, 58, 66, 55, 36,
38, 39, 49, 58, 57, 47, 29, -1, 59, 67, 56, 37,
39, -1, -1, 59, 58, 48, -1, -1, -1, 68, 57, 38,
40, 41, 51, 60, -1, -1, 31, 42, 61, -1, -1, -1,
41, 42, 52, 61, 60, 50, 32, 43, 62, 70, -1, 40,
42, 43, 53, 62, 61, 51, 33, 44, 63, 71, 60, 41,
43, 44, 54, 63, 62, 52, 34, 45, 64, 72, 61, 42,
44, 45, 55, 64, 63, 53, 35, 46, 65, 73, 62, 43,
45, 46, 56, 65, 64, 54, 36, 47, 66, 74, 63, 44,
46, 47, 57, 66, 65, 55, 37, 48, 67, 75, 64, 45,
47, 48, 58, 67, 66, 56, 38, 49, 68, 76, 65, 46,
48, 49, 59, 68, 67, 57, 39, -1, 69, 77, 66, 47,
49, -1, -1, 69, 68, 58, -1, -1, -1, 78, 67, 48,
50, 51, 61, 70, -1, -1, 41, 52, 71, -1, -1, -1,
51, 52, 62, 71, 70, 60, 42, 53, 72, 80, -1, 50,
52, 53, 63, 72, 71, 61, 43, 54, 73, 81, 70, 51,
53, 54, 64, 73, 72, 62, 44, 55, 74, 82, 71, 52,
54, 55, 65, 74, 73, 63, 45, 56, 75, 83, 72, 53,
55, 56, 66, 75, 74, 64, 46, 57, 76, 84, 73, 54,
56, 57, 67, 76, 75, 65, 47, 58, 77, 85, 74, 55,
57, 58, 68, 77, 76, 66, 48, 59, 78, 86, 75, 56,
58, 59, 69, 78, 77, 67, 49, -1, 79, 87, 76, 57,
59, -1, -1, 79, 78, 68, -1, -1, -1, 88, 77, 58,
60, 61, 71, 80, -1, -1, 51, 62, 81, -1, -1, -1,
61, 62, 72, 81, 80, 70, 52, 63, 82, 90, -1, 60,
62, 63, 73, 82, 81, 71, 53, 64, 83, 91, 80, 61,
63, 64, 74, 83, 82, 72, 54, 65, 84, 92, 81, 62,
64, 65, 75, 84, 83, 73, 55, 66, 85, 93, 82, 63,
65, 66, 76, 85, 84, 74, 56, 67, 86, 94, 83, 64,
66, 67, 77, 86, 85, 75, 57, 68, 87, 95, 84, 65,
67, 68, 78, 87, 86, 76, 58, 69, 88, 96, 85, 66,
68, 69, 79, 88, 87, 77, 59, -1, 89, 97, 86, 67,
69, -1, -1, 89, 88, 78, -1, -1, -1, 98, 87, 68,
70, 71, 81, 90, -1, -1, 61, 72, 91, -1, -1, -1,
71, 72, 82, 91, 90, 80, 62, 73, 92, -1, -1, 70,
72, 73, 83, 92, 91, 81, 63, 74, 93, -1, 90, 71,
73, 74, 84, 93, 92, 82, 64, 75, 94, -1, 91, 72,
74, 75, 85, 94, 93, 83, 65, 76, 95, -1, 92, 73,
75, 76, 86, 95, 94, 84, 66, 77, 96, -1, 93, 74,
76, 77, 87, 96, 95, 85, 67, 78, 97, -1, 94, 75,
77, 78, 88, 97, 96, 86, 68, 79, 98, -1, 95, 76,
78, 79, 89, 98, 97, 87, 69, -1, 99, -1, 96, 77,
79, -1, -1, 99, 98, 88, -1, -1, -1, -1, 97, 78,
80, 81, 91, -1, -1, -1, 71, 82, -1, -1, -1, -1,
81, 82, 92, -1, -1, 90, 72, 83, -1, -1, -1, 80,
82, 83, 93, -1, -1, 91, 73, 84, -1, -1, -1, 81,
83, 84, 94, -1, -1, 92, 74, 85, -1, -1, -1, 82,
84, 85, 95, -1, -1, 93, 75, 86, -1, -1, -1, 83,
85, 86, 96, -1, -1, 94, 76, 87, -1, -1, -1, 84,
86, 87, 97, -1, -1, 95, 77, 88, -1, -1, -1, 85,
87, 88, 98, -1, -1, 96, 78, 89, -1, -1, -1, 86,
88, 89, 99, -1, -1, 97, 79, -1, -1, -1, -1, 87,
89, -1, -1, -1, -1, 98, -1, -1, -1, -1, -1, 88
};
} // End of anonymous namespace
} // End of Groovie namespace

View File

@@ -0,0 +1,76 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, MojoTouch has exclusively licensed
* this code on November 10th, 2021, to be use in closed-source products.
* Therefore, any contributions (commits) to it will also be dual-licensed.
*
*/
#ifndef GROOVIE_LOGIC_WINERACK_H
#define GROOVIE_LOGIC_WINERACK_H
#include "common/random.h"
#include "common/system.h"
namespace Groovie {
/*
* Wine rack puzzle in Clandestiny.
* The player needs to create a path of bottles from one side to the other
* before the ghost of Auld Sot does.
*/
class WineRackGame {
public:
WineRackGame();
void run(byte *scriptVariables);
private:
void initGrid(byte difficulty);
void placeBottle(byte pos, byte val);
int8 calculateNextMove(byte op);
int8 findEmptySpot();
void sub05(int8 player, int8 *moves);
int8 sub06(int8 *moves1, int8 *moves2);
uint32 didPlayerWin();
void sub10(int8 endPos, int8 pos, int unused, int player, int *val);
void sub11(int8 pos, int8 *candidates);
uint32 didAiWin();
void sub13(int8 cell, int8 player, int8 *moves1, int8 *moves2);
void sub15(int8 cell, int8 *candidates);
void sub16(int8 cell, int8 *candidates);
int8 countEmtpy(int8 *moves);
int8 randomMoveStart();
int8 randomMoveStart2();
void testWinCondition(byte player, int baseX, int baseY);
void testGame(uint32 seed, Common::Array<int> moves, bool playerWin);
void runTests();
int _totalBottles;
byte _wineRackGrid[100];
byte _wineRackGrid2[100];
Common::RandomSource _random;
};
} // End of Groovie namespace
#endif // GROOVIE_LOGIC_WINERACK_H

98
engines/groovie/lzss.cpp Normal file
View File

@@ -0,0 +1,98 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/lzss.h"
namespace Groovie {
#define OUT_BUFF_SIZE 131072
#define COMP_THRESH 3 // Compression not attempted if string to be compressed is less than 3 long
LzssReadStream::LzssReadStream(Common::ReadStream *indata, uint8 lengthmask, uint8 lengthbits) {
/*
TODO: Nasty hack. Make a buffer bigger than I'll ever need... probably.
What should *really* happen is I should define a whole new type of stream
that gets lzss decompressed on the fly
*/
_outLzssBufData = (uint8 *)malloc(OUT_BUFF_SIZE);
_size = decodeLZSS(indata, lengthmask, lengthbits);
_pos = 0;
}
LzssReadStream::~LzssReadStream() {
free(_outLzssBufData);
}
uint32 LzssReadStream::decodeLZSS(Common::ReadStream *in, uint8 lengthmask, uint8 lengthbits) {
uint32 N = 1 << (16 - lengthbits); /* History buffer size */
byte *histbuff = new byte[N](); /* History buffer */
uint32 outstreampos = 0;
uint32 bufpos = 0;
while (!in->eos()) {
byte flagbyte = in->readByte();
for (uint32 i = 1; i <= 8; i++) {
if (!in->eos()) {
if ((flagbyte & 1) == 0) {
uint32 offsetlen = in->readUint16LE();
if (offsetlen == 0) {
break;
}
uint32 length = (offsetlen & lengthmask) + COMP_THRESH;
uint32 offset = (bufpos - (offsetlen >> lengthbits)) & (N - 1);
for (uint32 j = 0; j < length; j++) {
byte tempa = histbuff[(offset + j) & (N - 1)];
_outLzssBufData[outstreampos++] = tempa;
histbuff[bufpos] = tempa;
bufpos = (bufpos + 1) & (N - 1);
}
} else {
byte tempa = in->readByte();
if (in->eos()) {
break;
}
_outLzssBufData[outstreampos++] = tempa;
histbuff[bufpos] = tempa;
bufpos = (bufpos + 1) & (N - 1);
}
flagbyte = flagbyte >> 1;
}
}
}
delete[] histbuff;
return outstreampos;
}
bool LzssReadStream::eos() const {
return _pos >= _size;
}
uint32 LzssReadStream::read(void *buf, uint32 size) {
if (size > _size - _pos)
size = _size - _pos;
memcpy(buf, &_outLzssBufData[_pos], size);
_pos += size;
return size;
}
} // End of Groovie namespace

42
engines/groovie/lzss.h Normal file
View File

@@ -0,0 +1,42 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/stream.h"
namespace Groovie {
class LzssReadStream : public Common::ReadStream {
private:
uint8 *_outLzssBufData;
uint32 _size;
uint32 _pos;
uint32 decodeLZSS(Common::ReadStream *in, uint8 lengthmask, uint8 lengthbits);
public:
LzssReadStream(Common::ReadStream *indata, uint8 lengthmask, uint8 lengthbits);
~LzssReadStream() override;
bool eos() const override;
uint32 read(void *buf, uint32 size) override;
};
} // End of Groovie namespace

View File

@@ -0,0 +1,233 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/groovie.h"
#include "groovie/saveload.h"
#include "common/system.h"
#include "common/translation.h"
#include "backends/keymapper/action.h"
#include "backends/keymapper/keymapper.h"
#include "backends/keymapper/standard-actions.h"
#include "engines/advancedDetector.h"
#include "groovie/detection.h"
namespace Groovie {
static const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_T7G_FAST_MOVIE_SPEED,
{
_s("Fast movie speed"),
_s("Play movies at an increased speed"),
"fast_movie_speed",
false,
0,
0
}
},
{
GAMEOPTION_ORIGINAL_SAVELOAD,
{
_s("Use original save/load screens"),
_s("Use the original save/load screens instead of the ScummVM ones"),
"originalsaveload",
false,
0,
0
}
},
{
GAMEOPTION_EASIER_AI,
{
_s("Easier AI"),
_s("Decrease the difficulty of AI puzzles"),
"easier_ai",
false,
0,
0
}
},
{
GAMEOPTION_EASIER_AI_DEFAULT,
{
_s("Easier AI"),
_s("Decrease the difficulty of AI puzzles"),
"easier_ai",
true,
0,
0
}
},
{
GAMEOPTION_FINAL_HOUR,
{
_s("Updated Credits Music"),
_s("Play the song The Final Hour during the credits instead of reusing MIDI songs"),
"credits_music",
false,
0,
0
}
},
{
GAMEOPTION_SLIMHOTSPOTS,
{
_s("Slim Left/Right Hotspots"),
_s("Shrinks the hotspots on the left and right sides for exiting puzzles"),
"slim_hotspots",
true,
0,
0
}
},
{
GAMEOPTION_SPEEDRUN,
{
_s("Speedrun Mode"),
_s("Affects the controls for fast forwarding the game"),
"speedrun_mode",
false,
0,
0
}
},
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
class GroovieMetaEngine : public AdvancedMetaEngine<GroovieGameDescription> {
public:
const char *getName() const override {
return "groovie";
}
const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
return optionsList;
}
Common::Error createInstance(OSystem *syst, Engine **engine, const GroovieGameDescription *gd) const override;
bool hasFeature(MetaEngineFeature f) const override;
SaveStateList listSaves(const char *target) const override;
int getMaximumSaveSlot() const override;
bool removeSaveState(const char *target, int slot) const override;
SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const override;
int getAutosaveSlot() const override;
Common::KeymapArray initKeymaps(const char *target) const override;
};
Common::Error GroovieMetaEngine::createInstance(OSystem *syst, Engine **engine, const GroovieGameDescription *gd) const {
#ifndef ENABLE_GROOVIE2
if (gd->version != kGroovieT7G)
return Common::Error(Common::kUnsupportedGameidError, _s("GroovieV2 support is not compiled in"));
#endif
*engine = new GroovieEngine(syst,gd);
return Common::kNoError;
}
bool GroovieMetaEngine::hasFeature(MetaEngineFeature f) const {
return
(f == kSupportsListSaves) ||
(f == kSupportsLoadingDuringStartup) ||
(f == kSupportsDeleteSave) ||
(f == kSimpleSavesNames) ||
(f == kSavesSupportMetaInfo);
}
SaveStateList GroovieMetaEngine::listSaves(const char *target) const {
return SaveLoad::listValidSaves(target);
}
int GroovieMetaEngine::getMaximumSaveSlot() const {
return SaveLoad::getMaximumSlot();
}
bool GroovieMetaEngine::removeSaveState(const char *target, int slot) const {
if (!SaveLoad::isSlotValid(slot)) {
// Invalid slot, do nothing
return false;
}
Common::String filename = SaveLoad::getSlotSaveName(target, slot);
return g_system->getSavefileManager()->removeSavefile(filename);
}
SaveStateDescriptor GroovieMetaEngine::querySaveMetaInfos(const char *target, int slot) const {
SaveStateDescriptor desc;
Common::InSaveFile *savefile = SaveLoad::openForLoading(target, slot, &desc);
delete savefile;
return desc;
}
int GroovieMetaEngine::getAutosaveSlot() const {
return GroovieEngine::AUTOSAVE_SLOT;
}
Common::KeymapArray GroovieMetaEngine::initKeymaps(const char *target) const {
using namespace Common;
using namespace Groovie;
Keymap *engineKeyMap = new Keymap(Keymap::kKeymapTypeGame, "groovie-engine", _("Groovie engine"));
Action *act;
act = new Action(kStandardActionLeftClick, _("Left click"));
act->setLeftClickEvent();
act->addDefaultInputMapping("MOUSE_LEFT");
act->addDefaultInputMapping("JOY_A");
engineKeyMap->addAction(act);
act = new Action(kStandardActionRightClick, _("Right click"));
act->setRightClickEvent();
act->addDefaultInputMapping("MOUSE_RIGHT");
act->addDefaultInputMapping("JOY_B");
engineKeyMap->addAction(act);
act = new Action("SKIPORFAST", _("Skip or fast forward scene"));
act->setCustomEngineActionEvent(kActionSkip);
act->addDefaultInputMapping("ESCAPE");
act->addDefaultInputMapping("SPACE");
act->addDefaultInputMapping("JOY_X");
engineKeyMap->addAction(act);
return Keymap::arrayOf(engineKeyMap);
}
} // End of namespace Groovie
#if PLUGIN_ENABLED_DYNAMIC(GROOVIE)
REGISTER_PLUGIN_DYNAMIC(GROOVIE, PLUGIN_TYPE_ENGINE, Groovie::GroovieMetaEngine);
#else
REGISTER_PLUGIN_STATIC(GROOVIE, PLUGIN_TYPE_ENGINE, Groovie::GroovieMetaEngine);
#endif

42
engines/groovie/module.mk Normal file
View File

@@ -0,0 +1,42 @@
MODULE := engines/groovie
MODULE_OBJS := \
logic/cell.o \
video/player.o \
video/vdx.o \
cursor.o \
debug.o \
font.o \
graphics.o \
groovie.o \
lzss.o \
metaengine.o \
music.o \
resource.o \
saveload.o \
script.o
ifdef ENABLE_GROOVIE2
MODULE_OBJS += \
logic/beehive.o \
logic/cake.o \
logic/gallery.o \
logic/mousetrap.o \
logic/othello.o \
logic/pente.o \
logic/tlcgame.o \
logic/triangle.o \
logic/winerack.o \
video/roq.o
endif
# This module can be built as a plugin
ifeq ($(ENABLE_GROOVIE), DYNAMIC_PLUGIN)
PLUGIN := 1
endif
# Include common rules
include $(srcdir)/rules.mk
# Detection objects
DETECT_OBJS += $(MODULE)/detection.o

876
engines/groovie/music.cpp Normal file
View File

@@ -0,0 +1,876 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "audio/mididrv.h"
#include "audio/mixer.h"
#include "groovie/music.h"
#include "groovie/groovie.h"
#include "groovie/resource.h"
#include "groovie/logic/tlcgame.h"
#include "backends/audiocd/audiocd.h"
#include "common/config-manager.h"
#include "common/debug.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/memstream.h"
#include "common/textconsole.h"
#include "audio/audiostream.h"
#include "audio/midiparser.h"
#include "audio/miles.h"
#include "audio/decoders/mp3.h"
#include "audio/decoders/quicktime.h"
namespace Groovie {
// MusicPlayer
MusicPlayer::MusicPlayer(GroovieEngine *vm) :
_vm(vm), _isPlaying(false), _backgroundFileRef(0), _gameVolume(100),
_prevCDtrack(0), _backgroundDelay(0), _fadingStartTime(0), _fadingStartVolume(0),
_fadingEndVolume(0), _fadingDuration(0), _midiInit(false), _userVolume(0) {
}
MusicPlayer::~MusicPlayer() {
g_system->getAudioCDManager()->stop();
}
void MusicPlayer::playSong(uint32 fileref) {
Common::StackLock lock(_mutex);
if (_isPlaying)
unload();
// Set the volumes
_fadingEndVolume = 100;
_gameVolume = 100;
updateVolume();
// Play the referenced file once
play(fileref, false);
}
void MusicPlayer::setBackgroundSong(uint32 fileref) {
Common::StackLock lock(_mutex);
debugC(1, kDebugMIDI, "Groovie::Music: Changing the background song: %04X", fileref);
_backgroundFileRef = fileref;
}
void MusicPlayer::frameTick() {
if (_backgroundDelay > 0) {
_backgroundDelay--;
if (_backgroundDelay == 0)
playSong(_backgroundFileRef);
}
}
void MusicPlayer::setBackgroundDelay(uint16 delay) {
_backgroundDelay = delay;
}
void MusicPlayer::playCD(uint8 track) {
int startms = 0;
// Stop the MIDI playback
unload();
debugC(1, kDebugMIDI, "Groovie::Music: Playing CD track %d", track);
if (track == 3) {
// This is the credits song, start at 23:20
startms = 1400000;
// TODO: If we want to play it directly from the CD, we should decrement
// the song number (it's track 2 on the 2nd CD)
} else if ((track == 98) && (_prevCDtrack == 3)) {
// Track 98 is used as a hack to stop the credits song
g_system->getAudioCDManager()->stop();
stopCreditsIOS();
return;
}
// Save the playing track in order to be able to stop the credits song
_prevCDtrack = track;
// Wait until the CD stops playing the current song
// It was in the original interpreter, but it introduces a big delay
// in the middle of the introduction, so it's disabled right now
/*
g_system->getAudioCDManager()->updateCD();
while (g_system->getAudioCDManager()->isPlaying()) {
// Wait a bit and try again
_vm->_system->delayMillis(100);
g_system->getAudioCDManager()->updateCD();
}
*/
// Play the track starting at the requested offset (1000ms = 75 frames)
g_system->getAudioCDManager()->play(track - 1, 1, startms * 75 / 1000, 0);
// If the audio is not playing from the CD, play the "fallback" MIDI.
// The Mac version has no CD tracks, so it will always use the MIDI.
if (!g_system->getAudioCDManager()->isPlaying()) {
if (track == 2) {
// Intro MIDI fallback
if (_vm->getPlatform() == Common::kPlatformMacintosh)
playSong(70);
else
playSong((19 << 10) | 36); // XMI.GJD, file 36
} else if (track == 3) {
// TODO: Credits MIDI fallback
if (_vm->getPlatform() == Common::kPlatformIOS)
playCreditsIOS();
}
}
}
void MusicPlayer::startBackground() {
debugC(3, kDebugMIDI, "Groovie::Music: startBackground()");
if (!_isPlaying && _backgroundFileRef) {
debugC(3, kDebugMIDI, "Groovie::Music: Starting the background song (0x%4X)", _backgroundFileRef);
play(_backgroundFileRef, true);
}
}
void MusicPlayer::setUserVolume(uint16 volume) {
Common::StackLock lock(_mutex);
// Save the new user volume
_userVolume = volume;
if (_userVolume > 0x100)
_userVolume = 0x100;
// Apply it
updateVolume();
}
void MusicPlayer::setGameVolume(uint16 volume, uint16 time) {
Common::StackLock lock(_mutex);
debugC(1, kDebugMIDI, "Groovie::Music: Setting game volume from %d to %d in %dms", _gameVolume, volume, time);
// Save the start parameters of the fade
_fadingStartTime = _vm->_system->getMillis();
_fadingStartVolume = _gameVolume;
_fadingDuration = time;
// Save the new game volume
_fadingEndVolume = volume;
if (_fadingEndVolume > 100)
_fadingEndVolume = 100;
}
bool MusicPlayer::play(uint32 fileref, bool loop) {
// Unload the previous song
unload();
// Set the new state
_isPlaying = true;
// Load the new file
return load(fileref, loop);
}
void MusicPlayer::stop() {
_backgroundFileRef = 0;
setBackgroundDelay(0);
unload();
}
void MusicPlayer::applyFading() {
debugC(6, kDebugMIDI, "Groovie::Music: applyFading() _fadingStartTime = %d, _fadingDuration = %d, _fadingStartVolume = %d, _fadingEndVolume = %d", _fadingStartTime, _fadingDuration, _fadingStartVolume, _fadingEndVolume);
Common::StackLock lock(_mutex);
// Calculate the passed time
uint32 time = _vm->_system->getMillis() - _fadingStartTime;
debugC(6, kDebugMIDI, "Groovie::Music: time = %d, _gameVolume = %d", time, _gameVolume);
if (time >= _fadingDuration) {
// Set the end volume
_gameVolume = _fadingEndVolume;
} else {
// Calculate the interpolated volume for the current time
_gameVolume = (_fadingStartVolume * (_fadingDuration - time) +
_fadingEndVolume * time) / _fadingDuration;
}
if (_gameVolume == _fadingEndVolume) {
// If we were fading to 0, stop the playback and restore the volume
if (_fadingEndVolume == 0) {
debugC(1, kDebugMIDI, "Groovie::Music: Faded to zero: end of song. _fadingEndVolume set to 100");
// WORKAROUND The original interpreter would keep playing a track
// at volume 0 after it has faded out. When a new track was
// started, it would restore the volume first and a short part of
// the old track would be heard before the new track would start.
// To prevent this, playback is actually stopped after fading out,
// but _isPlaying remains true. This keeps the original
// interpreter behavior of not starting the background music, but
// it prevents the issue when starting playback of a new track.
unload(false);
}
}
// Apply it
updateVolume();
}
void MusicPlayer::onTimer(void *refCon) {
debugC(9, kDebugMIDI, "Groovie::Music: onTimer()");
MusicPlayer *music = (MusicPlayer *)refCon;
Common::StackLock lock(music->_mutex);
// Apply the game volume fading
if (music->_gameVolume != music->_fadingEndVolume) {
// Apply the next step of the fading
music->applyFading();
}
// If the game is accepting user input, start the background music if necessary
if (music->_vm->isWaitingForInput())
music->startBackground();
// Handle internal timed events
music->onTimerInternal();
}
void MusicPlayer::unload(bool updateState) {
debugC(1, kDebugMIDI, "Groovie::Music: Stopping the playback");
if (updateState)
// Set the new state
_isPlaying = false;
}
void MusicPlayer::playCreditsIOS() {
Audio::AudioStream *stream = Audio::SeekableAudioStream::openStreamFile("7th_Guest_Dolls_from_Hell_OC_ReMix");
if (!stream) {
warning("Could not find '7th_Guest_Dolls_from_Hell_OC_ReMix' audio file");
return;
}
_vm->_system->getMixer()->playStream(Audio::Mixer::kMusicSoundType, &_handleCreditsIOS, stream);
}
void MusicPlayer::stopCreditsIOS() {
_vm->_system->getMixer()->stopHandle(_handleCreditsIOS);
}
// MusicPlayerMidi
MusicPlayerMidi::MusicPlayerMidi(GroovieEngine *vm) :
MusicPlayer(vm), _midiParser(nullptr), _data(nullptr), _driver(nullptr) {
// Initialize the channel volumes
for (int i = 0; i < 0x10; i++) {
_chanVolumes[i] = 0x7F;
}
}
MusicPlayerMidi::~MusicPlayerMidi() {
// Stop the callback
if (_driver)
_driver->setTimerCallback(nullptr, nullptr);
Common::StackLock lock(_mutex);
// Unload the parser
unload();
delete _midiParser;
// Unload the MIDI Driver
if (_driver) {
_driver->close();
delete _driver;
}
}
void MusicPlayerMidi::send(uint32 b) {
if ((b & 0xFFF0) == 0x07B0) { // Volume change
// Save the specific channel volume
byte chan = b & 0xF;
_chanVolumes[chan] = (b >> 16) & 0x7F;
// Send the updated value
updateChanVolume(chan);
return;
}
if (_driver)
_driver->send(b);
}
void MusicPlayerMidi::sysEx(const byte *msg, uint16 length) {
if (_driver)
_driver->sysEx(msg, length);
}
uint16 MusicPlayerMidi::sysExNoDelay(const byte *msg, uint16 length) {
return _driver ? _driver->sysExNoDelay(msg, length) : 0;
}
void MusicPlayerMidi::metaEvent(byte type, const byte *data, uint16 length) {
switch (type) {
case 0x2F:
// End of Track, play the background song
endTrack();
break;
default:
if (_driver)
_driver->metaEvent(type, data, length);
break;
}
}
void MusicPlayerMidi::pause(bool pause) {
if (_midiParser) {
if (pause) {
_midiParser->pausePlaying();
} else {
_midiParser->resumePlaying();
}
}
}
void MusicPlayerMidi::updateChanVolume(byte channel) {
// Generate a MIDI Control change message for the volume
uint32 b = 0x7B0;
// Specify the channel
b |= (channel & 0xF);
// Scale by the user and game volumes
uint32 val = (_chanVolumes[channel] * _userVolume * _gameVolume) / 0x100 / 100;
val &= 0x7F;
// Send it to the driver
if (_driver)
_driver->send(b | (val << 16));
}
void MusicPlayerMidi::endTrack() {
debugC(3, kDebugMIDI, "Groovie::Music: endTrack()");
unload();
}
void MusicPlayerMidi::onTimerInternal() {
// TODO: We really only need to call this while music is playing.
if (_midiParser)
_midiParser->onTimer();
}
void MusicPlayerMidi::updateVolume() {
// Apply it to all the channels
for (int i = 0; i < 0x10; i++) {
updateChanVolume(i);
}
}
void MusicPlayerMidi::unload(bool updateState) {
MusicPlayer::unload(updateState);
// Unload the parser data
if (_midiParser)
_midiParser->unloadMusic();
// Unload the data
delete[] _data;
_data = nullptr;
}
bool MusicPlayerMidi::loadParser(Common::SeekableReadStream *stream, bool loop) {
if (!_midiParser)
return false;
// Read the whole file to memory
int length = stream->size();
_data = new byte[length];
stream->read(_data, length);
delete stream;
// Set the looping option
_midiParser->property(MidiParser::mpAutoLoop, loop);
// Start parsing the data
if (!_midiParser->loadMusic(_data, length)) {
error("Groovie::Music: Couldn't parse the data");
return false;
}
// Activate the timer source
if (_driver)
_driver->setTimerCallback(this, &onTimer);
return true;
}
// MusicPlayerXMI
MusicPlayerXMI::MusicPlayerXMI(GroovieEngine *vm, const Common::String &gtlName) :
MusicPlayerMidi(vm), _multisourceDriver(nullptr), _milesXmidiTimbres(nullptr) {
// Create the driver
MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
MusicType musicType = MidiDriver::getMusicType(dev);
if (musicType == MT_GM && ConfMan.getBool("native_mt32"))
musicType = MT_MT32;
_driver = nullptr;
_musicType = 0;
// 7th Guest uses FAT.AD/FAT.OPL/FAT.MT
// 11th Hour uses SAMPLE.AD/SAMPLE.OPL/SAMPLE.MT
switch (musicType) {
case MT_ADLIB:
_driver = _multisourceDriver = Audio::MidiDriver_Miles_AdLib_create(Common::Path(gtlName + ".AD"), Common::Path(gtlName + ".OPL"));
break;
case MT_MT32:
Audio::MidiDriver_Miles_Midi *milesDriver;
milesDriver = Audio::MidiDriver_Miles_MIDI_create(musicType, Common::Path(gtlName + ".MT"));
_milesXmidiTimbres = milesDriver;
_driver = _multisourceDriver = milesDriver;
break;
case MT_GM:
_driver = _multisourceDriver = Audio::MidiDriver_Miles_MIDI_create(musicType, "");
break;
case MT_NULL:
_driver = _multisourceDriver = new MidiDriver_NULL_Multisource();
break;
default:
break;
}
_musicType = musicType;
assert(_driver);
// Create the parser
_midiParser = MidiParser::createParser_XMIDI(nullptr, nullptr, 0);
_multisourceDriver->property(MidiDriver::PROP_USER_VOLUME_SCALING, true);
_multisourceDriver->property(MidiDriver::PROP_MILES_VERSION,
_vm->getEngineVersion() == kGroovieT7G ? Audio::MILES_VERSION_2 : Audio::MILES_VERSION_3);
if (_vm->getEngineVersion() == kGroovieT7G && musicType == MT_GM)
// The 7th Guest GM init sets drumkit to 0x30 (Orchestra) and relies on
// this remaining set; tracks don't set this at start. Some tracks
// temporarily change the drumkit; if playback is stopped at the wrong
// time this will cause the changed drumkit to remain in effect.
// Set a default drumkit value to make sure it is set correctly at the
// start of each track.
_multisourceDriver->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_DRUMKIT, 0x30);
if (_vm->getEngineVersion() == kGroovieT11H)
// Some The 11th Hour tracks use modulation, but not all tracks reset
// it at start. Set a default value to make sure it is reset at the
// start of each track.
_multisourceDriver->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_MODULATION, 0);
int result = _driver->open();
if (result > 0 && result != MidiDriver::MERR_ALREADY_OPEN)
error("Opening MidiDriver failed with error code %i", result);
_multisourceDriver->setSourceNeutralVolume(0, 100);
// Set the parser's driver
_midiParser->setMidiDriver(this);
// Set the timer rate
_midiParser->setTimerRate(_driver->getBaseTempo());
}
MusicPlayerXMI::~MusicPlayerXMI() {
_midiParser->stopPlaying();
}
void MusicPlayerXMI::send(int8 source, uint32 b) {
_multisourceDriver->send(source, b);
}
void MusicPlayerXMI::metaEvent(int8 source, byte type, const byte *data, uint16 length) {
if (type == 0x2F) // End Of Track
MusicPlayerMidi::endTrack();
_multisourceDriver->metaEvent(source, type, data, length);
}
void MusicPlayerXMI::stopAllNotes(bool stopSustainedNotes) {
if (_driver)
_driver->stopAllNotes(stopSustainedNotes);
}
bool MusicPlayerXMI::isReady(int8 source) {
return _driver ? _driver->isReady(source) : false;
}
void MusicPlayerXMI::updateVolume() {
_multisourceDriver->setSourceVolume(0, _gameVolume);
}
void MusicPlayerXMI::setUserVolume(uint16 volume) {
_multisourceDriver->syncSoundSettings();
}
bool MusicPlayerXMI::load(uint32 fileref, bool loop) {
debugC(1, kDebugMIDI, "Groovie::Music: Starting the playback of song: %04X", fileref);
// Open the song resource
Common::SeekableReadStream *file = _vm->_resMan->open(fileref);
if (!file) {
error("Groovie::Music: Couldn't find resource 0x%04X", fileref);
return false;
}
return loadParser(file, loop);
}
void MusicPlayerXMI::unload(bool updateState) {
MusicPlayerMidi::unload(updateState);
_multisourceDriver->deinitSource(0);
}
// MusicPlayerMac_t7g
MusicPlayerMac_t7g::MusicPlayerMac_t7g(GroovieEngine *vm) : MusicPlayerMidi(vm) {
// Create the parser
_midiParser = MidiParser::createParser_SMF();
// Create the driver
MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
_driver = MidiDriver::createMidi(dev);
assert(_driver);
_driver->open(); // TODO: Handle return value != 0 (indicating an error)
// Set the parser's driver
_midiParser->setMidiDriver(this);
// Set the timer rate
_midiParser->setTimerRate(_driver->getBaseTempo());
// Sanity check
assert(_vm->_macResFork);
}
bool MusicPlayerMac_t7g::load(uint32 fileref, bool loop) {
debugC(1, kDebugMIDI, "Groovie::Music: Starting the playback of song: %04X", fileref);
// First try for compressed MIDI
Common::SeekableReadStream *file = _vm->_macResFork->getResource(MKTAG('c','m','i','d'), fileref & 0x3FF);
if (file) {
// Found the resource, decompress it
Common::SeekableReadStream *tmp = decompressMidi(file);
delete file;
file = tmp;
} else {
// Otherwise, it's uncompressed
file = _vm->_macResFork->getResource(MKTAG('M','i','d','i'), fileref & 0x3FF);
if (!file)
error("Groovie::Music: Couldn't find resource 0x%04X", fileref);
}
return loadParser(file, loop);
}
Common::SeekableReadStream *MusicPlayerMac_t7g::decompressMidi(Common::SeekableReadStream *stream) {
// Initialize an output buffer of the given size
uint32 size = stream->readUint32BE();
byte *output = (byte *)malloc(size);
byte *current = output;
uint32 decompBytes = 0;
while ((decompBytes < size) && !stream->eos()) {
// 8 flags
byte flags = stream->readByte();
for (byte i = 0; (i < 8) && !stream->eos(); i++) {
if (flags & 1) {
// 1: Next byte is a literal
*(current++) = stream->readByte();
if (stream->eos())
continue;
decompBytes++;
} else {
// 0: It's a reference to part of the history
uint16 args = stream->readUint16BE();
if (stream->eos())
continue;
// Length = 4bit unsigned (3 minimal)
uint8 length = (args >> 12) + 3;
// Offset = 12bit signed (all values are negative)
int16 offset = (args & 0xFFF) | 0xF000;
// Copy from the past decompressed bytes
decompBytes += length;
while (length > 0) {
*(current) = *(current + offset);
current++;
length--;
}
}
flags = flags >> 1;
}
}
// Return the output buffer wrapped in a MemoryReadStream
return new Common::MemoryReadStream(output, size, DisposeAfterUse::YES);
}
// MusicPlayerMac_v2
MusicPlayerMac_v2::MusicPlayerMac_v2(GroovieEngine *vm) : MusicPlayerMidi(vm) {
// Create the parser
_midiParser = MidiParser::createParser_QT();
// Create the driver
MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
_driver = MidiDriver::createMidi(dev);
assert(_driver);
_driver->open(); // TODO: Handle return value != 0 (indicating an error)
// Set the parser's driver
_midiParser->setMidiDriver(this);
// Set the timer rate
_midiParser->setTimerRate(_driver->getBaseTempo());
}
bool MusicPlayerMac_v2::load(uint32 fileref, bool loop) {
debugC(1, kDebugMIDI, "Groovie::Music: Starting the playback of song: %04X", fileref);
// Find correct filename
ResInfo info;
_vm->_resMan->getResInfo(fileref, info);
uint len = info.filename.size();
if (len < 4)
return false; // This shouldn't actually occur
// Remove the extension and add ".mov"
info.filename.deleteLastChar();
info.filename.deleteLastChar();
info.filename.deleteLastChar();
info.filename += "mov";
Common::SeekableReadStream *file = SearchMan.createReadStreamForMember(Common::Path(info.filename));
if (!file) {
warning("Could not find file '%s'", info.filename.c_str());
return false;
}
return loadParser(file, loop);
}
MusicPlayerIOS::MusicPlayerIOS(GroovieEngine *vm) : MusicPlayer(vm) {
vm->getTimerManager()->installTimerProc(&onTimer, 50 * 1000, this, "groovieMusic");
}
MusicPlayerIOS::~MusicPlayerIOS() {
_vm->getTimerManager()->removeTimerProc(&onTimer);
}
void MusicPlayerIOS::updateVolume() {
// Just set the mixer volume for the music sound type
_vm->_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, _userVolume * _gameVolume / 100);
}
void MusicPlayerIOS::unload(bool updateState) {
MusicPlayer::unload(updateState);
_vm->_system->getMixer()->stopHandle(_handle);
}
bool MusicPlayerIOS::load(uint32 fileref, bool loop) {
// Find correct filename
ResInfo info;
_vm->_resMan->getResInfo(fileref, info);
uint len = info.filename.size();
if (len < 4)
return false; // This shouldn't actually occur
/*
19462 door
19463 ??
19464 ??
19465 puzzle?
19466 cake
19467 maze
19468 ambient (but not 69, amb b. odd)
19470 puzzle
19471
19473
19475 coffins or blood pump
19476 blood pump or coffins
19493
19499 chapel
19509 downstair ambient
19510 bedroom 'skip 3 and 5' puzzle (should loop from partway?)
19514
19515 bathroom drain teeth
*/
if ((fileref >= 19462 && fileref <= 19468) ||
fileref == 19470 || fileref == 19471 ||
fileref == 19473 || fileref == 19475 ||
fileref == 19476 || fileref == 19493 ||
fileref == 19499 || fileref == 19509 ||
fileref == 19510 || fileref == 19514 ||
fileref == 19515)
loop = true; // XMIs for these refs self-loop
// iOS port provides alternative intro sequence music
if (info.filename == "gu39.xmi") {
info.filename = "intro";
} else if (info.filename == "gu32.xmi") {
info.filename = "foyer";
} else {
// Remove the extension
info.filename.deleteLastChar();
info.filename.deleteLastChar();
info.filename.deleteLastChar();
info.filename.deleteLastChar();
}
if (info.filename == "ini_sc") {
// This is an initialization MIDI file, which is not
// needed for digital tracks
return false;
}
// Create the audio stream
Audio::SeekableAudioStream *seekStream = Audio::SeekableAudioStream::openStreamFile(Common::Path(info.filename));
if (!seekStream) {
warning("Could not play audio file '%s'", info.filename.c_str());
return false;
}
Audio::AudioStream *audStream = seekStream;
// Loop if requested
if (loop)
audStream = Audio::makeLoopingAudioStream(seekStream, 0);
// MIDI player handles volume reset on load, IOS player doesn't - force update here
updateVolume();
// Play!
_vm->_system->getMixer()->playStream(Audio::Mixer::kMusicSoundType, &_handle, audStream);
return true;
}
MusicPlayerTlc::MusicPlayerTlc(GroovieEngine *vm) : MusicPlayer(vm) {
_file = nullptr;
vm->getTimerManager()->installTimerProc(&onTimer, 50 * 1000, this, "groovieMusic");
}
MusicPlayerTlc::~MusicPlayerTlc() {
_vm->getTimerManager()->removeTimerProc(&onTimer);
}
void MusicPlayerTlc::updateVolume() {
// Just set the mixer volume for the music sound type
_vm->_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, _userVolume * _gameVolume / 100);
}
void MusicPlayerTlc::unload(bool updateState) {
MusicPlayer::unload(updateState);
_vm->_system->getMixer()->stopHandle(_handle);
if (_file) {
delete _file;
}
_file = nullptr;
}
Common::String MusicPlayerTlc::getFilename(uint32 fileref) {
#ifdef ENABLE_GROOVIE2
return TlcGame::getTlcMusicFilename(fileref);
#else
return "";
#endif
}
bool MusicPlayerTlc::load(uint32 fileref, bool loop) {
unload();
_file = new Common::File();
Common::String filename = getFilename(fileref);
// Apple platforms use m4a files instead of mpg
if (_vm->getPlatform() == Common::kPlatformUnknown)
filename += ".m4a";
else
filename += ".mpg";
// Create the audio stream from fileref
_file->open(Common::Path(filename));
Audio::SeekableAudioStream *seekStream = nullptr;
if (_file->isOpen()) {
if (filename.hasSuffix(".m4a"))
seekStream = Audio::makeQuickTimeStream(_file, DisposeAfterUse::NO);
#ifdef USE_MAD
else
seekStream = Audio::makeMP3Stream(_file, DisposeAfterUse::NO);
#endif
} else {
delete _file;
_file = nullptr;
}
if (!seekStream) {
warning("Could not play audio file '%s'", filename.c_str());
return false;
}
Audio::AudioStream *audStream = seekStream;
// TODO: Loop if requested
if (!loop)
warning("TODO: MusicPlayerTlc::load with loop == false");
if (loop || 1)
audStream = Audio::makeLoopingAudioStream(seekStream, 0);
// MIDI player handles volume reset on load, IOS player doesn't - force update here
updateVolume();
// Play!
_vm->_system->getMixer()->playStream(Audio::Mixer::kMusicSoundType, &_handle, audStream);
return true;
}
// This a list of files for background music. This list is hardcoded in the Clandestiny player.
const char *kClanMusicFiles[] = {
"mbf_arb1", "mbf_arm1", "mbf_bal1", "mbf_c2p2", "act18mus", "act15mus", "act21mus",
"act05mus", "act04mus", "act23mus", "act17mus", "act03mus", "act06mus", "act19mus",
"act07mus", "mbf_mne1", "act24mus", "act24mus", "act14mus", "act20mus", "act15mus",
"act13mus", "act08mus", "mbf_uph1", "mbf_uph1", "act19mus", "mbf_bol1", "mbf_cbk1",
"mbf_glf1", "mbf_bro1", "mbf_c1r1", "mbf_c1r1", "mbf_c1r1", "mbf_c1r1", "mbf_c2r1",
"mbf_c2r1", "mbf_c2r1", "mbf_c2r1", "mbf_c3r1", "mbf_c3r1", "mbf_c3r1", "mbf_c4r1",
"mbf_c4r1", "mbf_c1p2", "mbf_c3p3", "mbf_c1p3", "mbf_bro1", "mbf_c1p1", "act17mus",
"mbf_c2p2", "mbf_c2p1", "act10mus", "mbf_c1p1", "mbf_mne1", "mbf_c3p3", "act17mus",
"mbf_c3p2", "mbf_c3p1", "act25mus", "mbf_c4p2", "mbf_c4p1"
};
Common::String MusicPlayerClan::getFilename(uint32 fileref) {
return kClanMusicFiles[fileref];
}
} // End of Groovie namespace

234
engines/groovie/music.h Normal file
View File

@@ -0,0 +1,234 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_MUSIC_H
#define GROOVIE_MUSIC_H
#include "common/array.h"
#include "common/mutex.h"
#include "common/file.h"
#include "audio/mididrv.h"
#include "audio/mididrv_ms.h"
#include "audio/mixer.h"
#include "audio/miles.h"
class MidiParser;
namespace Groovie {
class GroovieEngine;
class TlcGame;
class MusicPlayer {
public:
MusicPlayer(GroovieEngine *vm);
virtual ~MusicPlayer();
void playSong(uint32 fileref);
// Stops all music playback. Clears the current
// background song.
void stop();
void setBackgroundSong(uint32 fileref);
void playCD(uint8 track);
void startBackground();
bool isPlaying() { return _isPlaying; }
// Pause or resume the music. Note that digital music
// already pauses when the ScummVM menu is open, so
// it does not seem to need an implementation.
virtual void pause(bool pause) { }
// Set the MIDI initialization state
void setMidiInit(bool midiInit) { _midiInit = midiInit; }
// Returns true if MIDI has been fully initialized
bool isMidiInit() { return _midiInit; }
void frameTick();
void setBackgroundDelay(uint16 delay);
// Volume
virtual void setUserVolume(uint16 volume);
void setGameVolume(uint16 volume, uint16 time);
private:
// Song playback
bool play(uint32 fileref, bool loop);
bool _isPlaying;
uint32 _backgroundFileRef;
uint8 _prevCDtrack;
uint16 _backgroundDelay;
// T7G iOS credits mp3 stream
void playCreditsIOS();
void stopCreditsIOS();
Audio::SoundHandle _handleCreditsIOS;
// Volume fading
uint32 _fadingStartTime;
uint16 _fadingStartVolume;
uint16 _fadingEndVolume;
uint16 _fadingDuration;
void applyFading();
protected:
GroovieEngine *_vm;
// True if the MIDI initialization has completed
bool _midiInit;
// Callback
static void onTimer(void *data);
virtual void onTimerInternal() {}
Common::Mutex _mutex;
// User volume
uint16 _userVolume;
// Game volume
uint16 _gameVolume;
// These are specific for each type of music
virtual void updateVolume() = 0;
virtual bool load(uint32 fileref, bool loop) = 0;
virtual void unload(bool updateState = true);
};
class MusicPlayerMidi : public MusicPlayer, public MidiDriver_BASE {
public:
MusicPlayerMidi(GroovieEngine *vm);
~MusicPlayerMidi() override;
// MidiDriver_BASE interface
void send(uint32 b) override;
void sysEx(const byte* msg, uint16 length) override;
uint16 sysExNoDelay(const byte *msg, uint16 length) override;
void metaEvent(byte type, const byte *data, uint16 length) override;
void pause(bool pause) override;
private:
// Channel volumes
byte _chanVolumes[0x10];
void updateChanVolume(byte channel);
protected:
byte *_data;
MidiParser *_midiParser;
MidiDriver *_driver;
void onTimerInternal() override;
void updateVolume() override;
void unload(bool updateState = true) override;
void endTrack();
bool loadParser(Common::SeekableReadStream *stream, bool loop);
};
class MusicPlayerXMI : public MusicPlayerMidi, public Audio::MidiDriver_Miles_Xmidi_Timbres {
public:
MusicPlayerXMI(GroovieEngine *vm, const Common::String &gtlName);
~MusicPlayerXMI();
using MusicPlayerMidi::send;
void send(int8 source, uint32 b) override;
using MusicPlayerMidi::metaEvent;
void metaEvent(int8 source, byte type, const byte *data, uint16 length) override;
void stopAllNotes(bool stopSustainedNotes) override;
void processXMIDITimbreChunk(const byte *timbreListPtr, uint32 timbreListSize) override {
if (_milesXmidiTimbres)
_milesXmidiTimbres->processXMIDITimbreChunk(timbreListPtr, timbreListSize);
};
bool isReady(int8 source = -1) override;
void setUserVolume(uint16 volume) override;
protected:
void updateVolume() override;
bool load(uint32 fileref, bool loop) override;
void unload(bool updateState = true) override;
private:
// Output music type
uint8 _musicType;
MidiDriver_Multisource *_multisourceDriver;
MidiDriver_Miles_Xmidi_Timbres *_milesXmidiTimbres;
};
class MusicPlayerMac_t7g : public MusicPlayerMidi {
public:
MusicPlayerMac_t7g(GroovieEngine *vm);
protected:
bool load(uint32 fileref, bool loop) override;
private:
Common::SeekableReadStream *decompressMidi(Common::SeekableReadStream *stream);
};
class MusicPlayerMac_v2 : public MusicPlayerMidi {
public:
MusicPlayerMac_v2(GroovieEngine *vm);
protected:
bool load(uint32 fileref, bool loop) override;
};
class MusicPlayerIOS : public MusicPlayer {
public:
MusicPlayerIOS(GroovieEngine *vm);
~MusicPlayerIOS() override;
protected:
void updateVolume() override;
bool load(uint32 fileref, bool loop) override;
void unload(bool updateState = true) override;
private:
Audio::SoundHandle _handle;
};
class MusicPlayerTlc : public MusicPlayer {
public:
MusicPlayerTlc(GroovieEngine *vm);
~MusicPlayerTlc();
protected:
virtual Common::String getFilename(uint32 fileref);
void updateVolume() override;
bool load(uint32 fileref, bool loop) override;
void unload(bool updateState = true) override;
private:
Audio::SoundHandle _handle;
Common::File *_file;
};
class MusicPlayerClan : public MusicPlayerTlc {
public:
MusicPlayerClan(GroovieEngine *vm) : MusicPlayerTlc(vm) {}
protected:
Common::String getFilename(uint32 fileref) override;
};
} // End of Groovie namespace
#endif // GROOVIE_MUSIC_H

View File

@@ -0,0 +1,362 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/archive.h"
#include "common/debug.h"
#include "common/file.h"
#include "common/macresman.h"
#include "common/substream.h"
#include "common/textconsole.h"
#include "common/config-manager.h"
#include "groovie/resource.h"
#include "groovie/groovie.h"
namespace Groovie {
// ResMan
Common::SeekableReadStream *ResMan::open(uint32 fileRef) {
// Get the information about the resource
ResInfo resInfo;
if (!getResInfo(fileRef, resInfo)) {
return nullptr;
}
debugC(1, kDebugResource, "Groovie::Resource: Opening resource %d", fileRef);
return open(resInfo);
}
Common::SeekableReadStream *ResMan::open(const ResInfo &resInfo) {
// Do we know the name of the required GJD?
if (resInfo.gjd >= _gjds.size()) {
// Is this a raw vob file? (TLC DVD)
if (resInfo.gjd >= 1000) {
Common::Path filename = Common::Path(Common::String::format("VOB%u.VOB", resInfo.offset));
if (!Common::File::exists(filename)) {
return nullptr;
}
Common::File *vobFile = new Common::File();
vobFile->open(filename);
return vobFile;
}
error("Groovie::Resource: Unknown GJD %d", resInfo.gjd);
return nullptr;
}
debugC(1, kDebugResource, "Groovie::Resource: Opening resource (%s, %d, %d, %d)", _gjds[resInfo.gjd].toString().c_str(), resInfo.offset, resInfo.size, resInfo.disks);
// Does it exist?
if (!Common::File::exists(_gjds[resInfo.gjd])) {
error("Groovie::Resource: %s not found (resInfo.disks: %d)", _gjds[resInfo.gjd].toString().c_str(), resInfo.disks);
return nullptr;
}
// Open the pack file
Common::File *gjdFile = new Common::File();
if (!gjdFile->open(_gjds[resInfo.gjd])) {
delete gjdFile;
error("Groovie::Resource: Couldn't open %s", _gjds[resInfo.gjd].toString().c_str());
return nullptr;
}
// Save the used gjd file (except xmi and gamwav)
if (resInfo.gjd < 19) {
_lastGjd = resInfo.gjd;
}
// Returning the resource substream
Common::SeekableSubReadStream *file = new Common::SeekableSubReadStream(gjdFile, resInfo.offset, resInfo.offset + resInfo.size, DisposeAfterUse::YES);
if (ConfMan.getBool("dump_resources")) {
dumpResource(file, Common::Path(resInfo.filename), false);
}
return file;
}
Common::String ResMan::getGjdName(const ResInfo &resInfo) {
if (resInfo.gjd >= _gjds.size()) {
if (resInfo.gjd >= 1000) {
return Common::String::format("VOB%u.VOB", resInfo.offset);
}
error("Groovie::Resource: Unknown GJD %d", resInfo.gjd);
}
return _gjds[resInfo.gjd].baseName();
}
void ResMan::dumpResource(const Common::String &fileName) {
uint32 fileRef = getRef(fileName);
dumpResource(fileRef, Common::Path(fileName));
}
void ResMan::dumpResource(uint32 fileRef, const Common::Path &fileName) {
Common::SeekableReadStream *inFile = open(fileRef);
dumpResource(inFile, fileName);
}
void ResMan::dumpResource(Common::SeekableReadStream *inFile, const Common::Path &fileName, bool dispose) {
Common::DumpFile outFile;
outFile.open(fileName);
int64 totalSize = inFile->size();
byte *data = new byte[totalSize];
inFile->read(data, totalSize);
outFile.write(data, totalSize);
outFile.flush();
delete[] data;
if (dispose)
delete inFile;
else
inFile->seek(0);
outFile.close();
}
// ResMan_t7g
static const char t7g_gjds[][0x15] = {"at", "b", "ch", "d", "dr", "fh", "ga", "hdisk", "htbd", "intro", "jhek", "k", "la", "li", "mb", "mc", "mu", "n", "p", "xmi", "gamwav"};
ResMan_t7g::ResMan_t7g(Common::MacResManager *macResFork) : _macResFork(macResFork) {
for (int i = 0; i < 0x15; i++) {
// Prepare the filename
Common::String filename = t7g_gjds[i];
filename += ".gjd";
// Handle the special case of Mac's hdisk.gjd
if (_macResFork && i == 7)
filename = "T7GData";
// Append it to the list of GJD files
_gjds.push_back(Common::Path(filename));
}
}
uint32 ResMan_t7g::getRef(Common::String name) {
// Get the name of the RL file
Common::String rlFileName(t7g_gjds[_lastGjd]);
rlFileName += ".rl";
Common::SeekableReadStream *rlFile = nullptr;
if (_macResFork) {
// Open the RL file from the resource fork
rlFile = _macResFork->getResource(rlFileName);
} else {
// Open the RL file
rlFile = SearchMan.createReadStreamForMember(Common::Path(rlFileName));
}
if (!rlFile)
error("Groovie::Resource: Couldn't open %s", rlFileName.c_str());
uint32 resNum;
bool found = false;
for (resNum = 0; !found && !rlFile->err() && !rlFile->eos(); resNum++) {
// Read the resource name
char readname[12];
rlFile->read(readname, 12);
// Test whether it's the resource we're searching
Common::String resname(readname, 12);
if (resname.hasPrefix(name.c_str())) {
debugC(2, kDebugResource, "Groovie::Resource: Resource %12s matches %s", readname, name.c_str());
found = true;
}
// Skip the rest of resource information
rlFile->read(readname, 8);
}
// Close the RL file
delete rlFile;
// Verify we really found the resource
if (!found) {
error("Groovie::Resource: Couldn't find resource %s in %s", name.c_str(), rlFileName.c_str());
return (uint32)-1;
}
return (_lastGjd << 10) | (resNum - 1);
}
bool ResMan_t7g::getResInfo(uint32 fileRef, ResInfo &resInfo) {
// Calculate the GJD and the resource number
resInfo.gjd = fileRef >> 10;
uint16 resNum = fileRef & 0x3FF;
// Get the name of the RL file
Common::String rlFileName(t7g_gjds[resInfo.gjd]);
rlFileName += ".rl";
Common::SeekableReadStream *rlFile = nullptr;
if (_macResFork) {
// Open the RL file from the resource fork
rlFile = _macResFork->getResource(rlFileName);
} else {
// Open the RL file
rlFile = SearchMan.createReadStreamForMember(Common::Path(rlFileName));
}
if (!rlFile)
error("Groovie::Resource: Couldn't open %s", rlFileName.c_str());
// Seek to the position of the desired resource
rlFile->seek(resNum * 20);
if (rlFile->eos()) {
delete rlFile;
error("Groovie::Resource: Invalid resource number: 0x%04X (%s)", resNum, rlFileName.c_str());
}
// Read the resource name
char resname[13];
rlFile->read(resname, 12);
resname[12] = 0;
debugC(2, kDebugResource, "Groovie::Resource: Resource name: %12s", resname);
resInfo.filename = resname;
// Read the resource information
resInfo.offset = rlFile->readUint32LE();
resInfo.size = rlFile->readUint32LE();
// Close the resource RL file
delete rlFile;
return true;
}
// ResMan_v2
ResMan_v2::ResMan_v2() {
Common::File indexfile;
// Open the GJD index file
if (!indexfile.open("gjd.gjd")) {
error("Groovie::Resource: Couldn't open gjd.gjd");
return;
}
Common::String line = indexfile.readLine();
while (!indexfile.eos() && !line.empty()) {
// Get the name before the space
Common::String filename;
for (const char *cur = line.c_str(); *cur != ' '; cur++) {
filename += *cur;
}
// Append it to the list of GJD files
if (!filename.empty()) {
_gjds.push_back(Common::Path(filename));
}
// Read the next line
line = indexfile.readLine();
}
// Close the GJD index file
indexfile.close();
}
uint32 ResMan_v2::getRef(Common::String name) {
// Open the RL file
Common::File rlFile;
if (!rlFile.open("dir.rl")) {
error("Groovie::Resource: Couldn't open dir.rl");
return (uint32)-1;
}
// resources are always in lowercase
name.toLowercase();
uint32 resNum;
bool found = false;
for (resNum = 0; !found && !rlFile.err() && !rlFile.eos(); resNum++) {
// Seek past metadata
rlFile.seek(14, SEEK_CUR);
// Read the resource name
char readname[18];
rlFile.read(readname, 18);
// Test whether it's the resource we're searching
Common::String resname(readname, 18);
if (resname.hasPrefixIgnoreCase(name.c_str())) {
debugC(2, kDebugResource, "Groovie::Resource: Resource %18s matches %s", readname, name.c_str());
found = true;
break;
}
}
// Close the RL file
rlFile.close();
// Verify we really found the resource
if (!found) {
warning("Groovie::Resource: Couldn't find resource %s", name.c_str());
return (uint32)-1;
}
return resNum;
}
bool ResMan_v2::getResInfo(uint32 fileRef, ResInfo &resInfo) {
// Open the RL file
Common::File rlFile;
if (!rlFile.open("dir.rl")) {
error("Groovie::Resource: Couldn't open dir.rl");
return false;
}
// Seek to the position of the desired resource
rlFile.seek(fileRef * 32);
if (rlFile.eos()) {
rlFile.close();
error("Groovie::Resource: Invalid resource number: 0x%04X", fileRef);
return false;
}
// Read the resource information
resInfo.disks = rlFile.readUint32LE(); // Seems to be a bitfield indicating on which disk(s) the file can be found
resInfo.offset = rlFile.readUint32LE();
resInfo.size = rlFile.readUint32LE();
resInfo.gjd = rlFile.readUint16LE();
// Read the resource name
char resname[19];
resname[18] = 0;
rlFile.read(resname, 18);
debugC(2, kDebugResource, "Groovie::Resource: Resource name: %18s", resname);
resInfo.filename = resname;
// 6 padding bytes? (it looks like they're always 0)
// Close the resource RL file
rlFile.close();
return true;
}
} // End of Groovie namespace

View File

@@ -0,0 +1,83 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_RESOURCE_H
#define GROOVIE_RESOURCE_H
namespace Common {
class MacResManager;
}
namespace Groovie {
struct ResInfo {
uint32 disks; // This seems to be a bitfield indicating on which disk(s) the file is located.
uint16 gjd;
uint32 offset;
uint32 size;
Common::String filename;
};
class ResMan {
public:
virtual ~ResMan() {}
Common::SeekableReadStream *open(uint32 fileRef);
Common::SeekableReadStream *open(const ResInfo &resInfo);
void dumpResource(const Common::String &fileName);
void dumpResource(uint32 fileRef, const Common::Path &fileName);
void dumpResource(Common::SeekableReadStream *inFile, const Common::Path &fileName, bool dispose = true);
Common::String getGjdName(const ResInfo &resInfo);
virtual uint32 getRef(Common::String name) = 0;
virtual bool getResInfo(uint32 fileRef, ResInfo &resInfo) = 0;
protected:
Common::Array<Common::Path> _gjds;
uint16 _lastGjd;
};
class ResMan_t7g : public ResMan {
public:
ResMan_t7g(Common::MacResManager *macResFork = 0);
~ResMan_t7g() override {}
uint32 getRef(Common::String name) override;
bool getResInfo(uint32 fileRef, ResInfo &resInfo) override;
private:
Common::MacResManager *_macResFork;
};
class ResMan_v2 : public ResMan {
public:
ResMan_v2();
~ResMan_v2() override {}
uint32 getRef(Common::String name) override;
bool getResInfo(uint32 fileRef, ResInfo &resInfo) override;
};
} // End of Groovie namespace
#endif // GROOVIE_RESOURCE_H

View File

@@ -0,0 +1,189 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/saveload.h"
#include "groovie/groovie.h"
#include "common/system.h"
#include "common/substream.h"
#include "common/translation.h"
#define SUPPORTED_SAVEFILE_VERSION 1
// 0 - Just script variables, compatible with the original
// 1 - Added one byte with version number at the beginning
namespace Groovie {
int SaveLoad::getMaximumSlot() {
return MAX_SAVES - 1;
}
bool SaveLoad::isSlotValid(int slot) {
return slot >= 0 && slot <= getMaximumSlot();
}
Common::String SaveLoad::getSlotSaveName(const Common::String &target, int slot) {
Common::String fileName = Common::String::format("%s.%03d", target.c_str(), slot);
return fileName;
}
SaveStateList SaveLoad::listValidSaves(const Common::String &target) {
SaveStateList list;
// some Groovie 2 games use save 0 with a garbage name for internal tracking, other games use slot 0 for Open House mode
const Common::U32String reservedName = _("Reserved"); // I18N: Savegame default name in Groovie engine
bool hasReserved = false;
// Get the list of savefiles
Common::String pattern = Common::String::format("%s.0##", target.c_str());
Common::StringArray savefiles = g_system->getSavefileManager()->listSavefiles(pattern);
// Sort the list of filenames
sort(savefiles.begin(), savefiles.end());
// Fill the information for the existing savegames
for (auto &savefile : savefiles) {
const char *ext = strrchr(savefile.c_str(), '.');
if (!ext)
continue;
int slot = atoi(ext + 1);
if (!isSlotValid(slot))
continue;
SaveStateDescriptor descriptor;
Common::InSaveFile *file = SaveLoad::openForLoading(target, slot, &descriptor);
if (file) {
// It's a valid savefile, save the descriptor
delete file;
if (slot == 0) {
hasReserved = true;
if (descriptor.getDescription() != "OPEN HOUSE" && descriptor.getDescription() != "Open House")
descriptor.setDescription(reservedName);
}
list.push_back(descriptor);
}
}
if (!hasReserved) {
SaveStateDescriptor desc;
desc.setDescription(reservedName);
list.push_back(desc);
}
return list;
}
Common::InSaveFile *SaveLoad::openForLoading(const Common::String &target, int slot, SaveStateDescriptor *descriptor) {
// Validate the slot number
if (!isSlotValid(slot)) {
return nullptr;
}
// Open the savefile
Common::String savename = getSlotSaveName(target, slot);
Common::InSaveFile *savefile = g_system->getSavefileManager()->openForLoading(savename);
if (!savefile) {
return nullptr;
}
// Read the savefile version
uint8 version;
if (savefile->size() == 1024) {
version = 0;
} else {
version = savefile->readByte();
}
// Verify we can read this version
if (version > SUPPORTED_SAVEFILE_VERSION) {
//TODO: show the error about unsupported savefile version
}
// Save the current position as the start for the engine data
int metaDataSize = savefile->pos();
// Fill the SaveStateDescriptor if it was provided
if (descriptor) {
// Initialize the SaveStateDescriptor
descriptor->setSaveSlot(slot);
// TODO: Add extra information
//setSaveDate(int year, int month, int day)
//setSaveTime(int hour, int min)
//setPlayTime(int hours, int minutes)
// Read the savegame description
Common::String description;
unsigned char c = 1;
for (int i = 0; (c != 0) && (i < 15); i++) {
c = savefile->readByte();
switch (c) {
case 0:
break;
case 16: // @
// fall through intended
case 254: // . (generated when pressing space)
c = ' ';
break;
case 244: // $
c = 0;
break;
default:
c += 0x30;
}
if (c != 0) {
description += c;
}
}
descriptor->setDescription(description);
}
// Return a substream, skipping the metadata
Common::SeekableSubReadStream *sub = new Common::SeekableSubReadStream(savefile, metaDataSize, savefile->size(), DisposeAfterUse::YES);
// Move to the beginning of the substream
sub->seek(0, SEEK_SET);
return sub;
}
Common::OutSaveFile *SaveLoad::openForSaving(const Common::String &target, int slot) {
// Validate the slot number
if (!isSlotValid(slot)) {
return nullptr;
}
// Open the savefile
Common::String savename = getSlotSaveName(target, slot);
Common::OutSaveFile *savefile = g_system->getSavefileManager()->openForSaving(savename);
if (!savefile) {
return nullptr;
}
// Write the savefile version
savefile->writeByte(SUPPORTED_SAVEFILE_VERSION);
return savefile;
}
} // End of Groovie namespace

View File

@@ -0,0 +1,48 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_SAVELOAD_H
#define GROOVIE_SAVELOAD_H
#include "common/savefile.h"
#include "engines/game.h"
#include "engines/savestate.h"
namespace Groovie {
class SaveLoad {
public:
// Validating slot numbers
static int getMaximumSlot();
static bool isSlotValid(int slot);
// Getting information
static Common::String getSlotSaveName(const Common::String &target, int slot);
static SaveStateList listValidSaves(const Common::String &target);
// Opening savefiles
static Common::InSaveFile *openForLoading(const Common::String &target, int slot, SaveStateDescriptor *descriptor = nullptr);
static Common::OutSaveFile *openForSaving(const Common::String &target, int slot);
};
} // End of Groovie namespace
#endif // GROOVIE_SAVELOAD_H

2738
engines/groovie/script.cpp Normal file

File diff suppressed because it is too large Load Diff

288
engines/groovie/script.h Normal file
View File

@@ -0,0 +1,288 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_SCRIPT_H
#define GROOVIE_SCRIPT_H
#include "groovie/groovie.h"
#ifdef ENABLE_GROOVIE2
#include "groovie/logic/beehive.h"
#include "groovie/logic/cake.h"
#include "groovie/logic/gallery.h"
#include "groovie/logic/mousetrap.h"
#include "groovie/logic/othello.h"
#include "groovie/logic/pente.h"
#include "groovie/logic/triangle.h"
#include "groovie/logic/winerack.h"
#endif
#include "common/random.h"
#include "common/rect.h"
namespace Common {
class SeekableReadStream;
}
namespace Graphics {
struct Surface;
}
namespace Groovie {
class CellGame;
class Debugger;
class GroovieEngine;
class TlcGame;
class Script {
friend class Debugger;
public:
Script(GroovieEngine *vm, EngineVersion version);
~Script();
void setDebugger(Debugger *debugger);
void setVariable(uint16 varnum, byte value);
void timerTick();
bool loadScript(Common::String scriptfile);
void directGameLoad(int slot);
void directGameSave(int slot, const Common::String &desc);
bool canDirectSave() const;
void step();
void setMouseClick(uint8 button);
void setKbdChar(uint8 c);
void setAction(uint8 a);
void setBitFlag(int bitnum, bool value);
bool getBitFlag(int bitnum);
Common::String &getContext();
private:
GroovieEngine *_vm;
Common::RandomSource _random;
bool _firstbit;
uint8 _lastCursor;
EngineVersion _version;
// Script filename (for debugging purposes)
Common::String _scriptFile;
Common::String _savedScriptFile;
// Save names
Common::String _saveNames[MAX_SAVES];
bool _wantAutosave;
// Code
byte *_code;
uint16 _codeSize;
uint16 _currentInstruction;
byte *_savedCode;
uint16 _savedCodeSize;
uint16 _savedInstruction;
// Variables
byte _variables[0x400];
byte _savedVariables[0x180];
// Stack
uint16 _stack[0x20];
uint8 _stacktop;
uint8 _savedStacktop;
// Input
bool _mouseClicked;
uint8 _eventMouseClicked;
uint8 _kbdChar;
uint8 _eventKbdChar;
uint8 _eventAction;
uint16 _inputLoopAddress;
uint8 _newCursorStyle;
uint16 _hotspotTopAction;
uint16 _hotspotTopCursor;
uint16 _hotspotBottomAction;
uint16 _hotspotBottomCursor;
uint16 _hotspotRightAction;
uint16 _hotspotLeftAction;
uint16 _hotspotSlot;
bool _fastForwarding;
void resetFastForward();
// Video
Common::SeekableReadStream *_videoFile;
uint32 _videoRef;
uint16 _bitflags;
uint16 _videoSkipAddress;
// Debugging
Debugger *_debugger;
Common::String _debugString;
uint16 _oldInstruction;
// Special classes depending on played game
CellGame *_cellGame;
TlcGame *_tlcGame;
// Helper functions
uint8 getCodeByte(uint16 address);
uint8 readScript8bits();
uint16 readScript16bits();
uint32 readScript32bits();
uint16 readScript8or16bits();
uint8 readScriptChar(bool allow7C, bool limitVal, bool limitVar);
void readScriptString(Common::String &str);
uint8 readScriptVar();
uint32 getVideoRefString(Common::String &resName);
void executeInputAction(uint16 address);
bool hotspot(Common::Rect rect, uint16 addr, uint8 cursor);
void loadgame(uint slot);
bool preview_loadgame(uint slot);
void savegame(uint slot, const Common::String &name);
bool playvideofromref(uint32 fileref, bool loopUntilAudioDone = false);
bool playBackgroundSound(uint32 fileref, uint32 loops);
void printString(Graphics::Surface *surface, const char *str);
// Opcodes
typedef void (Script::*OpcodeFunc)();
OpcodeFunc *_opcodes;
static OpcodeFunc _opcodesT7G[];
static OpcodeFunc _opcodesV2[];
void o_invalid();
void o_nop();
void o_nop8();
void o_nop16();
void o_nop32();
void o_nop8or16();
void o_playsong();
void o_bf9on();
void o_palfadeout();
void o_bf8on();
void o_bf6on();
void o_bf7on();
void o_setbackgroundsong();
void o_videofromref();
void o_bf5on();
void o_inputloopstart();
void o_keyboardaction();
void o_hotspot_rect();
void o_hotspot_left();
void o_hotspot_right();
void o_hotspot_center();
void o_hotspot_current();
void o_inputloopend();
void o_random();
void o_jmp();
void o_loadstring();
void o_ret();
void o_call();
void o_sleep();
void o_strcmpnejmp_var();
void o_copybgtofg();
void o_strcmpnejmp();
void o_xor_obfuscate();
void o_vdxtransition();
void o_swap();
void o_inc();
void o_dec();
void o_strcmpeqjmp();
void o_mov();
void o_add();
void o_videofromstring1();
void o_videofromstring2();
void o_stopmidi();
void o_endscript();
void o_sethotspottop();
void o_sethotspotbottom();
void o_loadgame();
void o_savegame();
void o_hotspotbottom_4();
void o_midivolume();
void o_jne();
void o_loadstringvar();
void o_chargreatjmp();
void o_bf7off();
void o_charlessjmp();
void o_copyrecttobg();
void o_restorestkpnt();
void o_obscureswap();
void o_printstring();
void o_hotspot_slot();
void o_checkvalidsaves();
void o_resetvars();
void o_mod();
void o_loadscript();
void o_setvideoorigin();
void o_sub();
void o_returnscript();
void o_sethotspotright();
void o_sethotspotleft();
void o_getcd();
void o_playcd();
void o_musicdelay();
void o_hotspot_outrect();
void o_stub56();
void o_wipemaskfromstring58();
void o_stub59();
void o2_bf0on();
void o2_copybgtofg();
void o2_printstring();
void o2_playsong();
void o2_midicontrol();
void o2_setbackgroundsong();
void o2_videofromref();
void o2_vdxtransition();
void o2_setvideoskip();
void o2_savescreen();
void o2_restorescreen();
void o_gamelogic();
void o2_copyfgtobg();
void o2_setscriptend();
void o2_playsound();
void o2_check_sounds_overlays();
void o2_preview_loadgame();
#ifdef ENABLE_GROOVIE2
BeehiveGame _beehive;
CakeGame _cake;
GalleryGame _gallery;
MouseTrapGame _mouseTrap;
OthelloGame _othello;
PenteGame _pente;
TriangleGame _triangle;
WineRackGame _wineRack;
#endif
};
} // End of Groovie namespace
#endif // GROOVIE_SCRIPT_H

View File

@@ -0,0 +1,160 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/debug.h"
#include "audio/audiostream.h"
#include "groovie/video/player.h"
#include "groovie/groovie.h"
#include "audio/mixer.h"
#include "common/debug-channels.h"
namespace Groovie {
VideoPlayer::VideoPlayer(GroovieEngine *vm) :
_vm(vm), _syst(vm->_system), _file(nullptr), _audioStream(nullptr), _fps(0), _overrideSpeed(false), _flags(0),
_begunPlaying(false), _millisBetweenFrames(0), _lastFrameTime(0), _frameTimeDrift(0) {
_startTime = _syst->getMillis();
int16 h = g_system->getOverlayHeight();
_subtitles.setBBox(Common::Rect(20, h - 120, g_system->getOverlayWidth() - 20, h - 20));
_subtitles.setColor(0xff, 0xff, 0xff);
_subtitles.setFont("LiberationSans-Regular.ttf");
}
bool VideoPlayer::load(Common::SeekableReadStream *file, uint16 flags) {
_file = file;
_flags = flags;
_overrideSpeed = false;
_startTime = _syst->getMillis();
stopAudioStream();
_fps = loadInternal();
if (_fps != 0) {
setOverrideSpeed(_overrideSpeed);
_begunPlaying = false;
return true;
} else {
_file = nullptr;
return false;
}
}
void VideoPlayer::setOverrideSpeed(bool isOverride) {
_overrideSpeed = isOverride;
if (_fps != 0) {
if (isOverride)
_millisBetweenFrames = 1000.0f / 26.0f;
else
_millisBetweenFrames = 1000.0f / float(_fps);
}
}
void VideoPlayer::fastForward() {
_millisBetweenFrames = 0;
_frameTimeDrift = 0;
stopAudioStream();
}
bool VideoPlayer::isFastForwarding() {
return DebugMan.isDebugChannelEnabled(kDebugFast) || _millisBetweenFrames <= 0;
}
bool VideoPlayer::playFrame() {
bool end = true;
// Process the next frame while the file is open
if (_file) {
end = playFrameInternal();
_subtitles.drawSubtitle(_lastFrameTime - _startTime);
}
// The file has been completely processed
if (end) {
_file = nullptr;
// Wait for pending audio
if (_audioStream) {
if (_audioStream->endOfData() || isFastForwarding()) {
// Mark the audio stream as finished (no more data will be appended)
_audioStream->finish();
_audioStream = nullptr;
} else {
// Don't end if there's still audio playing
end = false;
}
}
unloadSubtitles();
}
return end;
}
void VideoPlayer::unloadSubtitles() {
if (_subtitles.isLoaded()) {
_subtitles.close();
g_system->hideOverlay();
}
}
void VideoPlayer::waitFrame() {
if (isFastForwarding()) {
return;
}
uint32 currTime = _syst->getMillis();
if (!_begunPlaying) {
_begunPlaying = true;
_lastFrameTime = currTime;
_frameTimeDrift = 0.0f;
if (_subtitles.isLoaded()) {
g_system->showOverlay(false);
g_system->clearOverlay();
}
} else {
uint32 millisDiff = currTime - _lastFrameTime;
float fMillis = _millisBetweenFrames + _frameTimeDrift;
// use floorf instead of roundf, because delayMillis often slightly over-sleeps
uint32 millisSleep = MAX(0.0f, floorf(fMillis) - float(millisDiff));
if (millisSleep > 0) {
debugC(7, kDebugVideo, "Groovie::Player: Delaying %d (currTime=%d, _lastFrameTime=%d, millisDiff=%d, _millisBetweenFrame=%.2f, _frameTimeDrift=%.2f)",
millisSleep, currTime, _lastFrameTime, millisDiff, _millisBetweenFrames, _frameTimeDrift);
_syst->delayMillis(millisSleep);
currTime = _syst->getMillis();
debugC(7, kDebugVideo, "Groovie::Player: Finished delay at %d", currTime);
}
_frameTimeDrift = fMillis - float(currTime - _lastFrameTime);
if (abs(_frameTimeDrift) >= _millisBetweenFrames) {
_frameTimeDrift = 0;
}
debugC(6, kDebugVideo, "Groovie::Player: Frame displayed at %d (%f FPS), _frameTimeDrift=%.2f", currTime, 1000.0 / (currTime - _lastFrameTime), _frameTimeDrift);
_lastFrameTime = currTime;
}
}
} // End of Groovie namespace

View File

@@ -0,0 +1,89 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_VIDEO_PLAYER_H
#define GROOVIE_VIDEO_PLAYER_H
#include "common/system.h"
#include "video/subtitles.h"
namespace Audio {
class QueuingAudioStream;
}
namespace Groovie {
class GroovieEngine;
class VideoPlayer {
public:
VideoPlayer(GroovieEngine *vm);
virtual ~VideoPlayer() {}
bool load(Common::SeekableReadStream *file, uint16 flags);
bool playFrame();
virtual void resetFlags() {}
virtual void setOrigin(int16 x, int16 y) {}
virtual void stopAudioStream() = 0;
void fastForward();
bool isFastForwarding();
virtual void drawString(Graphics::Surface *surface, const Common::String &text, int posx, int posy, uint32 color, bool blackBackground) {}
virtual void copyfgtobg(uint8 arg) {}
void setOverrideSpeed(bool isOverride);
void loadSubtitles(const char *fname) { _subtitles.loadSRTFile(fname); }
void unloadSubtitles();
virtual bool isFileHandled() { return false; }
protected:
// To be implemented by subclasses
virtual uint16 loadInternal() = 0;
virtual bool playFrameInternal() = 0;
bool getOverrideSpeed() const { return _overrideSpeed; }
GroovieEngine *_vm;
OSystem *_syst;
Common::SeekableReadStream *_file;
uint16 _flags;
Audio::QueuingAudioStream *_audioStream;
private:
// Synchronization stuff
bool _begunPlaying;
bool _overrideSpeed;
uint16 _fps;
float _millisBetweenFrames;
uint32 _lastFrameTime;
float _frameTimeDrift;
uint32 _startTime;
Video::Subtitles _subtitles;
protected:
virtual void waitFrame();
};
} // End of Groovie namespace
#endif // GROOVIE_VIDEO_PLAYER_H

File diff suppressed because it is too large Load Diff

138
engines/groovie/video/roq.h Normal file
View File

@@ -0,0 +1,138 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_VIDEO_ROQ_H
#define GROOVIE_VIDEO_ROQ_H
#include "groovie/video/player.h"
#include "audio/mixer.h"
namespace Video {
class VideoDecoder;
}
namespace Groovie {
class GroovieEngine;
struct ROQBlockHeader {
uint16 type;
uint32 size;
uint16 param;
};
class ROQPlayer : public VideoPlayer {
public:
ROQPlayer(GroovieEngine *vm);
~ROQPlayer();
void setOrigin(int16 x, int16 y) override;
Audio::SoundHandle getSoundHandle() {
return _soundHandle;
}
void drawString(Graphics::Surface *surface, const Common::String &text, int posx, int posy, uint32 color, bool blackBackground) override;
void copyfgtobg(uint8 arg) override;
bool isFileHandled() override { return _isFileHandled; }
protected:
void waitFrame() override;
uint16 loadInternal() override;
bool playFrameInternal() override;
void stopAudioStream() override;
virtual void createAudioStream(bool stereo);
Audio::SoundHandle _soundHandle;
Graphics::Surface *_bg, *_screen, *_overBuf;
Graphics::Surface *_currBuf, *_prevBuf;
private:
bool readBlockHeader(ROQBlockHeader &blockHeader);
bool isValidBlockHeaderType(uint16 blockHeaderType);
bool processBlock();
bool processBlockInfo(ROQBlockHeader &blockHeader);
bool processBlockQuadCodebook(ROQBlockHeader &blockHeader);
bool processBlockQuadVector(ROQBlockHeader &blockHeader);
void processBlockQuadVectorBlock(int baseX, int baseY);
void processBlockQuadVectorBlockSub(int baseX, int baseY);
bool processBlockStill(ROQBlockHeader &blockHeader);
bool processBlockSoundMono(ROQBlockHeader &blockHeader);
bool processBlockSoundStereo(ROQBlockHeader &blockHeader);
bool processBlockAudioContainer(ROQBlockHeader &blockHeader);
bool playFirstFrame() { return _flagNoPlay; }; // _alpha && !_flagOverlay; }
void clearOverlay();
void dumpAllSurfaces(const Common::String &funcname);
void paint2(byte i, int destx, int desty);
void paint4(byte i, int destx, int desty);
void paint8(byte i, int destx, int desty);
void copy(byte size, int destx, int desty, int dx, int dy);
// Origin
int16 _origX, _origY;
//int16 _screenOffset;
void calcStartStop(int &start, int &stop, int origin, int length);
// Block coding type
byte getCodingType();
uint16 _codingType;
byte _codingTypeCount;
// Codebooks
uint16 _num2blocks;
uint16 _num4blocks;
uint32 _codebook2[256 * 4];
byte _codebook4[256 * 4];
// Flags
bool _flagNoPlay; //!< Play only first frame and do not print the image to the screen
bool _flagOverlay; //!< If _flagNoPlay is set. Copy frame to the foreground otherwise to the background
bool _altMotionDecoder; // Some ROQ vids use a variation on the copy codeblock
bool _flagMasked; //!< Clear the video instead of play it, used in pente
// Buffers
void redrawRestoreArea(int screenOffset, bool force);
void buildShowBuf();
byte _scaleX, _scaleY;
byte _offScale;
int8 _motionOffX, _motionOffY;
bool _interlacedVideo;
bool _dirty;
byte _alpha;
bool _firstFrame;
Common::Rect *_restoreArea; // Area to be repainted by foreground
Video::VideoDecoder *_videoDecoder;
bool _isFileHandled;
};
class ROQSoundPlayer : public ROQPlayer {
public:
ROQSoundPlayer(GroovieEngine *vm);
~ROQSoundPlayer();
void createAudioStream(bool stereo) override;
};
} // End of Groovie namespace
#endif // GROOVIE_VIDEO_ROQ_H

View File

@@ -0,0 +1,584 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "groovie/video/vdx.h"
#include "groovie/graphics.h"
#include "groovie/groovie.h"
#include "groovie/lzss.h"
#include "common/debug.h"
#include "common/debug-channels.h"
#include "common/textconsole.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
#include "audio/decoders/raw.h"
#include "graphics/paletteman.h"
#define TILE_SIZE 4 // Size of each tile on the image: only ever seen 4 so far
#define VDX_IDENT 0x9267 // 37479
namespace Groovie {
VDXPlayer::VDXPlayer(GroovieEngine *vm) :
VideoPlayer(vm), _origX(0), _origY(0), _flagOnePrev(false),
_fg(&_vm->_graphicsMan->_foreground), _bg(&_vm->_graphicsMan->_background) {
}
VDXPlayer::~VDXPlayer() {
//delete _audioStream;
}
void VDXPlayer::resetFlags() {
_flagOnePrev = false;
}
void VDXPlayer::setOrigin(int16 x, int16 y) {
_origX = x;
_origY = y;
}
void VDXPlayer::stopAudioStream() {
if (_audioStream) {
g_system->getMixer()->stopHandle(_soundHandle);
}
_audioStream = nullptr;
}
uint16 VDXPlayer::loadInternal() {
if (DebugMan.isDebugChannelEnabled(kDebugVideo)) {
int8 i;
debugN(1, "Groovie::VDX: New VDX: bitflags are ");
for (i = 15; i >= 0; i--) {
debugN(1, "%d", _flags & (1 << i)? 1 : 0);
if (i % 4 == 0) {
debugN(1, " ");
}
}
debug(1, " <- 0 ");
}
// Flags:
// - 1 Puzzle piece? Skip palette, don't redraw full screen, draw still to b/ack buffer
// - 2 Transparent color is 0xFF
// - 5 Skip still chunks
// - 7
// - 8 Just show the first frame
// - 9 Start a palette fade in
_flagZero = ((_flags & (1 << 0)) != 0);
_flagOne = ((_flags & (1 << 1)) != 0);
_flag2Byte = (_flags & (1 << 2)) ? 0xFF : 0x00;
_flagThree = ((_flags & (1 << 3)) != 0);
_flagFour = ((_flags & (1 << 4)) != 0);
_flagFive = ((_flags & (1 << 5)) != 0);
_flagSix = ((_flags & (1 << 6)) != 0);
_flagSeven = ((_flags & (1 << 7)) != 0);
_flagEight = ((_flags & (1 << 8)) != 0);
_flagNine = ((_flags & (1 << 9)) != 0);
// Enable highspeed if we're not obeying fps, and not marked as special
// This will be disabled in chunk audio if we're actually an audio vdx
if (_vm->_modeSpeed == kGroovieSpeedFast && ((_flags & (1 << 15)) == 0))
setOverrideSpeed(true);
if (_flagOnePrev && !_flagOne && !_flagEight) {
_flagSeven = true;
}
// Save _flagOne for the next video
_flagOnePrev = _flagOne;
//_flagTransparent = _flagOne;
_flagFirstFrame = _flagEight;
//_flagSkipPalette = _flagSeven;
_flagSkipPalette = false;
//_flagSkipStill = _flagFive || _flagSeven;
//_flagUpdateStill = _flagNine || _flagSix;
// Begin reading the file
debugC(1, kDebugVideo, "Groovie::VDX: Playing video");
if (_file->readUint16LE() != VDX_IDENT) {
error("Groovie::VDX: This does not appear to be a 7th guest VDX file");
return 0;
} else {
debugC(5, kDebugVideo, "Groovie::VDX: VDX file identified correctly");
}
uint16 tmp;
// Skip unknown data: 6 bytes, ref Martine
tmp = _file->readUint16LE();
debugC(2, kDebugVideo, "Groovie::VDX: Martine1 = 0x%04X", tmp);
tmp = _file->readUint16LE();
debugC(2, kDebugVideo, "Groovie::VDX: Martine2 = 0x%04X", tmp);
tmp = _file->readUint16LE();
debugC(2, kDebugVideo, "Groovie::VDX: Martine3 (FPS?) = %d", tmp);
return tmp;
}
bool VDXPlayer::playFrameInternal() {
byte currRes = 0x80;
Common::ReadStream *vdxData = nullptr;
while (currRes == 0x80) {
currRes = _file->readByte();
// Skip unknown data: 1 byte, ref Edward
byte tmp = _file->readByte();
uint32 compSize = _file->readUint32LE();
uint8 lengthmask = _file->readByte();
uint8 lengthbits = _file->readByte();
if (_file->eos())
break;
debugC(5, kDebugVideo, "Groovie::VDX: Edward = 0x%04X", tmp);
// Read the chunk data and decompress if needed
if (compSize)
vdxData = _file->readStream(compSize);
if (lengthmask && lengthbits) {
Common::ReadStream *decompData = new LzssReadStream(vdxData, lengthmask, lengthbits);
delete vdxData;
vdxData = decompData;
}
// Use the current chunk
switch (currRes) {
case 0x00:
debugC(6, kDebugVideo, "Groovie::VDX: Replay frame");
break;
case 0x20:
debugC(5, kDebugVideo, "Groovie::VDX: Still frame");
getStill(vdxData);
break;
case 0x25:
debugC(5, kDebugVideo, "Groovie::VDX: Animation frame");
getDelta(vdxData);
break;
case 0x80:
debugC(5, kDebugVideo, "Groovie::VDX: Sound resource");
chunkSound(vdxData);
break;
default:
error("Groovie::VDX: Invalid resource type: %d", currRes);
}
delete vdxData;
vdxData = nullptr;
}
// Wait until the current frame can be shown
waitFrame();
// TODO: Move it to a better place
// Update the screen
if (currRes == 0x25) {
//if (_flagSeven) {
//_vm->_graphicsMan->mergeFgAndBg();
//}
_vm->_graphicsMan->updateScreen(_bg);
}
// Report the end of the video if we reached the end of the file or if we
// just wanted to play one frame.
if (_file->eos() || _flagFirstFrame) {
_origX = _origY = 0;
return 1;
} else {
return 0;
}
}
static const uint16 vdxBlockMapLookup[] = {
0xc800, 0xec80, 0xfec8, 0xffec, 0xfffe, 0x3100, 0x7310, 0xf731,
0xff73, 0xfff7, 0x6c80, 0x36c8, 0x136c, 0x6310, 0xc631, 0x8c63,
0xf000, 0xff00, 0xfff0, 0x1111, 0x3333, 0x7777, 0x6666, 0xcccc,
0x0ff0, 0x00ff, 0xffcc, 0x0076, 0xff33, 0x0ee6, 0xccff, 0x6770,
0x33ff, 0x6ee0, 0x4800, 0x2480, 0x1248, 0x0024, 0x0012, 0x2100,
0x4210, 0x8421, 0x0042, 0x0084, 0xf888, 0x0044, 0x0032, 0x111f,
0x22e0, 0x4c00, 0x888f, 0x4470, 0x2300, 0xf111, 0x0e22, 0x00c4,
0xf33f, 0xfccf, 0xff99, 0x99ff, 0x4444, 0x2222, 0xccee, 0x7733,
0x00f8, 0x00f1, 0x00bb, 0x0cdd, 0x0f0f, 0x0f88, 0x13f1, 0x19b3,
0x1f80, 0x226f, 0x27ec, 0x3077, 0x3267, 0x37e4, 0x38e3, 0x3f90,
0x44cf, 0x4cd9, 0x4c99, 0x5555, 0x603f, 0x6077, 0x6237, 0x64c9,
0x64cd, 0x6cd9, 0x70ef, 0x0f00, 0x00f0, 0x0000, 0x4444, 0x2222
};
void VDXPlayer::getDelta(Common::ReadStream *in) {
uint16 k, l;
// Get the size of the local palette
uint16 palSize = in->readUint16LE();
// Load the palette if it isn't empty
if (palSize) {
uint16 palBitField[16];
// Load the bit field
for (l = 0; l < 16; l++) {
palBitField[l] = in->readUint16LE();
}
// Load the actual palette
for (l = 0; l < 16; l++) {
int flag = 1 << 15;
for (uint16 j = 0; j < 16; j++) {
int palIndex = (l * 16) + j;
if (flag & palBitField[l]) {
for (k = 0; k < 3; k++) {
_palBuf[(palIndex * 3) + k] = in->readByte();
}
}
flag = flag >> 1;
}
}
// Apply the palette
if (!_flagSeven) {
//if (!_flagSix && !_flagSeven) {
setPalette(_palBuf);
}
}
uint8 currOpCode = in->readByte();
uint8 param1, param2, param3;
uint16 currentLine = 0;
uint32 offset = 0;
while (!in->eos()) {
byte colors[16];
if (currOpCode < 0x60) {
param1 = in->readByte();
param2 = in->readByte();
expandColorMap(colors, vdxBlockMapLookup[currOpCode], param1, param2);
decodeBlockDelta(offset, colors, 640);
offset += TILE_SIZE;
} else if (currOpCode > 0x7f) {
param1 = in->readByte();
param2 = in->readByte();
param3 = in->readByte();
expandColorMap(colors, (param1 << 8) + currOpCode, param2, param3);
decodeBlockDelta(offset, colors, 640);
offset += TILE_SIZE;
} else switch (currOpCode) {
case 0x60: /* Fill tile with the 16 colors given as parameters */
for (l = 0; l < 16; l++) {
colors[l] = in->readByte();
}
decodeBlockDelta(offset, colors, 640);
offset += TILE_SIZE;
break;
case 0x61: /* Skip to the end of this line, next block is start of next */
/* Note this is used at the end of EVERY line */
currentLine++;
offset = currentLine * TILE_SIZE * 640;
break;
case 0x62:
case 0x63:
case 0x64:
case 0x65:
case 0x66:
case 0x67:
case 0x68:
case 0x69:
case 0x6a:
case 0x6b: /* Skip next param1 blocks (within line) */
offset += (currOpCode - 0x62) * TILE_SIZE;
break;
case 0x6c:
case 0x6d:
case 0x6e:
case 0x6f:
case 0x70:
case 0x71:
case 0x72:
case 0x73:
case 0x74:
case 0x75: /* Next param1 blocks are filled with color param2 */
param1 = currOpCode - 0x6b;
param2 = in->readByte();
for (l = 0; l < 16; l++) {
colors[l] = param2;
}
for (k = 0; k < param1; k++) {
decodeBlockDelta(offset, colors, 640);
offset += TILE_SIZE;
}
break;
case 0x76:
case 0x77:
case 0x78:
case 0x79:
case 0x7a:
case 0x7b:
case 0x7c:
case 0x7d:
case 0x7e:
case 0x7f: /* Next bytes contain colors to fill the next param1 blocks in the current line*/
param1 = currOpCode - 0x75;
for (k = 0; k < param1; k++) {
param2 = in->readByte();
for (l = 0; l < 16; l++) {
colors[l] = param2;
}
decodeBlockDelta(offset, colors, 640);
offset += TILE_SIZE;
}
break;
default:
error("Groovie::VDX: Broken somehow");
}
currOpCode = in->readByte();
}
}
void VDXPlayer::getStill(Common::ReadStream *in) {
uint16 numXTiles = in->readUint16LE();
debugC(5, kDebugVideo, "Groovie::VDX: numXTiles=%d", numXTiles);
uint16 numYTiles = in->readUint16LE();
debugC(5, kDebugVideo, "Groovie::VDX: numYTiles=%d", numYTiles);
// It's skipped in the original:
uint16 colorDepth = in->readUint16LE();
debugC(5, kDebugVideo, "Groovie::VDX: colorDepth=%d", colorDepth);
uint16 imageWidth = TILE_SIZE * numXTiles;
uint8 mask = 0;
byte *buf;
if (_flagOne) {
// Paint to the foreground
buf = (byte *)_fg->getPixels();
if (_flag2Byte) {
mask = 0xff;
} else {
mask = 0;
}
// TODO: Verify this is the right procedure. Couldn't find it on the
// disassembly, but it's required to work properly
_flagFirstFrame = true;
} else {
// Paint to the background
buf = (byte *)_bg->getPixels();
}
// Read the palette
in->read(_palBuf, 3 * 256);
if (_flagSeven) {
_flagFive = true;
}
// Skip the frame when flag 5 is set, unless flag 1 is set
if (!_flagFive || _flagOne) {
byte colors[16];
for (uint16 j = 0; j < numYTiles; j++) {
byte *currentTile = buf + j * TILE_SIZE * imageWidth;
for (uint16 i = numXTiles; i; i--) {
uint8 color1 = in->readByte();
uint8 color0 = in->readByte();
uint16 colorMap = in->readUint16LE();
expandColorMap(colors, colorMap, color1, color0);
decodeBlockStill(currentTile, colors, 640, mask);
currentTile += TILE_SIZE;
}
}
// Apply the palette
if (_flagNine) {
// Flag 9 starts a fade in
if (!isFastForwarding())
fadeIn(_palBuf);
else
setPalette(_palBuf);
} else {
if (!_flagOne && !_flagSeven) {
// Actually apply the palette
setPalette(_palBuf);
}
}
if (!_flagOne) {
_vm->_graphicsMan->updateScreen(_bg);
}
/*
if (_flagSix) {
if (_flagOne) {
_vm->_graphicsMan->updateScreen(_fg);
} else {
_vm->_graphicsMan->updateScreen(_bg);
}
_flagSix = 0;
}
*/
} else {
// Skip the remaining data
debugC(10, kDebugVideo, "Groovie::VDX: Skipping still frame");
while (!in->eos()) {
in->readByte();
}
}
}
void VDXPlayer::expandColorMap(byte *out, uint16 colorMap, uint8 color1, uint8 color0) {
// It's a bit faster to start from the end
out += 16;
for (int i = 16; i; i--) {
// Set the corresponding color
// The following is an optimized version of:
// *--out = (colorMap & 1) ? color1 : color0;
uint8 selector = -(colorMap & 1);
*--out = (selector & color1) | (~selector & color0);
// Update the flag map to test the next color
colorMap >>= 1;
}
}
void VDXPlayer::decodeBlockStill(byte *buf, byte *colors, uint16 imageWidth, uint8 mask) {
assert(TILE_SIZE == 4);
for (int y = TILE_SIZE; y; y--) {
if (_flagOne) {
// TODO: optimize with bit logic?
for (int x = 0; x < TILE_SIZE; x++) {
// 0xff pixels don't modify the buffer
if (*colors != 0xff) {
// Write the color
*buf = *colors | mask;
// Note: if the mask is 0, it paints the image
// else, it paints the image's mask using 0xff
}
// Point to the next color
colors++;
// Point to the next pixel
buf++;
}
// Point to the start of the next line
buf += imageWidth - TILE_SIZE;
} else {
*((uint32 *)buf) = *((uint32 *)colors);
colors += 4;
// Point to the start of the next line
buf += imageWidth;
}
}
}
void VDXPlayer::decodeBlockDelta(uint32 offset, byte *colors, uint16 imageWidth) {
assert(TILE_SIZE == 4);
byte *dest;
// TODO: Verify just the else block is required
//if (_flagOne) {
// Paint to the foreground
//dest = (byte *)_fg->getPixels() + offset;
//} else {
dest = (byte *)_bg->getPixels() + offset;
//}
// Move the pointers to the beginning of the current block
int32 blockOff = _origX + _origY * imageWidth;
dest += blockOff;
byte *fgBuf = nullptr;
if (_flagSeven) {
fgBuf = (byte *)_fg->getPixels() + offset + blockOff;
//byte *bgBuf = (byte *)_bg->getPixels() + offset + blockOff;
}
for (int y = TILE_SIZE; y; y--) {
if (_flagSeven) {
// Paint mask
for (int x = 0; x < TILE_SIZE; x++) {
// TODO: this can probably be optimized with bit logic
if (fgBuf[x] != 0xff) {
if (*colors == 0xff) {
dest[x] = fgBuf[x];
} else {
dest[x] = *colors;
}
}
colors++;
}
fgBuf += imageWidth;
} else {
// Paint directly
*((uint32 *)dest) = *((uint32 *)colors);
colors += 4;
}
// Move to the next line
dest += imageWidth;
}
}
void VDXPlayer::chunkSound(Common::ReadStream *in) {
if (getOverrideSpeed())
setOverrideSpeed(false);
if (!_audioStream && !isFastForwarding()) {
_audioStream = Audio::makeQueuingAudioStream(22050, false);
g_system->getMixer()->playStream(Audio::Mixer::kSpeechSoundType, &_soundHandle, _audioStream);
}
byte *data = (byte *)malloc(60000);
int chunksize = in->read(data, 60000);
if (!isFastForwarding()) {
_audioStream->queueBuffer(data, chunksize, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
} else {
free(data);
}
}
void VDXPlayer::fadeIn(uint8 *targetpal) {
// Don't do anything if we're asked to skip palette changes
if (_flagSkipPalette)
return;
// TODO: Is it required? If so, move to an appropriate place
// Copy the foreground to the background
memcpy((byte *)_vm->_graphicsMan->_foreground.getPixels(), (byte *)_vm->_graphicsMan->_background.getPixels(), 640 * 320);
// Start a fadein
_vm->_graphicsMan->fadeIn(targetpal);
// Show the background
_vm->_graphicsMan->updateScreen(_bg);
}
void VDXPlayer::setPalette(uint8 *palette) {
if (_flagSkipPalette)
return;
debugC(7, kDebugVideo, "Groovie::VDX: Setting palette");
_syst->getPaletteManager()->setPalette(palette, 0, 256);
}
} // End of Groovie namespace

View File

@@ -0,0 +1,85 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef GROOVIE_VIDEO_VDX_H
#define GROOVIE_VIDEO_VDX_H
#include "groovie/video/player.h"
#include "audio/mixer.h"
namespace Common {
class ReadStream;
}
namespace Groovie {
class VDXPlayer : public VideoPlayer {
public:
VDXPlayer(GroovieEngine *vm);
~VDXPlayer() override;
void resetFlags() override;
void setOrigin(int16 x, int16 y) override;
protected:
uint16 loadInternal() override;
bool playFrameInternal() override;
void stopAudioStream() override;
private:
Graphics::Surface *_fg, *_bg;
uint8 _palBuf[3 * 256];
Audio::SoundHandle _soundHandle;
// Origin
int16 _origX, _origY;
// Video flags
bool _flagZero;
bool _flagOne;
bool _flagOnePrev;
byte _flag2Byte;
bool _flagThree;
bool _flagFour;
bool _flagFive;
bool _flagSix;
bool _flagSeven;
bool _flagEight;
bool _flagNine;
//bool _flagSkipStill;
bool _flagSkipPalette;
bool _flagFirstFrame;
//bool _flagTransparent;
//bool _flagUpdateStill;
void getStill(Common::ReadStream *in);
void getDelta(Common::ReadStream *in);
void expandColorMap(byte *out, uint16 colorMap, uint8 color1, uint8 color0);
void decodeBlockStill(byte *buf, byte *colors, uint16 imageWidth, uint8 mask);
void decodeBlockDelta(uint32 offset, byte *colors, uint16 imageWidth);
void chunkSound(Common::ReadStream *in);
void setPalette(uint8 *palette);
void fadeIn(uint8 *palette);
};
} // End of Groovie namespace
#endif // GROOVIE_VIDEO_VDX_H