Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Whether you're a newbie or an experienced programmer, any questions, help, or just talk of any language will be welcomed here.

Moderator: Coders of Rage

Post Reply
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Post by MarauderIIC »

I know Marcel was curious, so ---

Code to solve a http://en.wikipedia.org/wiki/15-puzzle (specific assignment (pdf)) using informed search techniques.

Easy explanation:
Solves puzzle by looking at current state's neighbors, picking the best (aka closest to solution, ranked by a particular function [total manhattan distance of the puzzle or # of tiles misplaced in the puzzle]) of the set of {current state's neighbors and the neighbors of other states we looked at previously} and if goal stop otherwise repeat. Thought it was neat.

Algorithms implemented:
http://en.wikipedia.org/wiki/A*
http://en.wikipedia.org/wiki/Best-first_search

Employing, for heuristics: http://en.wikipedia.org/wiki/Heuristics ... er_science [weights to determine what direction to try]
http://en.wikipedia.org/wiki/Manhattan_distance
# of misplaced tiles

Maybe more detail later. I haven't slept yet, and it's 730 AM. I get this way when I am working on something (and making progress).
Probably like 12 hours of work here (had to do a bit of finangling to figure out how the heap worked, wasn't sure if the answers all over the board was right, but I can't find any more errors). That and as you can see my operator== has some funky syntax, that's cause I was using pointer containers and I used STL's find() =p.

Would have (should have) used a priority_queue except I can't iterate through it to see if I've already added something. The vectors as heaps should act as a PQ.
http://www.sgi.com/tech/stl/make_heap.html
http://en.wikipedia.org/wiki/Heapsort
http://www.sgi.com/tech/stl/priority_queue.html
http://www.sgi.com/tech/stl/Vector.html

graph.h

Code: Select all

#ifndef GRAPH_H
#define GRAPH_H

#include "puzzle.h"
#include <vector>

using namespace std;

class Graph {
public:

	//Employed using push_heap, pop_heap so they act as a PQ.
	//STL priority_queue does not allow iterating through elements, which A* requires.
	vector<PuzzleState*> openNodes;
	vector<PuzzleState*> closedNodes;
	bool Open(PuzzleState& p);
	bool Close(PuzzleState* p);
	PuzzleState* top();
	Graph();
	~Graph();
};

#endif
graph.cpp

Code: Select all

#include "puzzle.h"
#include "graph.h"
#include <vector>
#include <algorithm>

using namespace std;

/***Graph::Open(PuzzleState* p)***
If p is not in the 'open' list, it is added and true is returned.
Otherwise, if it is found, parent is set to p's parent if necessary,
	depth is fixed, and false is returned.
****************************/
bool Graph::Open(PuzzleState& p) {

	vector<PuzzleState*>::iterator found;

	//Loop prevention
	//(Generating its set of neighbors that are not on C)

	found = find(closedNodes.begin(), closedNodes.end(), p);
	if (found != closedNodes.end()) {
		delete &p;
		return false;
	}

	//For each element that IS on O, if the path through x to N is
	//better than the currently known path, update g(x) := g(n) + d(n, x)
	//and associate the pointer to N with X, update f^(x) = g(x) + h(x)
	found = find(openNodes.begin(), openNodes.end(), p);
	if (found != openNodes.end()) {
		if ((*found)->getDepth() > p.getDepth()) {
			(*found)->setParent(p.getParent());
			(*found)->setDepth(p.getDepth());
			(*found)->CalculateHeuristic();
		}
		delete (&p);
		//Reorder O according to updated values of f^
		make_heap(openNodes.begin(), openNodes.end(), PSGreater);
		return false;
	}

	//For each node on M that is not on O, establish a pointer to N (constructor),
	//set g(x) := g(n) + d(n,x) [constructor],
	//set f^(x) = g(x) + h(x) [calculateHeuristic was called on generation (moveBlank)]
	//Add our new one (or replace the old one)
	openNodes.push_back(&p);
	//Reorder O according to updated values of f^
	push_heap(openNodes.begin(), openNodes.end(), PSGreater);
	return true;
}

