Materials available at: https://forejune.co/cuda/
Nim Game
Example:
Players A, B
Initial n = 5
A removes 2 block, remaining n = 3
B removes 1 block, remaining n = 2
A removes 1 block, remaining n = 1
B removes the last block, so B lost and A won
Initial n = 6
|
A B A B A B |
// minimaxnim.cpp
// nim game using minimax algorithm
// without alpha-beta pruning
// https://forejune.co/cuda/
#include <iostream>
#include <math.h>
using namespace std;
enum Player {MAX, MIN};
struct Node {
int score;
int move;
};
class MiniMaxNim{
private:
Player ai, opp;
public:
int n; //number of objects in pile
Player turn; //which player's turn
MiniMaxNim(int n0, Player ai0, Player opp0)
{
ai = ai0;
opp = opp0;
if ( n0 > 1 && n0 < 100)
n = n0;
else
n = 1;
}
int checkWinner()
{
if ( n == 1 ){
if (turn == opp)
return 1; //ai wins
else
return 2; //opp wins
}
return -1; //not determined
}
bool lost(Player player)
{
if (turn == player && n == 1)
return true;
return false;
}
bool remove(int k)
{
if ( k < 1 || k > n / 2 )
return false; //fail
n = n - k;
return true;
}
// evaluate score at a leaf
int evaluation(int state)
{
if ( state == 1 )
return 1;
else
return -1;
return 0;
}
Node minimax(bool max, int depth)
{
int state = 64;
if ( lost( opp ) )
state = 1;
else if ( lost( ai ) )
state = -1;
if ( state != 64 ) { //end game
int score = evaluation(state);
Node node = {score, -1}; //-1 signifies a leaf
return node;
}
Node v;
v.move = -1;
Player currentTurn = turn;
if ( max ) { //maximize score
v.score = -64;
int m = n / 2;
for (int i = 1; i <= m; i++) {
n = n - i; //tentative move
turn = currentTurn == ai ? opp : ai; // change turn
Node maxNode = minimax(false, depth+1); //next call is min
n = n + i; //restore n
turn = currentTurn; //restore turn
int s = maxNode.score;
if ( s > v.score) {
v.score = s;
v.move = i;
}
} // for
} else { //minimize score
v.score = 64;
int m = n / 2;
for (int i = 1; i <= m; i++){
n = n - i;
turn = currentTurn == opp ? ai : opp; //switch turn
Node minNode = minimax(true, depth+1); //next call is max
n = n + i; //restore n
turn = currentTurn; //retore turn
int s = minNode.score;
if ( s < v.score) {
v.score = s;
v.move = i;
}
}
}
return v;
}
};
int main()
{
int n;
cout << "Enter number of blocks you want to start: ";
cin >> n;
Player ai = MAX, opp = MIN;
MiniMaxNim nim( n, ai, opp );
nim.turn = ai;
while ( true ) {
int status = nim.checkWinner();
if (status > 0) {
if (status == 1)
cout << "AI wins!" << endl;
else
cout << "You win!" << endl;
break;
}
int move;
if (nim.turn == ai){
cout << "Blocks remain: " << nim.n << endl;
move = nim.minimax(true, 0).move;
nim.remove( move );
cout << "AI takes " << move << endl;
nim.turn = opp;
} else {
for ( ; ; ){
cout << "Blocks remain: " << nim.n << endl;
cout << "Your move (1-" << nim.n/2 << "): ";
cin >> move;
if ( !nim.remove(move) ) {
cout << "Invalid move; try again: \n";
continue;
} else
break;
}
nim.turn = ai;
}
}
return 0;
}
|
Running time T(n) ~ T(n-1) + T(n-2) + ... + T(n-n/2)
Solving the recurrence equation:
The algorithm repeatedly solves the same subproblems many times.
Dynamic Programming
n 1 2 3 4 5 6 7 score: -1, 1, -1, 1, 1, 1, -1
// nimdp.cpp : nim game using dynamic programming
// https://forejune.co/cuda
#include <stdlib.h>
using namespace std;
int getScore(int n, int S[])
{
if (n == 1)
S[n] = -1; //got last block
if (S[n] != 0)
return S[n];
int m = n / 2; //floor of n/2.0
for (int i = 1; i <= m; i++) {
int nextN = n - i;
if (getScore(nextN, S) == -1) { //found a path the other player loses
S[n] = 1;
return S[n];
}
}
S[n] = -1;
return S[n];
}
//choose a winning move i if one exists, otherwise pick a random legal move
int bestMove(int n, int score[])
{
int m = n / 2;
if ( m < 1 )
m = 1;
for (int i = 1; i <= m; i++) {
if (getScore(n - i, score) == -1)
return i; //choose move so that the other player loses
}
return (rand() % m + 1); //no winning move; pick any random legal move
}
|
// testNimdp.cpp
// https://forejune.co/cuda
#include <stdlib.h>
#include <iostream>
using namespace std;
const int NMAX = 99; //maximum blocks allowed
int bestMove(int n, int score[]);
int main(void)
{
srand(time(0));
int n;
int score[NMAX+1] = {0};
do {
cout << "Enter number of blocks you want to start (2 to 99): ";
cin >> n;
} while (n < 2 || n > NMAX);
int n0 = n;
cout << "AI moves first. Starting with n = " << n << endl;
int move;
while ( true ) {
// --- AI turn ---
if (n == 1) {
cout << "AI lost; you won!" << endl;
break;
}
move = bestMove(n, score);
n -= move;
cout << "AI takes " << move << ", " << n << " remain" << endl;
if (n == 1) {
cout << "You lost!" << endl;
break;
}
// Your turn
int m = n / 2;
for (; ;) {
cout << "Your turn. Enter number (1-" << m << "): ";
cin >> move;
if (move < 1 || move > m)
cout << "Invalid move; try again: ";
else
break;
}
n -= move;
cout << "You take " << move << ", " << n << " remain" << endl;
}
return 0;
}
|
Using a GUI
//testNimg.cpp
//https://forejune.co/cuda
#include <GL/glut.h>
#include <stdlib.h>
#include <iostream>
#include <math.h>
using namespace std;
const int NMAX = 99; //maximum blocks allowed
// choose a winning move k if one exists, otherwise pick a legal move
int bestMove(int n, int score[]);
int window;
int nBlocks = 0; //current number of blocks in pile
int takeN = 0; //number of blocks to be removed
int state = 0; //state of the game
char digits[4]; //holds digits the user enters
int nDigits = 0; //number of digits entered
int score[NMAX+1] = {0};//scores for AI
bool gameOver = false; //tells if game is finished
/* Initialize material property, light source, lighting model,
* and depth buffer.
*/
void init(void)
{
srand(time(0));
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_diffuse[] = { 1.0, 0.5, 0.5, 1.0 };
GLfloat mat_shininess[] = { 50.0 };
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
GLfloat light[] = { 1.0, 0.8, 0.8 };
GLfloat lmodel_ambient[] = { 0.8, 0.8, 0.8, 1.0 };
glClearColor (1.0, 1.0, 1.0, 0.0);
glShadeModel (GL_SMOOTH);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glLightfv(GL_LIGHT0, GL_DIFFUSE, light );
glLightfv(GL_LIGHT0, GL_SPECULAR, light );
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
glMatrixMode (GL_PROJECTION);
glLoadIdentity();
glOrtho (-10, 10, -10, 10, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
}
void message ( char *s )
{
char *p;
for ( p = s; *p; p++ )
glutBitmapCharacter (GLUT_BITMAP_TIMES_ROMAN_24, (int) *p);
}
void resetDigits()
{
nDigits = 0;
for (int i = 0; i < 4; i++)
digits[i] = 0;
}
void display(void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
char s[100];
// display the blocks
if ( nBlocks > 0 && nBlocks <= NMAX ) {
float dx = 1.2; //displacement in x-direction
float dy = 1.2; //displacement in y-direction
int nRows = ceil(nBlocks/10.0);
int k = 0;
for (int i = 0; i < nRows; i++) {
glLoadIdentity();
glTranslatef ( -8, 8-i*dy, 0 );
for (int j = 0; j < 10; j++ ){
if ( j == 5 ) glTranslatef(dx, 0, 0);
glTranslatef ( dx, 0, 0 );
glutSolidSphere (0.5, 32, 48);
k++;
if ( k >= nBlocks ) break;
}
}
}
//display messages
glLoadIdentity();
glRasterPos3f(-8, -7, 0);
if ( state == 0 ) { //initial state of game
char *s = (char *) "Enter initial blocks (2-99):";
message( s );
if ( nDigits > 0 )
message( digits );
} else if ( state == 1 ) { //game ready for play
sprintf(s, "%s %d", "Remaining number of blocks: ", nBlocks);
message( s );
glRasterPos3f(-8, -8, 0);
sprintf(s, "%s", "Press 'p' to play; AI moves first!");
message( s );
} else if (state == 2){ //AI
sprintf(s, "%s %d", "You took ", takeN);
message( s );
glRasterPos3f(-8, -8, 0);
sprintf(s, "%s %d", "Remaining number of blocks: ", nBlocks);
message( s );
glRasterPos3f(-8, -9, 0);
if (nBlocks > 1) {
sprintf(s, "%s", "Press 'a' for AI move!");
message( s );
resetDigits();
} else {
sprintf(s, "%s", "You have won! Press 'r' to restart!");
message( s );
gameOver = true;
}
} else if ( state == 3 ){ //Your move
sprintf(s, "%s %d", "AI took ", takeN);
message( s );
glRasterPos3f(-8, -8, 0);
sprintf(s, "%s %d", "Remaining number of blocks: ", nBlocks);
message( s );
glRasterPos3f(-8, -9, 0);
if (nBlocks > 1) {
sprintf(s, "%s %d %s", "Your move (1-", nBlocks/2, "):");
message( s );
message( digits );
} else {
sprintf(s, "%s", "AI has won! Press 'r' to restart!");
message ( s );
gameOver = true;
}
}
glFlush ();
}
void play()
{
takeN = bestMove(nBlocks, score);
nBlocks -= takeN;
state = 3; // now your turn (AI moves first)
resetDigits();
glutPostRedisplay();
}
void reset() //reset the game
{
nBlocks = 0;
state = 0;
resetDigits();
gameOver = false;
glutPostRedisplay();
}
int digitsValue()
{
int d = 0;
for (int i = 0; i < nDigits; i++) {
d *= 10;
d += (digits[i] - '0');
}
return d;
}
void keyboard(unsigned char key, int x, int y)
{
if (key == 27) { //escape key
glutDestroyWindow(window);
exit(0);
}
if ( key == 'r' ) {
reset();
return;
}
if ( gameOver )
return;
if ( key >= '0' && key <= '9' ) {
if ( state == 0 || state == 3 ) {
if ( nDigits < 2) {
digits[nDigits++] = key;
digits[nDigits] = 0; //form null-terminated string
glutPostRedisplay();
}
}
return;
}
switch(key) {
case 'p': //play game
if (state < 2 )
play();
break;
case '\r':
if ( nDigits == 0 ) break;
if (state == 0 ) {
nBlocks = digitsValue();
if (nBlocks < 2) break; //initial states
glutPostRedisplay();
state = 1;
} else if (state == 3) { //Your turn
takeN = digitsValue();
if ( takeN < 1 || takeN > nBlocks/2){
resetDigits();
glutPostRedisplay();
break;
}
nBlocks -= takeN;
digits[nDigits+1] = key;
glutPostRedisplay();
resetDigits();
state = 2; //next turn is AI
}
break;
case 'a':
if ( state == 2 ) { //AI turn
takeN = bestMove(nBlocks, score);
nBlocks -= takeN;
state = 3; //next is your move
glutPostRedisplay();
}
break;
}
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize (600, 600);
glutInitWindowPosition (100, 100);
window = glutCreateWindow (argv[0]);
init ();
glutDisplayFunc(display);
glutKeyboardFunc(keyboard);
glutMainLoop();
return 0;
}
|
|
|