Treating this as a graph searching problem, I have solved the 4x4 board (with 5 tile types) using uninformed breadth first search. When I try to upgrade to the real case, a 5x5 board with 6 tile types, the problem is too large to solve (4x4 case has up to 5^16 states with branching factor 16, the 5x5 case has up to 6^25 states with branching factor 20). I would like to try an informed heuristic search method like greedy search or A*, but it is not clear to me how the heuristic should work - it is tough to come up with a heuristic that guides the search towards full clears _without_ guiding it towards single row/column clears too.
I would like opinions on writing a good heuristic for this goal.
Below is a C code for the 4x4 case, the eventual target platform is flash / actionscript 3. I need quite a bit of speedup before this opponent will be able to function in real time - it might not be possible to do it using searching algorithms (maybe there is a clever state machine that can construct the board from the bottom up?).
Thanks for the help!
#include <iostream>
#include <queue>
#include <map>
#include <string>
#include <vector>
#include <cstdlib>
#include <time.h>
using namespace std;
#define NX 4
#define NY 4
#define NT 5
class GameBoard
{
public:
GameBoard();
int tiles[NX][NY];
// Make this GameBoard into a string.
string toString();
// Fill with random data.
void initRandom();
void initString (string s);
void cycleColumn (int direction, int x);
void cycleRow (int direction, int y);
void print ();
int detectWin();
int detectX (int shrink_x, int shrink_y);
int detectY (int shrink_x, int shrink_y);
int detectFullClear();
void printFullClear();
};
GameBoard::GameBoard()
{
}
void GameBoard::initRandom()
{
for (int ix = 0; ix < NX; ix++)
for (int iy = 0; iy < NY; iy++)
tiles[ix][iy] = rand() % NT;
}
string GameBoard::toString()
{
string s = "";
for (int iy = 0; iy < NY; iy++)
for (int ix = 0; ix < NX; ix++)
s += ( tiles[ix][iy] + '0' );
return s;
}
void GameBoard::initString(string s)
{
int used = 0;
for (int iy = 0; iy < NY; iy++)
for (int ix = 0; ix < NX; ix++)
tiles[ix][iy] = ( s[used++]-'0' );
}
void GameBoard::cycleColumn (int x, int direction)
{
int temp;
switch (direction)
{
case 1:
// Cycle positive.
temp = tiles[x][NY-1];
for (int iy = NY-1; iy > 0; iy--)
tiles[x][iy] = tiles[x][iy-1];
tiles[x][0] = temp;
break;
case -1:
// Cycle negative.
temp = tiles[x][0];
for (int iy = 0; iy < NY-1; iy++)
tiles[x][iy] = tiles[x][iy+1];
tiles[x][NY-1] = temp;
break;
}
}
void GameBoard::cycleRow (int y, int direction)
{
int temp;
switch (direction)
{
case 1:
// Cycle positive.
temp = tiles[NX-1][y];
for (int ix = NX-1; ix > 0; ix--)
tiles[ix][y] = tiles[ix-1][y];
tiles[0][y] = temp;
break;
case -1:
// Cycle negative.
temp = tiles[0][y];
for (int ix = 0; ix < NX-1; ix++)
tiles[ix][y] = tiles[ix+1][y];
tiles[NX-1][y] = temp;
break;
}
}
void GameBoard::print ()
{
for (int iy = 0; iy < NY; iy++)
{
for (int ix =0; ix < NX; ix++)
cout << tiles[ix][iy];
cout << endl;
}
}
int GameBoard::detectWin ()
{
int ans = 0;
// Detect along x.
for (int iy = 0; iy < NY; iy++)
{
int thisRowMatches = 1;
int detecttype = tiles[0][iy];
for (int ix = 1; ix < NX; ix++)
if (tiles[ix][iy] != detecttype)
thisRowMatches = 0;
if (thisRowMatches)
ans = 1;
}
// Detect along y.
for (int ix = 0; ix < NX; ix++)
{
int thisColMatches = 1;
int detecttype = tiles[ix][0];
for (int iy = 1; iy < NY; iy++)
if (tiles[ix][iy] != detecttype)
thisColMatches = 0;
if (thisColMatches)
ans = 1;
}
return ans;
}
int GameBoard::detectX (int shrink_x, int shrink_y)
{
// Try to clear along x.
int iy = 0;
int matchedY = -1;
while (iy < shrink_y && (matchedY == -1) )
{
int thisRowMatches = 1;
int detecttype = tiles[0][iy];
for (int ix = 1; ix < shrink_x; ix++)
if (tiles[ix][iy] != detecttype)
thisRowMatches = 0;
if (thisRowMatches)
matchedY = iy;
else
iy++;
}
return matchedY;
}
int GameBoard::detectY (int shrink_x, int shrink_y)
{
// Try to clear along x.
int ix = 0;
int matchedX = -1;
while (ix < shrink_x && (matchedX == -1) )
{
int thisColMatches = 1;
int detecttype = tiles[ix][0];
for (int iy = 1; iy < shrink_y; iy++)
if (tiles[ix][iy] != detecttype)
thisColMatches = 0;
if (thisColMatches)
matchedX = ix;
else
ix++;
}
return matchedX;
}
void GameBoard::printFullClear()
{
int shrink_x = NX;
int shrink_y = NY;
int could_clear_something = 1;
while (could_clear_something && (shrink_x > 0) && (shrink_y > 0) )
{
// Print the partial board.
for (int iy = 0; iy < shrink_y; iy++)
{
for (int ix = 0; ix < shrink_x; ix++)
cout << tiles[ix][iy];
cout << endl;
}
cout << endl;
could_clear_something = 0;
int matchedY = detectX(shrink_x, shrink_y);
if (matchedY != -1)
{
could_clear_something = 1;
// Copy up from matchedY down..
for (int ix = 0; ix < shrink_x; ix++)
for (int iy = matchedY; iy < shrink_y-1; iy++)
tiles[ix][iy] = tiles[ix][iy+1];
shrink_y --;
}
else
{
int matchedX = detectY(shrink_x, shrink_y);
if (matchedX != -1)
{
could_clear_something = 1;
// Copy left from matchedX right...
for (int iy = 0; iy < shrink_y; iy++)
for (int ix = matchedX; ix < shrink_x-1; ix++)
tiles[ix][iy] = tiles[ix+1][iy];
shrink_x --;
}
}
}
}
int GameBoard::detectFullClear ()
{
int shrink_x = NX;
int shrink_y = NY;
int could_clear_something = 1;
while (could_clear_something && (shrink_x > 0) && (shrink_y > 0) )
{
/*
// Print the partial board.
for (int iy = 0; iy < shrink_y; iy++)
{
for (int ix = 0; ix < shrink_x; ix++)
cout << tiles[ix][iy];
cout << endl;
}
cout << endl;
*/
could_clear_something = 0;
int matchedY = detectX(shrink_x, shrink_y);
if (matchedY != -1)
{
could_clear_something = 1;
// Copy up from matchedY down..
for (int ix = 0; ix < shrink_x; ix++)
for (int iy = matchedY; iy < shrink_y-1; iy++)
tiles[ix][iy] = tiles[ix][iy+1];
shrink_y --;
}
else
{
int matchedX = detectY(shrink_x, shrink_y);
if (matchedX != -1)
{
could_clear_something = 1;
// Copy left from matchedX right...
for (int iy = 0; iy < shrink_y; iy++)
for (int ix = matchedX; ix < shrink_x-1; ix++)
tiles[ix][iy] = tiles[ix+1][iy];
shrink_x --;
}
}
}
if (shrink_x == 0 || shrink_y == 0)
return 1; // wow, full clear.
return 0;
}
struct Searchnode
{
string state;
Searchnode* predecessor;
};
int main ()
{
GameBoard g;
srand ( time(NULL) );
g.initRandom();
cout << "Initial map state." << endl;
g.print();
cout << endl;
// A queue to store search nodes.
deque <Searchnode* > expandQ;
// A map to record states we've already tried.
map <string, int> explored;
// A search node that we'll new, over and over.
Searchnode* s;
// A list of search nodes to delete.
vector<Searchnode*> garbageCollector;
// The winning game configuration.
Searchnode* winNode = NULL;
// Start searching. Origin node is the game state, as-is.
s = new Searchnode;
// Insertion procedure..
s->state = g.toString();
s->predecessor = NULL;
expandQ.push_back(s);
explored[s->state] = 1;
garbageCollector.push_back(s);
// Search loop.
while ( winNode == NULL && !expandQ.empty() )
{
// Pop the next node to expand.
s = expandQ.front();
expandQ.pop_front();
g.initString( s-> state );
// Does this node win?
if (g.detectWin() )
{
if ( g.detectFullClear() )
winNode = s;
}
else
{
// Push all unexplored neighbor states of this node.
// Cycle the rows +
for (int iy = 0; iy < NY; iy++)
{
g.cycleRow(iy,1);
// Is this state unexplored?
if (explored.find( g.toString() ) == explored.end() )
{
// Add it to the explored map, and enqueue it.
Searchnode* s_next = new Searchnode;
s_next->state = g.toString();
s_next->predecessor = s;
expandQ.push_back(s_next);
explored[ s_next -> state ] = 1;
// Garbage collect this node later.
garbageCollector.push_back(s_next);
}
g.cycleRow(iy,-1);
}
// Cycle the rows -
for (int iy = 0; iy < NY; iy++)
{
g.cycleRow(iy,-1);
// Is this state unexplored?
if (explored.find( g.toString() ) == explored.end() )
{
// Add it to the explored map, and enqueue it.
Searchnode* s_next = new Searchnode;
s_next->state = g.toString();
s_next->predecessor = s;
expandQ.push_back(s_next);
explored[ s_next -> state ] = 1;
// Garbage collect this node later.
garbageCollector.push_back(s_next);
}
g.cycleRow(iy,1);
}
// Cycle the cols +
for (int ix = 0; ix < NX; ix++)
{
g.cycleColumn(ix,1);
// Is this state unexplored?
if (explored.find( g.toString() ) == explored.end() )
{
// Add it to the explored map, and enqueue it.
Searchnode* s_next = new Searchnode;
s_next->state = g.toString();
s_next->predecessor = s;
expandQ.push_back(s_next);
explored[ s_next -> state ] = 1;
// Garbage collect this node later.
garbageCollector.push_back(s_next);
}
g.cycleColumn(ix,-1);
}
// Cycle the cols -
for (int ix = 0; ix < NX; ix++)
{
g.cycleColumn(ix,-1);
// Is this state unexplored?
if (explored.find( g.toString() ) == explored.end() )
{
// Add it to the explored map, and enqueue it.
Searchnode* s_next = new Searchnode;
s_next->state = g.toString();
s_next->predecessor = s;
expandQ.push_back(s_next);
explored[ s_next -> state ] = 1;
// Garbage collect this node later.
garbageCollector.push_back(s_next);
}
g.cycleColumn(ix,1);
}
}
}
if (winNode == NULL)
cout << "Full clear could not be found." << endl;
else
{
cout << "Full clear animation:" << endl;
g.initString( winNode -> state );
g.printFullClear ();
// Count back towards the goal.
int pathlength = -1;
cout << "Reverse path:" << endl;
while (winNode != NULL)
{
g.initString( winNode -> state );
g.print();
cout << endl;
winNode = winNode -> predecessor;
pathlength++;
}
cout << "Path length: " << pathlength << " moves. " << endl;
}
cout << "Searched " << garbageCollector.size() << " different states." << endl;
// Delete all the searchnodes we new'd.
for (unsigned int i = 0; i < garbageCollector.size(); i++)
delete garbageCollector;
return 0;
}