/***Graph::Close(PuzzleState* p)***
If p is not in the 'closed' list, it is added and true is returned.
Otherwise, false is returned.
*****************************/
bool Graph::Close(PuzzleState* p) {

	vector<PuzzleState*>::iterator found;

	pop_heap(openNodes.begin(), openNodes.end(), PSGreater);
	found = find(openNodes.begin(), openNodes.end(), p);
	openNodes.erase(found);
	//make_heap(openNodes.begin(), openNodes.end(), PSGreater);

	if (find(closedNodes.begin(), closedNodes.end(), p) != closedNodes.end())
		return false;

	closedNodes.push_back(p);
	push_heap(closedNodes.begin(), closedNodes.end(), PSGreater);
	return true;
}

//Returns top of openNodes (lowest f^ value)
PuzzleState* Graph::top() {
	return *(openNodes.begin());
}

Graph::Graph() {
	//Don't think this is necessary, but...
	make_heap(openNodes.begin(), openNodes.end(), PSGreater);
	make_heap(closedNodes.begin(), closedNodes.end(), PSGreater);
}

//Deallocates everything
Graph::~Graph() {
	for (vector<PuzzleState*>::iterator i = openNodes.begin();i != openNodes.end();++i) {
		delete (*i);
		*i = NULL;
	}

	for (vector<PuzzleState*>::iterator i = closedNodes.begin();i != closedNodes.end();++i) {
		delete (*i);
		*i = NULL;
	}
}
puzzlestate.h

Code: Select all

#ifndef PUZZLE_H
#define PUZZLE_H

#include <string>
using namespace std;

enum DIRECTION { NODIR = -1, UP, DOWN, LEFT, RIGHT, MAXDIR };

DIRECTION& operator++(DIRECTION& dir);

class Piece {
public:
	int number;
	Piece* neighbors[4];
	Piece();
};

class PuzzleState {
	const static int BOARD_WIDTH = 4;
	const static int BOARD_HEIGHT = 4;

	Piece board[BOARD_WIDTH][BOARD_HEIGHT];	//PuzzleState board, 0 == blank

	int blankX, blankY;		//Current coordinates of the blank tile
	int heuristic;
	int depth;
	PuzzleState* parent;

	void Swap(int x, int y);
	int CalculateManhattan();
	int CalculateMisplaced();

public:
	bool useManhattan;	//Determines what fn CalculateHeuristic() uses
						//[ CalculateManhattan() vs CalculateMisplaced() ]
	bool useAStar;		//If false, then depth is ignored, making this into Best-First

	PuzzleState();
	PuzzleState(const PuzzleState& rhs);

	bool MoveBlank(DIRECTION dir);
	void Scramble(int moves);
	int CalculateHeuristic();
	string generateOutput();

	//Accessors -----------
	void setParent(PuzzleState* pparent)	{ parent = pparent; }
	void setDepth(int pdepth)				{ depth = pdepth; }

	PuzzleState* getParent() const		{ return parent; }
	int getDepth() const				{ return depth; }
	int getHeuristic() const			{ return heuristic; }
	const int getBoardHeight() const	{ return BOARD_HEIGHT; }
	const int getBoardWidth() const		{ return BOARD_WIDTH; }

	friend bool operator==(const PuzzleState* lhs, const PuzzleState& rhs);
};

bool PSGreater(const PuzzleState* lhs, const PuzzleState* rhs);

#endif
puzzlestate.cpp

Code: Select all


#include "puzzle.h"
#include <cstdlib>			//rand, abs
#include <ctime>			//time for rand
#include <string>			//for puzzlestate::generateoutput
#include <sstream>			//for puzzlestate::generateoutput

#include "graph.h"			//for Scramble()...
#include <vector>			//for Scramble()...
#include <algorithm>		//for Scramble()...
#include <iostream>

using namespace std;

DIRECTION& operator++(DIRECTION& dir) {
	return dir = (dir == MAXDIR) ? UP : DIRECTION(dir + 1);
}

