Created
July 6, 2022 15:34
-
-
Save gulrak/7d9d76ea32b8ef0514983f85ccf13601 to your computer and use it in GitHub Desktop.
A self-playing Tic-Tac-Toe example for ray lib in C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//----------------------------------------------------------------------------- | |
// Self playing Tic-Tac-Toe example with raylib :-) | |
//----------------------------------------------------------------------------- | |
// LICENSE: zlib/libpng | |
// | |
// Copyright (c) 2022 Steffen Schümann (@gulrak) | |
// | |
// This software is provided "as-is", without any express or implied | |
// warranty. In no event will the authors be held liable for any damages | |
// arising from the use of this software. | |
// | |
// Permission is granted to anyone to use this software for any purpose, | |
// including commercial applications, and to alter it and redistribute it | |
// freely, subject to the following restrictions: | |
// | |
// 1. The origin of this software must not be misrepresented; you must not | |
// claim that you wrote the original software. If you use this software in | |
// a product, an acknowledgment in the product documentation would be | |
// appreciated but is not required. | |
// | |
// 2. Altered source versions must be plainly marked as such, and must not | |
// be misrepresented as being the original software. | |
// | |
// 3. This notice may not be removed or altered from any source | |
// distribution. | |
//----------------------------------------------------------------------------- | |
// This implementation uses a Bitboard to represent the whole game state in | |
// a uin32_t so no need of move/unmove, just use a new state in the recursion | |
// and be done with it. | |
// | |
// Gamestate: | |
// 10987654321098765432109876543210 | |
// ----------abcdOOOOOOOOOXXXXXXXXX | |
// X/O: the fields for player X and O, least significant | |
// a: Game over flag | |
// b: If game was one, by whom (0 = X, 1 = O) | |
// c: Was the game won? (0 = no, 1 = yes) | |
// d: WHo is on move? (0 = X, 1 = O) | |
// | |
// Use Cursor UP/DOWN to change delay. | |
//----------------------------------------------------------------------------- | |
#include "raylib.h" | |
#include "rlgl.h" | |
#include <stdint.h> | |
#define PLAYER_TO_MOVE (1<<18) | |
#define GAME_WON (1<<19) | |
#define GAME_WON_BY (1<<20) | |
#define GAME_OVER (1<<21) | |
#define GRID_SIZE 80 | |
#define LINE_WIDTH 8 | |
static const uint16_t winMasks[8] = { | |
7, 7<<3, 7<<6, 0x49, 0x49<<1, 0x49<<2, 0x54, 0x111 | |
}; | |
static uint32_t checkEnd(uint32_t state) | |
{ | |
if(state & GAME_OVER) return state; // state is already final | |
for(int i = 0; i < 8; ++i) { | |
if(state & PLAYER_TO_MOVE) { | |
if(((state>>9) & winMasks[i]) == winMasks[i]) | |
return state | GAME_WON | GAME_OVER | GAME_WON_BY; // O wins | |
} | |
else { | |
if((state & winMasks[i]) == winMasks[i]) | |
return state | GAME_WON | GAME_OVER; // X wins | |
} | |
} | |
if(((state | (state>>9)) & 0x1ff) == 0x1ff) state |= GAME_OVER; // draw | |
return state; | |
} | |
inline uint32_t generateMoves(uint32_t state) | |
{ | |
if(state & GAME_OVER) return 0; // state is already final | |
return ~(state | state>>9) & 0x1ff; // return bits for all free fields | |
} | |
inline uint32_t nextMove(uint32_t* moves) | |
{ | |
uint32_t result = *moves & -*moves; // get least significant bit set | |
*moves &= *moves -1; // and remove it from the moves | |
return result; | |
} | |
uint64_t nodesSearched = 0; | |
int negaMax(uint32_t state, int depth, uint32_t* moveOut) { | |
if (state & GAME_OVER) | |
return (state & GAME_WON) ? -1000 : 0; // we lost or it is a draw | |
int maxScore = -9999; | |
uint32_t moves = generateMoves(state); | |
while (moves) { | |
uint32_t move = nextMove(&moves); | |
uint32_t movedState = checkEnd(state | (state & PLAYER_TO_MOVE ? move << 9 : move)); // make move in into newState | |
int score = -negaMax(movedState ^ PLAYER_TO_MOVE, depth + 1, 0) + (depth == 1 ? GetRandomValue(0,10) : 0); // evaluate | |
if (score > maxScore) { | |
maxScore = score; // store better score | |
if(moveOut) *moveOut = move; // and possibly the move | |
} | |
} | |
++nodesSearched; | |
return maxScore; | |
} | |
int main(void) | |
{ | |
const int screenWidth = 640; | |
const int screenHeight = 480; | |
const int offsetX = (screenWidth - GRID_SIZE*3) / 2; | |
const int offsetY = (screenHeight - GRID_SIZE*3) / 2; | |
uint32_t state = 0; | |
float timer = 3; | |
double searchTime = 0; | |
int gamesPlayed = 0; | |
int delay = 20; | |
InitWindow(screenWidth, screenHeight, "Self playing TicTacToe by Steffen 'Gulrak' Schümann"); | |
while (!WindowShouldClose()) | |
{ | |
timer -= GetFrameTime(); | |
if(timer <= 0) { | |
if(state&GAME_OVER) { | |
state = 0; // last game is over start new | |
++gamesPlayed; | |
} | |
if(state == 0) | |
state |= 1<<GetRandomValue(0,8); // initial move is random | |
else { | |
double start = GetTime(); | |
uint32_t move = 0; | |
negaMax(state, 1, &move); // find best move | |
searchTime += GetTime() - start; | |
state |= (state & PLAYER_TO_MOVE ? move << 9 : move); // make move | |
} | |
state = checkEnd(state); // update final state information | |
if(!(state & GAME_OVER)) | |
state ^= PLAYER_TO_MOVE; // not done, next player | |
timer = state & GAME_OVER ? (float)delay/25.0f : (float)delay/50.0f; | |
} | |
if(IsKeyPressed(KEY_UP) && delay < 100) ++delay; | |
if(IsKeyPressed(KEY_DOWN) && delay > 0) --delay; | |
BeginDrawing(); | |
ClearBackground(RAYWHITE); | |
for(int i = 1; i < 3; ++i) { | |
DrawLineEx((Vector2){offsetX + GRID_SIZE*i, offsetY}, (Vector2){offsetX + GRID_SIZE*i, offsetY + GRID_SIZE*3}, LINE_WIDTH, BLACK); | |
DrawLineEx((Vector2){offsetX, offsetY + GRID_SIZE*i}, (Vector2){offsetX + GRID_SIZE*3, offsetY + GRID_SIZE*i}, LINE_WIDTH, BLACK); | |
} | |
for(int field = 0; field < 9; ++field) { | |
if(state & 1<<field) { | |
DrawLineEx((Vector2){offsetX + (field%3)*GRID_SIZE + LINE_WIDTH*2, offsetY + (field/3)*GRID_SIZE + LINE_WIDTH*2}, (Vector2){offsetX + (field%3)*GRID_SIZE + GRID_SIZE - LINE_WIDTH*2, offsetY + (field/3)*GRID_SIZE + GRID_SIZE - LINE_WIDTH*2}, LINE_WIDTH, RED); | |
DrawLineEx((Vector2){offsetX + (field%3)*GRID_SIZE + GRID_SIZE - LINE_WIDTH*2, offsetY + (field/3)*GRID_SIZE + LINE_WIDTH*2}, (Vector2){offsetX + (field%3)*GRID_SIZE + LINE_WIDTH*2, offsetY + (field/3)*GRID_SIZE + GRID_SIZE - LINE_WIDTH*2}, LINE_WIDTH, RED); | |
} | |
if(state & 1<<(field+9)) { | |
DrawCircle(offsetX + (field%3)*GRID_SIZE + GRID_SIZE/2, offsetY + (field/3)*GRID_SIZE + GRID_SIZE/2, GRID_SIZE/2 - LINE_WIDTH, BLUE); | |
DrawCircle(offsetX + (field%3)*GRID_SIZE + GRID_SIZE/2, offsetY + (field/3)*GRID_SIZE + GRID_SIZE/2, GRID_SIZE/2 - LINE_WIDTH*2, RAYWHITE); | |
} | |
} | |
if(state & GAME_OVER) { | |
DrawText("GAME OVER!", 225, 10, 30, BLACK); | |
if(state & GAME_WON) | |
DrawText(state & GAME_WON_BY ? "O WON!" : "X WON!", 265, 60, 30, state & GAME_WON_BY ? RED : BLUE); | |
else | |
DrawText(" DRAW", 265, 60, 30, BLACK); | |
} | |
DrawText(TextFormat("delay: %d", delay), GetScreenWidth() - 100, 5, 20, BLACK); | |
DrawText(TextFormat("node search rate: %.1f Mn/s", searchTime>0.0001 ? nodesSearched / searchTime / 1000000 : 0.0), 10, GetScreenHeight() - 25, 20, BLACK); | |
DrawText(TextFormat("games played: %d", gamesPlayed), 430, GetScreenHeight() - 25, 20, BLACK); | |
EndDrawing(); | |
} | |
CloseWindow(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment