GROOVIE: Othello Cursed Coins puzzle for Clandestiny

This commit is contained in:
Die4Ever 2021-11-13 19:37:19 -06:00
parent e078df4125
commit 40b8acad47
No known key found for this signature in database
GPG Key ID: 7B8AAD15B16CE289
2 changed files with 681 additions and 6 deletions

View File

@ -26,16 +26,642 @@
*
*/
#include "groovie/groovie.h"
#include "groovie/logic/othello.h"
#include "groovie/groovie.h"
namespace Groovie {
OthelloGame::OthelloGame() : _random("OthelloGame") {
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 OthelloGame::run(byte *scriptVariables) {
// TODO
void sortPossibleMoves(Freeboard (&boards)[30], int numPossibleMoves) {
if (numPossibleMoves < 2)
return;
Common::sort(&boards[0], &boards[numPossibleMoves]);
}
} // End of Groovie namespace
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 being
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)
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::setClickable(Freeboard *nextBoard, Freeboard *currentBoard, byte *vars) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
byte b = _lookupPlayer[currentBoard->_boardstate[x][y]];
vars[xyToVar(x, y)] = b;
if (nextBoard->_boardstate[x][y] == b && b != 0) {
vars[xyToVar(x, y)] += 32;
}
}
}
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;
}
int OthelloGame::getAllPossibleMoves(Freeboard *freeboard, 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 (freeboard->_boardstate[moveSpot / 8][moveSpot % 8] == 0) {
int8 **lineSpot = *line;
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?
goto LAB_OUT;
} while (freeboard->_boardstate[*testSpot / 8][*testSpot % 8] != opponent);
// we found the opponent, skip to the first piece that doesn't belong to the opponent
for (; freeboard->_boardstate[*testSpot / 8][*testSpot % 8] == opponent; testSpot++) {}
// start over again if didn't find a piece of our own on the other side
} while (freeboard->_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(freeboard, moveSpot);
boards[numPossibleMoves]._score = scoreBoard(&boards[numPossibleMoves]);
numPossibleMoves++;
}
LAB_OUT:
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 score = aiRecurse(&possibleMoves[move], _depths[_counter], 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;
}
setClickable(&_board, &_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;
}
setClickable(&_board, &_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;
}
setClickable(&_board, &_board, vars);
}
void OthelloGame::op5(byte *vars) {
_counter = vars[2];
readBoardStateFromVars(vars);
initLines();
vars[4] = 1;
}
OthelloGame::OthelloGame()
: _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
test();
#endif
}
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

@ -35,15 +35,64 @@
namespace Groovie {
/*
* Othello/Reversi puzzle in Clandestiny and UHP.
* 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();
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 setClickable(Freeboard *nextBoard, Freeboard *currentBoard, byte *vars);
void readBoardStateFromVars(byte *vars);
Freeboard getPossibleMove(Freeboard *freeboard, int moveSpot);
int getAllPossibleMoves(Freeboard *freeboard, 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;
};
} // End of Groovie namespace