/***Piece() [constructor]***
Sets piece to a blank
***************************/
Piece::Piece() {
	number = 0;
}

/***PuzzleState() [constructor]***
Initializes PuzzleState board to ascending numerical order, blank tile at
	bottom-right. Initializes relevant variables.
****************************/
PuzzleState::PuzzleState() {

	Piece curPiece;
	curPiece.number = 1;

	for (int y = 0; y < BOARD_HEIGHT;y++) {
		for (int x = 0; x < BOARD_WIDTH;x++) {
			if (curPiece.number >= BOARD_HEIGHT * BOARD_WIDTH)
				curPiece.number = 0;
			board[x][y] = curPiece;
			curPiece.number++;
		}
	}

	parent = NULL;
	blankX = BOARD_WIDTH-1;
	blankY = BOARD_HEIGHT-1;
	depth = 0;
	useManhattan = true;
	useAStar = true;
}

/***PuzzleState(const PuzzleState& rhs)***
Copies all members from rhs to *this
*****************************************/
PuzzleState::PuzzleState(const PuzzleState& rhs) {
	for (int curY = 0; curY < BOARD_HEIGHT;curY++)
		for (int curX = 0; curX < BOARD_WIDTH;curX++)
			board[curX][curY].number = rhs.board[curX][curY].number;
	//Establish a pointer to N,
	//set g(x) := g(n) + d(n,x),
	//set f^(x) = g(x) + h(x)
	blankX = rhs.blankX;
	blankY = rhs.blankY;

	useAStar = rhs.useAStar;
	useManhattan = rhs.useManhattan;

	if (useAStar)
		depth = rhs.depth+1;				//g(x) := g(n) + d(n,x),
	else
		depth = 0;

	parent = const_cast<PuzzleState*>(&rhs);
	heuristic = rhs.heuristic;

	CalculateHeuristic();	//f^(x) = g(x) + h(x), although a Move should be called or we have a duplicate, deeper state.
}

bool operator==(const PuzzleState* lhs, const PuzzleState& rhs) {
	for (int curY = 0; curY < lhs->BOARD_HEIGHT; curY++) {
		for (int curX = 0; curX < lhs->BOARD_WIDTH; curX++)
			if (lhs->board[curX][curY].number != rhs.board[curX][curY].number)
				return false;
	}
	return true;
}

bool PSGreater(const PuzzleState* lhs, const PuzzleState* rhs) {
	return lhs->getHeuristic() > rhs->getHeuristic();
};

/***Swap(int x, int y)***
 Swaps board[x][y] & board[blankX][blankY] whether the move is legal or not
************************/
void PuzzleState::Swap(int x, int y) {
	Piece blank;

	board[blankX][blankY] = board[x][y];
	board[x][y] = blank;
	blankX = x;
	blankY = y;
	CalculateHeuristic();
}

/***MoveBlank(DIRECTION dir)***
 Swaps blank tile with the tile to its DIRECTION
 Returns true on success.
******************************/
bool PuzzleState::MoveBlank(DIRECTION dir) {
	if (dir == UP && blankY == 0)
		return false;
	if (dir == DOWN && blankY == BOARD_HEIGHT-1)
		return false;
	if (dir == LEFT && blankX == 0)
		return false;
	if (dir == RIGHT && blankX == BOARD_WIDTH-1)
		return false;

	int x = blankX;
	int y = blankY;

	if (dir == RIGHT)
		x++;
	else if (dir == LEFT)
		x--;
	else if (dir == UP)
		y--;
	else if (dir == DOWN)
		y++;
	else //NODIR
		return false;

	Swap(x, y);
	return true;
}

/***isAdjacent(int x, int y)***
 Returns direction to blank tile if blank is adjacent to board[x][y]
***************************/
/*DIRECTION PuzzleState::isAdjacent(int x, int y) {

	for (int direction = 0;direction < (int)MAXDIR;direction++) {
		if (board[x][y].neighbors[direction]->number == 0)
			return (DIRECTION)direction;
	}
	return NODIR;
}*/

