/* 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 . * */ #include "common/algorithm.h" #include "vcruise/circuitpuzzle.h" namespace VCruise { namespace CircuitPuzzleTables { // These are hard-coded into the Schizm executable (they're 32-bit in it), figured these out by hand // from tracing screen captures and feeding through a boundary finder script. static const int16 g_barriersHorizontal1[100] = { 166, 6, 191, 62, 244, 7, 267, 62, 320, 7, 341, 62, 394, 8, 416, 63, 469, 8, 492, 62, 164, 80, 191, 134, 244, 80, 265, 130, 320, 80, 336, 131, 394, 80, 415, 130, 468, 80, 492, 130, 163, 152, 191, 199, 245, 152, 263, 199, 320, 152, 340, 199, 395, 152, 415, 199, 469, 152, 493, 199, 166, 221, 187, 270, 245, 221, 266, 270, 320, 220, 341, 271, 394, 221, 414, 271, 469, 221, 492, 271, 166, 290, 187, 343, 244, 290, 263, 343, 320, 290, 341, 343, 395, 289, 415, 343, 469, 291, 494, 346, }; static const int16 g_barriersVertical1[64] = { 187, 64, 240, 84, 266, 63, 316, 84, 340, 64, 392, 83, 416, 64, 470, 84, 186, 135, 240, 151, 266, 135, 315, 152, 341, 134, 393, 151, 416, 135, 479, 152, 187, 204, 239, 221, 266, 203, 314, 221, 341, 204, 391, 220, 416, 204, 470, 220, 187, 273, 239, 293, 266, 271, 314, 292, 342, 271, 391, 292, 416, 272, 471, 292, }; static const int16 g_barriersHorizontal2[100] = { 160, 8, 185, 62, 239, 7, 260, 63, 309, 8, 332, 63, 388, 8, 407, 63, 455, 8, 485, 63, 162, 79, 184, 130, 241, 80, 260, 131, 313, 80, 332, 131, 387, 82, 408, 131, 460, 82, 484, 131, 162, 153, 184, 200, 238, 153, 258, 200, 312, 153, 333, 201, 386, 153, 408, 201, 459, 153, 485, 200, 161, 220, 183, 270, 237, 220, 259, 270, 313, 221, 332, 270, 384, 220, 408, 269, 459, 219, 485, 267, 161, 289, 183, 343, 238, 288, 259, 350, 312, 289, 332, 341, 383, 291, 408, 342, 460, 290, 484, 341, }; static const int16 g_barriersVertical2[64] = { 186, 63, 239, 84, 263, 64, 313, 84, 337, 64, 389, 84, 411, 64, 464, 85, 184, 135, 238, 151, 259, 136, 311, 151, 336, 135, 388, 152, 410, 136, 465, 152, 184, 203, 238, 220, 263, 204, 311, 220, 338, 203, 388, 219, 408, 203, 464, 219, 185, 272, 237, 291, 260, 272, 313, 291, 337, 271, 388, 290, 411, 271, 463, 289, }; static const int16 g_linksHorizontal1[100] = { 136, 24, 206, 38, 216, 24, 284, 38, 294, 24, 363, 38, 374, 24, 442, 38, 452, 24, 520, 38, 136, 97, 205, 110, 215, 97, 284, 110, 294, 98, 363, 110, 373, 98, 442, 110, 451, 98, 520, 110, 137, 170, 206, 182, 216, 170, 284, 182, 294, 170, 363, 182, 373, 170, 442, 182, 452, 170, 520, 182, 137, 242, 204, 255, 216, 242, 284, 255, 295, 242, 363, 254, 374, 242, 441, 254, 452, 242, 520, 254, 137, 315, 204, 328, 216, 315, 284, 328, 295, 315, 362, 327, 374, 315, 441, 327, 452, 315, 520, 327, }; static const int16 g_linksVertical1[64] = { 205, 36, 217, 98, 284, 36, 295, 99, 362, 36, 374, 99, 441, 37, 452, 99, 205, 109, 217, 171, 284, 109, 295, 171, 363, 109, 374, 171, 441, 109, 452, 170, 205, 181, 217, 244, 284, 181, 295, 243, 362, 181, 374, 243, 441, 181, 452, 243, 205, 254, 217, 316, 284, 254, 295, 316, 362, 254, 374, 316, 441, 254, 453, 316, }; static const int16 g_linksHorizontal2[100] = { 135, 26, 206, 39, 214, 27, 284, 39, 292, 27, 362, 40, 370, 28, 439, 41, 447, 29, 515, 40, 135, 98, 206, 111, 214, 99, 284, 111, 292, 99, 362, 111, 370, 99, 439, 112, 447, 100, 515, 112, 135, 170, 206, 182, 214, 170, 284, 182, 293, 170, 362, 182, 370, 170, 438, 182, 447, 170, 514, 182, 135, 241, 206, 255, 214, 241, 285, 254, 293, 241, 362, 254, 370, 241, 438, 253, 447, 241, 515, 253, 135, 314, 206, 327, 214, 314, 284, 326, 292, 313, 362, 325, 370, 313, 438, 325, 447, 312, 515, 324, }; static const int16 g_linksVertical2[64] = { 204, 37, 216, 100, 283, 38, 294, 100, 360, 39, 372, 100, 437, 39, 449, 102, 204, 109, 216, 171, 283, 110, 294, 171, 360, 110, 371, 171, 437, 111, 448, 171, 204, 181, 216, 242, 283, 181, 294, 242, 360, 181, 371, 242, 436, 181, 448, 242, 204, 253, 216, 315, 283, 252, 294, 315, 360, 252, 372, 314, 436, 251, 448, 314, }; } // End of namespace CircuitPuzzleTables struct CircuitPuzzleAIEvaluator { CircuitPuzzleAIEvaluator(); static const uint kMaxMovesToReach = CircuitPuzzle::kBoardWidth * CircuitPuzzle::kBoardHeight * 2; uint stepsToReach[CircuitPuzzle::kBoardWidth][CircuitPuzzle::kBoardHeight]; }; class CircuitPuzzleVisitedSet { public: CircuitPuzzleVisitedSet(); void set(const Common::Point &coord); bool get(const Common::Point &coord) const; void clear(); private: uint32 _bits; }; CircuitPuzzleVisitedSet::CircuitPuzzleVisitedSet() : _bits(0) { } void CircuitPuzzleVisitedSet::set(const Common::Point &coord) { int bit = coord.y * static_cast(CircuitPuzzle::kBoardWidth) + coord.x; _bits |= (1u << bit); } bool CircuitPuzzleVisitedSet::get(const Common::Point &coord) const { int bit = coord.y * static_cast(CircuitPuzzle::kBoardWidth) + coord.x; return (_bits & (1u << bit)) != 0; } void CircuitPuzzleVisitedSet::clear() { _bits = 0; } CircuitPuzzleAIEvaluator::CircuitPuzzleAIEvaluator() { for (uint x = 0; x < CircuitPuzzle::kBoardWidth; x++) for (uint y = 0; y < CircuitPuzzle::kBoardHeight; y++) stepsToReach[x][y] = kMaxMovesToReach; } CircuitPuzzle::Action::Action() : _direction(kCellDirectionDown) { } CircuitPuzzle::CircuitPuzzle(int layout) : _havePreviousAction(false) { _startPoint = Common::Point(0, 0); _goalPoint = Common::Point(kBoardWidth - 1, 0); const int16 *linksHoriz = nullptr; const int16 *linksVert = nullptr; const int16 *barriersHoriz = nullptr; const int16 *barriersVert = nullptr; if (layout == 1) { linksHoriz = CircuitPuzzleTables::g_linksHorizontal1; linksVert = CircuitPuzzleTables::g_linksVertical1; barriersHoriz = CircuitPuzzleTables::g_barriersHorizontal1; barriersVert = CircuitPuzzleTables::g_barriersVertical1; } else if (layout == 2) { linksHoriz = CircuitPuzzleTables::g_linksHorizontal2; linksVert = CircuitPuzzleTables::g_linksVertical2; barriersHoriz = CircuitPuzzleTables::g_barriersHorizontal2; barriersVert = CircuitPuzzleTables::g_barriersVertical2; } else error("Unknown circuit screen layout"); // Pre-connect the side rails for (uint i = 0; i < (kBoardHeight - 1u); i++) { *getConnectionState(Common::Point(0, i), KDirectionDown) = kLinkStateConnected; *getConnectionState(Common::Point(kBoardWidth - 1, i), KDirectionDown) = kLinkStateConnected; } // Block edge points for (uint i = 0; i < kBoardWidth; i++) _cells[i][kBoardHeight - 1]._downLink = kLinkStateBlocked; for (uint i = 0; i < kBoardHeight; i++) _cells[kBoardWidth - 1][i]._rightLink = kLinkStateBlocked; // Barriers are traced from pixel matches, but links are traced from the highlight boxes. // Since the highlight boxes are (1,1) larger than the clipping box of the animation, and because // the coordinates are inclusive, we need to add (1,1) to barrier sizes, but not link sizes, since the // inclusive (+1,+1) cancels out from the oversize (-1,-1) adjustment. // Resolve horizontal links and barriers for (uint y = 0; y < kBoardHeight; y++) { for (uint x = 0; x < (kBoardWidth - 1u); x++) { uint rectDataOffset = (x + y * (kBoardWidth - 1u)) * 4u; CellRectSpec &rectSpec = _cellRectSpecs[x][y]; rectSpec._rightBarrierRect = Common::Rect(barriersHoriz[rectDataOffset + 0], barriersHoriz[rectDataOffset + 1], barriersHoriz[rectDataOffset + 2] + 1, barriersHoriz[rectDataOffset + 3] + 1); rectSpec._rightLinkRect = Common::Rect(linksHoriz[rectDataOffset + 0], linksHoriz[rectDataOffset + 1], linksHoriz[rectDataOffset + 2], linksHoriz[rectDataOffset + 3]); } } // Resolve vertical links and barriers. Skip the first and last column. for (uint y = 0; y < (kBoardHeight - 1u); y++) { for (uint x = 1; x < (kBoardWidth - 1u); x++) { uint rectDataOffset = ((x - 1) + y * (kBoardWidth - 2u)) * 4u; CellRectSpec &rectSpec = _cellRectSpecs[x][y]; rectSpec._downBarrierRect = Common::Rect(barriersVert[rectDataOffset + 0], barriersVert[rectDataOffset + 1], barriersVert[rectDataOffset + 2] + 1, barriersVert[rectDataOffset + 3] + 1); rectSpec._downLinkRect = Common::Rect(linksVert[rectDataOffset + 0], linksVert[rectDataOffset + 1], linksVert[rectDataOffset + 2], linksVert[rectDataOffset + 3]); } } } bool CircuitPuzzle::executeAIAction(Common::RandomSource &randomSource, Common::Point &outCoord, CellDirection &outBlockDirection) { // Don't know exactly what algorithm Schizm uses, we use something that approximates the original // pretty well most of the time: // - Identify all connection paths that are tied for the fewest number of new connections required to win. // - Enumerate all open connections on those paths. // - If the previous move blocked a horizontal connection (i.e. with a vertical barrier), prioritize // connections on the same X coordinate as that block. // - Block a random connection from the candidates. // // There seem to be times that Schizm doesn't do this. In particular, Schizm will (rarely) fail to block // a connection even if it's the only connection that will immediately win the puzzle for the player. // // It also doesn't prioritize making moves that will immediately win for the AI. CircuitPuzzleAIEvaluator evaluator; computeStepsToReach(evaluator); uint stepsToReachGoal = evaluator.stepsToReach[_goalPoint.x][_goalPoint.y]; if (stepsToReachGoal == 0 || stepsToReachGoal == CircuitPuzzleAIEvaluator::kMaxMovesToReach) return false; const uint kMaxLinks = kBoardWidth * kBoardHeight * 2; Action potentialBlocks[kMaxLinks]; uint numPotentialBlocks = 0; Common::Point pointsList1[kMaxLinks]; Common::Point pointsList2[kMaxLinks]; Common::Point *pointsToFloodFill = pointsList1; Common::Point *pointsToProspect = pointsList2; uint numPointsToFloodFill = 1; uint numPointsToProspect = 0; pointsToFloodFill[0] = _goalPoint; CircuitPuzzleVisitedSet visitedSet; uint prospectLevel = stepsToReachGoal; while (prospectLevel > 0) { floodFillLinks(pointsToFloodFill, numPointsToFloodFill, visitedSet); for (uint i = 0; i < numPointsToFloodFill; i++) { const Common::Point &pt = pointsToFloodFill[i]; for (uint dir = 0; dir < kDirectionCount; dir++) { const LinkState *linkState = getConnectionState(pt, static_cast(dir)); if (linkState && (*linkState) == kLinkStateOpen) { Common::Point connectedPoint = getConnectedPoint(pt, static_cast(dir)); if (!visitedSet.get(connectedPoint)) { visitedSet.set(connectedPoint); if (evaluator.stepsToReach[connectedPoint.x][connectedPoint.y] + 1u == prospectLevel) { // This point is on the shortest path Action action; switch (dir) { case kDirectionUp: action._point = connectedPoint; action._direction = kCellDirectionDown; break; case KDirectionDown: action._point = pt; action._direction = kCellDirectionDown; break; case kDirectionLeft: action._point = connectedPoint; action._direction = kCellDirectionRight; break; case kDirectionRight: action._point = pt; action._direction = kCellDirectionRight; break; default: error("Internal error: Bad direction"); return false; } potentialBlocks[numPotentialBlocks] = action; numPotentialBlocks++; pointsToProspect[numPointsToProspect] = connectedPoint; numPointsToProspect++; } } } } } Common::Point *tempList = pointsToFloodFill; pointsToFloodFill = pointsToProspect; pointsToProspect = tempList; numPointsToFloodFill = numPointsToProspect; numPointsToProspect = 0; prospectLevel--; } if (numPotentialBlocks == 0) return false; // All potential blocks are now on the shortest path. // Try to mimic some of the AI behavior of Schizm to form wall advances. // The highest-priority move is one that runs parallel to the previous move. uint selectedBlock = 0; if (numPotentialBlocks > 1) { uint blockQualities[kMaxLinks]; for (uint i = 0; i < numPotentialBlocks; i++) blockQualities[i] = 0; uint highestQuality = 0; if (_havePreviousAction) { for (uint i = 0; i < numPotentialBlocks; i++) { uint quality = 0; const Action &pblock = potentialBlocks[i]; // We don't want to favor horizontal walls because otherwise that triggers are degenerate behavior where the player can run a wall // directly across and the AI will keeps inserting horizontal walls parallel to the player action. bool isWallBlock = false; if (_previousAction._direction == kCellDirectionRight && pblock._direction == kCellDirectionRight && _previousAction._point.x == pblock._point.x) isWallBlock = true; #if 0 else if (_previousAction._direction == kCellDirectionDown && pblock._direction == kCellDirectionDown && _previousAction._point.y == pblock._point.y) isWallBlock = true; #endif // If this forms a vertical wall, it's quality 2 if (isWallBlock) quality = 2; else { // If this forms a corner, it's quality 1 (disabled, this seems less accurate) #if 0 if (_previousAction._direction != pblock._direction) { Common::Point prevAdjacent = _previousAction._point; if (_previousAction._direction == kCellDirectionRight) prevAdjacent.x++; else if (_previousAction._direction == kCellDirectionDown) prevAdjacent.y++; Common::Point pblockAdjacent = pblock._point; if (pblock._direction == kCellDirectionRight) pblockAdjacent.x++; else if (pblock._direction == kCellDirectionDown) pblockAdjacent.y++; if (prevAdjacent == pblock._point || prevAdjacent == pblockAdjacent || _previousAction._point == pblock._point || _previousAction._point == pblockAdjacent) quality = 1; } #endif } blockQualities[i] = quality; if (quality > highestQuality) highestQuality = quality; } } uint blocksInHighestQuality[kMaxLinks]; uint numBlocksInHighestQuality = 0; for (uint i = 0; i < numPotentialBlocks; i++) { if (blockQualities[i] == highestQuality) { blocksInHighestQuality[numBlocksInHighestQuality] = i; numBlocksInHighestQuality++; } } if (numBlocksInHighestQuality == 1) selectedBlock = blocksInHighestQuality[0]; else { assert(numBlocksInHighestQuality > 1); selectedBlock = blocksInHighestQuality[randomSource.getRandomNumber(numBlocksInHighestQuality - 1)]; } } const Action &pblock = potentialBlocks[selectedBlock]; outCoord = pblock._point; outBlockDirection = pblock._direction; if (pblock._direction == kCellDirectionDown) _cells[pblock._point.x][pblock._point.y]._downLink = kLinkStateBlocked; if (pblock._direction == kCellDirectionRight) _cells[pblock._point.x][pblock._point.y]._rightLink = kLinkStateBlocked; _havePreviousAction = true; _previousAction = pblock; return true; } void CircuitPuzzle::addLink(const Common::Point &coord, CellDirection direction) { validateCoord(coord); CellState &cell = _cells[coord.x][coord.y]; LinkState *linkState = nullptr; if (direction == kCellDirectionDown) linkState = &cell._downLink; else if (direction == kCellDirectionRight) linkState = &cell._rightLink; if (linkState == nullptr || (*linkState) != kLinkStateOpen) error("Internal error: Circuit link state was invalid"); *linkState = kLinkStateConnected; } CircuitPuzzle::Conclusion CircuitPuzzle::checkConclusion() const { CircuitPuzzleAIEvaluator evaluator; computeStepsToReach(evaluator); uint stepsToReachGoal = evaluator.stepsToReach[_goalPoint.x][_goalPoint.y]; if (stepsToReachGoal == 0) return kConclusionPlayerWon; if (stepsToReachGoal == CircuitPuzzleAIEvaluator::kMaxMovesToReach) return kConclusionPlayerLost; return kConclusionNone; } const CircuitPuzzle::CellRectSpec *CircuitPuzzle::getCellRectSpec(const Common::Point &coord) const { validateCoord(coord); return &_cellRectSpecs[coord.x][coord.y]; } bool CircuitPuzzle::isCellDownLinkOpen(const Common::Point &coord) const { validateCoord(coord); return _cells[coord.x][coord.y]._downLink == kLinkStateOpen; } bool CircuitPuzzle::isCellRightLinkOpen(const Common::Point &coord) const { validateCoord(coord); return _cells[coord.x][coord.y]._rightLink == kLinkStateOpen; } CircuitPuzzle::CellState::CellState() : _downLink(kLinkStateOpen), _rightLink(kLinkStateOpen) { } Common::Point CircuitPuzzle::getConnectedPoint(const Common::Point &coord, Direction direction) { switch (direction) { case kDirectionUp: return Common::Point(coord.x, coord.y - 1); case KDirectionDown: return Common::Point(coord.x, coord.y + 1); case kDirectionLeft: return Common::Point(coord.x - 1, coord.y); case kDirectionRight: return Common::Point(coord.x + 1, coord.y); default: return coord; }; } CircuitPuzzle::LinkState *CircuitPuzzle::getConnectionState(const Common::Point &coord, Direction direction) { if (!isPositionValid(coord)) return nullptr; switch (direction) { case kDirectionUp: if (coord.y == 0) return nullptr; return &_cells[coord.x][coord.y - 1]._downLink; case KDirectionDown: if (coord.y == static_cast(kBoardHeight - 1)) return nullptr; return &_cells[coord.x][coord.y]._downLink; case kDirectionLeft: if (coord.x <= 0) return nullptr; return &_cells[coord.x - 1][coord.y]._rightLink; case kDirectionRight: if (coord.x == static_cast(kBoardWidth - 1)) return nullptr; return &_cells[coord.x][coord.y]._rightLink; default: return nullptr; }; } const CircuitPuzzle::LinkState *CircuitPuzzle::getConnectionState(const Common::Point &coord, Direction direction) const { return const_cast(this)->getConnectionState(coord, direction); } bool CircuitPuzzle::isPositionValid(const Common::Point &coord) { if (coord.x < 0 || coord.y < 0 || coord.x >= static_cast(kBoardWidth) || coord.y >= static_cast(kBoardHeight)) return false; return true; } void CircuitPuzzle::computeStepsToReach(CircuitPuzzleAIEvaluator &evaluator) const { const uint kMaxLinks = kBoardWidth * kBoardHeight * 2; Common::Point pointsList1[kMaxLinks]; Common::Point pointsList2[kMaxLinks]; Common::Point *pointsToFloodFill = pointsList1; Common::Point *pointsToProspect = pointsList2; uint numPointsToFloodFill = 1; uint numPointsToProspect = 0; uint floodFillValue = 0; pointsToFloodFill[0] = _startPoint; for (uint x = 0; x < kBoardWidth; x++) for (uint y = 0; y < kBoardHeight; y++) evaluator.stepsToReach[x][y] = CircuitPuzzleAIEvaluator::kMaxMovesToReach; CircuitPuzzleVisitedSet visitedSet; while (numPointsToFloodFill > 0) { floodFillLinks(pointsToFloodFill, numPointsToFloodFill, visitedSet); for (uint i = 0; i < numPointsToFloodFill; i++) { const Common::Point &pt = pointsToFloodFill[i]; evaluator.stepsToReach[pt.x][pt.y] = floodFillValue; for (uint dir = 0; dir < kDirectionCount; dir++) { const LinkState *linkState = getConnectionState(pt, static_cast(dir)); if (linkState && (*linkState) == kLinkStateOpen) { Common::Point connectedPoint = getConnectedPoint(pt, static_cast(dir)); if (!visitedSet.get(connectedPoint)) { visitedSet.set(connectedPoint); pointsToProspect[numPointsToProspect] = connectedPoint; numPointsToProspect++; } } } } Common::Point *tempList = pointsToFloodFill; pointsToFloodFill = pointsToProspect; pointsToProspect = tempList; numPointsToFloodFill = numPointsToProspect; numPointsToProspect = 0; floodFillValue++; } } void CircuitPuzzle::floodFillLinks(Common::Point *pointsList, uint &listSize, CircuitPuzzleVisitedSet &visitedSet) const { for (uint i = 0; i < listSize; i++) { const Common::Point &pt = pointsList[i]; visitedSet.set(pt); for (uint dir = 0; dir < kDirectionCount; dir++) { const LinkState *linkState = getConnectionState(pt, static_cast(dir)); if (linkState && (*linkState) == kLinkStateConnected) { Common::Point connectedPoint = getConnectedPoint(pt, static_cast(dir)); if (!visitedSet.get(connectedPoint)) { pointsList[listSize] = connectedPoint; listSize++; } } } } } void CircuitPuzzle::validateCoord(const Common::Point &coord) { assert(coord.x >= 0 && coord.y >= 0 && coord.x < static_cast(kBoardWidth) && coord.y < static_cast(kBoardHeight)); } } // End of namespace VCruise