Skip to content

Instantly share code, notes, and snippets.

@gulrak
Created July 6, 2022 15:34
Show Gist options
  • Save gulrak/7d9d76ea32b8ef0514983f85ccf13601 to your computer and use it in GitHub Desktop.
Save gulrak/7d9d76ea32b8ef0514983f85ccf13601 to your computer and use it in GitHub Desktop.
A self-playing Tic-Tac-Toe example for ray lib in C
//-----------------------------------------------------------------------------
// 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