/***Scramble(int moves)***
 Scrambles the PuzzleState by randomly moving the blank tile 'moves' spaces.
 Records this amount in PuzzleState::scrambleMoves
 Returns scrambleMoves.
*************************/
void PuzzleState::Scramble(int moves) {

	int scrambleMoves = 0;
	//Keep from getting same random # more than once
	srand((unsigned int)time(NULL));
	unsigned int lastRand = rand();

	PuzzleState* test;
	vector<PuzzleState*> states;

	test = new PuzzleState(*this);				//Initialize our test space
	states.push_back(new PuzzleState(*this));	//Push a copy of the start state
	cout << this->generateOutput();

	while (scrambleMoves < moves) {
		srand(lastRand + (unsigned int)time(NULL));
		lastRand = rand();
		//Don't move if the movement is already on the state list
		//In other words, make sure the movments are contiguous.

		//Guarantee that the move is legal
		if (test->MoveBlank((DIRECTION)(lastRand % 4))) {
			//Guarantee nonrepeating scramble
			if (find(states.begin(), states.end(), *test) == states.end()) {
				//Now that we have a move that wasn't on the state list, do the move
				MoveBlank((DIRECTION)(lastRand % 4));
				scrambleMoves++;
				states.push_back(new PuzzleState(*this));		//Push a copy onto the state listing
				cout << this->generateOutput();
			}
		}
		//Reset test state to either 1) the not moved version or 2) the moved version of 'this'
		delete test;
		test = new PuzzleState(*this);
	}
	delete test;
	test = NULL;
	cout << "--------------" << endl;
	for (vector<PuzzleState*>::iterator i = states.begin(); i != states.end(); ++i) {
		delete (*i);
		*i = NULL;
	}
}

int PuzzleState::CalculateManhattan() {
	// 1  2  3  4 % 4: 1 2 3 0, -1: 0 1 2 -1
	// 5  6  7  8 % 4: 1 2 3 0
	// 9 10 11 12 % 4
	//13 14 15 00

	int properx, propery;
	int xdist, ydist;

	heuristic = 0;
	for (int y = 0;y < BOARD_HEIGHT;y++) {
		for (int x = 0;x < BOARD_WIDTH;x++) {
			properx = (board[x][y].number % BOARD_WIDTH)-1;
			if (properx == -1)
				properx = BOARD_WIDTH-1;
			xdist = abs(x-properx);

			if (board[x][y].number == 0)
				propery = BOARD_HEIGHT-1;
			else
				propery = int( (board[x][y].number-1) / BOARD_HEIGHT);
			ydist = abs(y-propery);

			heuristic += xdist + ydist;
		}
	}

	heuristic += depth;
	return heuristic;
}

int PuzzleState::CalculateMisplaced() {

	int curNumber = 1;
	int misplaced = 0;

	for (int curY = 0;curY < BOARD_HEIGHT;curY++) {
		for (int curX = 0;curX < BOARD_WIDTH;curX++) {
			if (board[curX][curY].number != curNumber)
				misplaced++;
			curNumber++;
			if (curNumber == 16)
				curNumber = 0;
		}
	}

	heuristic = misplaced + depth;
	return heuristic;
}

int PuzzleState::CalculateHeuristic() {
	if (useManhattan)
		return CalculateManhattan();
	else
		return CalculateMisplaced();
}

string PuzzleState::generateOutput() {
	string retval;
	for (int y = 0; y < BOARD_HEIGHT;y++) {
		for (int x = 0; x < BOARD_WIDTH;x++) {
			stringstream conv;
			if (board[x][y].number < 10)
				conv << " ";
			conv << board[x][y].number;
			retval += conv.str() + " ";
			conv.clear();
		}
		retval += "\n";
	}
	retval += "---\n";
	return retval;
}
main.cpp

Code: Select all

#include "puzzle.h"
#include "graph.h"
#include <vector>
#include <algorithm>		//for make_heap, sort_heap, push_heap, pop_heap...
#include <iostream>
#include <string>

using namespace std;

int main() {

	//Create lists Open & Closed
	Graph g;

	PuzzleState* start = new PuzzleState();		//Gets scrambled later
	PuzzleState goal;						//Unmodified PuzzleState, used to test for goal state

	start->useAStar = true; //if false, best-first search is used
	start->useManhattan = true; //if false, misplaced tiles is used
	start->Scramble(5);//change this to change how many moves we scramble (you probably want to do <= 10)

	//Put initial node on O
	g.Open(*start);

	//O can't be empty. If it could be, we would test for it & exit w/ fail here

	PuzzleState* current;
	PuzzleState* neighbor;
	unsigned int examined = 0;

	while (!g.openNodes.empty()) {

		//Take 1st node, remove it from O, put it on C.
		current = g.top();
		g.Close(current);
		examined++;

		//If current is a goal, exit with success
		if (current == goal) {
			cout << "====" << endl;
			int steps = -1;
			string output = "";
			PuzzleState* state = current;
			while (state != NULL) {
				steps++;
				output = (state->generateOutput() + output);
				state = state->getParent();
			}
			cout << output << endl;
			cout << "Goal reached in " << steps << " moves." << endl;
			cout << "There were " << examined << " states examined." << endl;
			if (current->useAStar)
				cout << "And a maximum depth of " << current->getDepth() << endl;
#ifdef _WIN32
			system("pause");
#endif
			return 0;
		}

		//Expand N, generating the set of its neighbors that are not on C, call it M.
		//Add all neighbors to heap
		for (DIRECTION dir = DIRECTION(NODIR+1);dir < MAXDIR;++dir) {
			//For each node not in O,  establish a pointer to N, ...

			//Constructor: establish a pointer to N, g(x) := g(n) + d(n,x)
			neighbor = new PuzzleState(*current);
			if (neighbor->MoveBlank(dir))					//gen. child and f^(x) = g(x) + h(x)
				g.Open(*neighbor);
			else
				delete neighbor;
		}
	}

	cout << "No goal." << endl;
#ifdef _WIN32
	system("pause");
#endif
	return 1;
}
I won't paste my results and stuff, but here's the conclusion:
Conclusions:
A* with misplaced tiles seems to be the best search method out of these, but if one wants to use best-first, Manhattan distance should be used to make it usable. Apparently, adding depth considerations to total Manhattan distance, as A* does, stops its effectiveness in general. Best-first using misplaced tiles is essentially useless, as it has a good chance of examining an unnecessarily large number of states, making it prohibitive on both time and memory constraints, and while best-first with Manhattan distance makes best-first usable, it is still not as consistently good as A* is.

-Alex (signing this in case my prof does a google search and my own post shows up ;) )
Last edited by MarauderIIC on Sat Oct 18, 2008 10:54 pm, edited 4 times in total.
Reason: final code version, i think
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Re: Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Post by MarauderIIC »

I had forgotten to post main and made some edits.
And some of the implementation is wrong anyways. will fix
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Re: Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Post by MarauderIIC »

Final code version edited in, I think.
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
User avatar
M_D_K
Chaos Rift Demigod
Chaos Rift Demigod
Posts: 1087
Joined: Tue Oct 28, 2008 10:33 am
Favorite Gaming Platforms: PC
Programming Language of Choice: C/++
Location: UK

Re: Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Post by M_D_K »

Wow! Thats twice now that I've impressed by your code. Bookmarked for later(it's really late right now or early depends if you've been to bed)
Gyro Sheen wrote:you pour their inventory onto my life
IRC wrote: <sparda> The routine had a stack overflow, sorry.
<sparda> Apparently the stack was full of shit.
User avatar
MarauderIIC
Respected Programmer
Respected Programmer
Posts: 3406
Joined: Sat Jul 10, 2004 3:05 pm
Location: Maryland, USA

Re: Solving 15-puzzle : (Intro to AI [CS463] : HW4)

Post by MarauderIIC »

Glad I could impress =) Thanks for the comment.
I realized the moment I fell into the fissure that the book would not be destroyed as I had planned.
Post Reply