Othello for DOORS

othellosmlOthello (aka Reversi) has fast become one of the most popular and most often played games in history, spawning contests and tournaments on regional, national and even worldwide levels. Othello is a game of skill and strategy, yet it only takes a minute to learn!

//  Othello for DOORS

/*
	About Othello.

	Although Othello can be played on any size board the standard game uses
	a board with 64 squares (8x8).

	The opening position has white discs at (3, 3) and (4, 4) and black
	discs at (3, 4) and (4, 3).

	Each othello disc is black on one side and white on the other.

	Black plays first and places a disc, black side uppermost, on any square
	on the board such that he "traps" at least one of his opponents pieces
	between the disc he has just placed and any other of his colour already
	on the board. Trapped discs are flipped to show the black side uppermost.

	Play continues alternately. First black, then white. If at any time a
	player does not have a legal move i.e. there is nowhere he can play that
	flips one of his opponent's discs, he must pass and his opponent plays again.
	It is possible to pass several times in succession. When neither player has a
	legal move (usually when the board is full but not always) the game ends.
	The winner is the player with the most pieces of his colour showing.

	TBD: 

	Display scores during game.

	Display flip count and rating on right-mouse click to assist
	learner players.

	Known issues:

	Can't seem to prevent extra mouse clicks being acted upon
	when it is not the user's turn.

	Changes:

    10-Jul-2007		Tony Goodman	First working version.
*/

DB  dbMain       = null
DBE dbeCanvas    = null
DBE dbeStart     = null

int	WHITE        = 0		// white disc
int	BLACK        = 1		// black disc
int	EMPTY        = 2		// empty square

int STATES       = 3        // square can be Black, White or empty

// colour for the discs and an empty square
int COLOURS[STATES]  = { realColor_White, realColor_Black, realColor_Sea_Green }

// used for debug
string STRINGS[STATES] = { "WHITE", "BLACK", "EMPTY" }

// Human plays Black, computer plays White. Black always goes first.
int turn = BLACK

// prevent spurious mouse clicks
bool mouseActive = true

// playing board dimensions
int GRID_SIZE    = 8        // number of squares across
int SQUARE_SIZE  = 50       // pixels

/************************************
	Data structures to hold state of play etc
************************************/
int BOARD_SIZE = GRID_SIZE * GRID_SIZE   // 8 * 8 = 64 squares

int playerArray[BOARD_SIZE]              // state of each square on the board
int valueArray[BOARD_SIZE + BOARD_SIZE]  // value of each square to the player
int flipsArray[BOARD_SIZE + BOARD_SIZE]  // flip count for each square to the player

int score[2] = { 0, 0 }                  // total score for each player

// forward declaration
void othello()

/************************************
	getPlayer
	Returns the state of the square at (x, y).
	This will be WHITE, BLACK or EMPTY.
************************************/
int getPlayer(int x, int y)
{
	return(playerArray[(y * GRID_SIZE) + x])
}

/************************************
	setPlayer
	Sets the state of the square at (x, y).
	Allowable states are WHITE, BLACK or EMPTY.
************************************/
void setPlayer(int x, int y, int player)
{
	playerArray[(y * GRID_SIZE) + x] = player
}

/************************************
	getValue
	Returns the value (rating) of the square at (x, y) for the player.
************************************/
int getValue(int x, int y, int player)
{
	return(valueArray[(y * GRID_SIZE) + x + (player * BOARD_SIZE)])
}

/************************************
	setValue
	Sets the value (rating) of the square at (x, y) for the player.
************************************/
void setValue(int x, int y, int player, int val)
{
	valueArray[(y * GRID_SIZE) + x + (player * BOARD_SIZE)] = val
}

/************************************
	getFlips
	Returns the flip count of the square at (x, y) for the player.
************************************/
int getFlips(int x, int y, int player)
{
	return(flipsArray[(y * GRID_SIZE) + x + (player * BOARD_SIZE)])
}

/************************************
	setFlips
	Sets the flip count of the square at (x, y) for the player.
************************************/
void setFlips(int x, int y, int player, int flips)
{
	flipsArray[(y * GRID_SIZE) + x + (player * BOARD_SIZE)] = flips
}

/************************************
	otherPlayer
************************************/
int otherPlayer(int player)
{
    return((player == WHITE) ? BLACK : WHITE)
}

/************************************
	gameOver
	Called when neither player can make a move.
	Display the result of the game to the user.
************************************/
void gameOver()
{
	infoBox("Game Over!\nWhite: " score[WHITE] "\nBlack: " score[BLACK] "")
}

/************************************
	drawDisc
	Draws a player's disc on the board in square (x, y).
************************************/
void drawDisc(int x, int y, int player)
{
	realColor(dbeCanvas, COLOURS[player])

	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 4, 40, 40)
}

/************************************
	drawTrans
	Called when the other players discs are being flipped.
	Displays an animated transition from one disc to another in square (x, y).
************************************/
void drawTrans(int x, int y, int fromPlayer, int toPlayer)
{
	// empty the square...
	realColor(dbeCanvas, COLOURS[EMPTY])
	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 4, 40, 40)

	// ... and display a quarter-turned disc (ellipse) in old colour
	realColor(dbeCanvas, COLOURS[fromPlayer])
	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 14, 40, 20)

	// give user a chance to see it
	sleep_(150)

	// empty the square...
	//realColor(dbeCanvas, COLOURS[EMPTY])
	//ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 4, 40, 40)

	// ... and draw disc on its edge (half-turned)
	//realColor(dbeCanvas, COLOURS[fromPlayer])
	//rectangle(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 22, 40, 3)
	//realColor(dbeCanvas, COLOURS[toPlayer])
	//rectangle(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 25, 40, 3)
	// give user a chance to see it
	//sleep_(100)

	// empty the square...
	realColor(dbeCanvas, COLOURS[EMPTY])
	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 4, 40, 40)

	// ... and display quarter-turned disc (ellipse) in new colour
	realColor(dbeCanvas, COLOURS[toPlayer])
	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 14, 40, 20)

	// give user a chance to see it
	sleep_(150)

	// display a full disc (circle) in new colour
	realColor(dbeCanvas, COLOURS[toPlayer])
	ellipse(dbeCanvas, x * SQUARE_SIZE + 4, y * SQUARE_SIZE + 4, 40, 40)
}

/************************************
	doDrawCanvas

	Redraw the canvas.
************************************/
void doDrawCanvas(DBE dbeCanvas)
{
	int x = 0
    int y = 0

    // fill background with empty square colour
	realBackground(dbeCanvas, COLOURS[EMPTY])

	// draw the grid of squares
    for (y = 0; y < GRID_SIZE; y++)
    {
		for (x = 0; x < GRID_SIZE; x++)
		{
			realColor(dbeCanvas, realColor_Black)
			box(dbeCanvas, x * SQUARE_SIZE, y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE)
		}
	}

	// draw player's discs
	for (x = 0; x < GRID_SIZE; x++)
	{
		for (y = 0; y < GRID_SIZE; y++)
		{
			drawDisc(x, y, getPlayer(x, y))
		}
	}
}

/************************************
	rawPutDisc
	Place a disc on the board and update the players' scores.
	If trans is TRUE, then the current player is flipping a disc and we show
	the transition. If trans is false then the player is placing a disc on an

	empty square so we can just draw it.
************************************/
void rawPutDisc(int x, int y, int player, bool trans)
{
	// get other player
	int other = otherPlayer(player)

	// if the square is occupied by the other player, then if we are flipping
	// the other player's disc, so reduce their score
    if (getPlayer(x, y) == other)
    {
	    score[other]--
    }

    // place this player's disc on the square
    setPlayer(x, y, player)

    // increase this player's score
    score[player]++

    // are we placing a disc or flipping discs?
    if (!trans)
    {
	    // this player is placing the disc in an empty square - just draw it
	    drawDisc(x, y, player)
    }
    else
    {
	    // this player is flipping discs - show the transistion from the other
    	// player's colour to this player's colour.
    	drawTrans(x, y, otherPlayer(player), player)
	}
}

/************************************
	flipDiscs
************************************/
void flipDiscs(int x, int y, int player)
{
	int deltax   = 0
	int deltay   = 0
	int distance = 0
    int posx     = 0
    int posy     = 0

    // place the player's disc in the empty square at (x, y).
    rawPutDisc(x, y, player, false)

    // check surrounding squares for other player's discs that can be flipped
    for (deltay = -1; deltay <= 1; deltay++)
    {
		for (deltax = -1; deltax <= 1; deltax++)
		{
		    for (distance = 1;; distance++)
		    {
				posx = x + (distance * deltax)
				posy = y + (distance * deltay)

				// stop if we go off the board
				if (posx < 0 || posx >= GRID_SIZE || posy < 0 || posy >= GRID_SIZE)
				{
				    break
			    }

				// stop when we reach an empty square
				if (getPlayer(posx, posy) == EMPTY)
				{
				    break
			    }

				if (getPlayer(posx, posy) == player)
				{
				    // backtrack, flipping discs
				    for (distance--; distance > 0; distance--)
				    {
						posx = x + (distance * deltax)
						posy = y + (distance * deltay)
						rawPutDisc(posx, posy, player, true)
				    }

				    break
				}
		    }
		}
    }
}

/************************************
	numFlips
************************************/
int numFlips(int x, int y, int player)
{
    int deltax = 0
	int deltay = 0
	int distance = 0
    int posx = 0
    int posy = 0
    int count = 0

    for (deltay = -1; deltay <= 1; deltay++)
    {
		for (deltax = -1; deltax <= 1; deltax++)
		{
		    for (distance = 1;; distance++)
		    {
				posx = x + (distance * deltax)
				posy = y + (distance * deltay)

				//print("checking square (" posx ", " posy ")\n")

				// stop if we go off the board
				if (posx < 0 || posx >= GRID_SIZE || posy < 0 || posy >= GRID_SIZE)
				{
					//print("off board\n")
				    break
			    }

				// stop when we reach an empty square
				if (getPlayer(posx, posy) == EMPTY)
				{
					//print("empty")
				    break
			    }

				// only update the flip count when we reach another of the
				// player's discs
				if (getPlayer(posx, posy) == player)
				{
				    count += distance - 1;
				    break
				}
	    	}
		}
    }

    //print("flip count = " count "\n")
    return(count)
}

/************************************
	anyMoves
************************************/
bool anyMoves(int turn)
{
	int x = 0
    int y = 0

    for(y = 0; y < GRID_SIZE; y++)
    {
		for(x = 0; x < GRID_SIZE; x++)
		{
	    	if (getPlayer(x, y) != EMPTY)
	    	{
		    	continue
	    	}

	    	if (numFlips(x, y, turn) > 0)
	    	{
		    	return(true)
	    	}
		}
    }

    return(false)
}

/************************************
	doneTurn
************************************/
void doneTurn()
{
	bool moves = anyMoves(turn)

	//print("done turn " turn "\n")

    turn = otherPlayer(turn)

    doDrawCanvas(dbeCanvas)

    // check whether the other player has any moves
    if (!anyMoves(turn))
    {
		if (!moves)
		{
			gameOver()
			return
		}

		if (turn == BLACK)
		{
			infoBox("You cannot go - computer will take another turn.")
		}
		else
		{
			infoBox("Computer cannot go - please take another turn.")
			mouseActive = true
		}

		// and switch players again
		turn = otherPlayer(turn)
		return
    }

    // computer takes its turn
    if (turn != BLACK)
    {
	    // make it look like we are thinking...
	    sleep_(1000)
	    othello()
    }
    else
    {
	    //ready(dbMain)
	    mouseActive = true
    }
}

/************************************
	calcFlipCounts
	Calculate the flip count for every square on the board for the player.
************************************/
void calcFlipCounts(int player)
{
	int x = 0
    int y = 0

    for(y = 0; y < GRID_SIZE; y++)
    {
		for(x = 0; x < GRID_SIZE; x++)
		{
			// initialise flip count to zero
	    	setFlips(x, y, player, 0)

	     	// if the square is empty then there is no disc to flip
	    	if (getPlayer(x, y) != EMPTY)
	    	{
		    	continue
	    	}

	    	// count and store the flip count for the square
	    	setFlips(x, y, WHITE, numFlips(x, y, player))
		}
    }
}

/************************************
	rate
************************************/
int rate(int x, int y, int player)
{
	int rating = 0

	// non-empty squares have a zero rating
    if (getPlayer(x, y) != EMPTY)
    {
	    return(0)
    }

    // get flip count for this square
    rating = getFlips(x, y, player)

    if (rating > 0)
    {
		// increase all non-zero weightings so we have room
		// to wait 'less ideal' squares below the baseline
		rating += 10

		// raise edge ratings 4 points, corners are raised 8
		if (x == 0 || x == GRID_SIZE - 1)
		{
			rating += 4
		}

		if (y == 0 || y == GRID_SIZE - 1)
		{
			rating += 4
		}

		// lower next-to-edge ratings by 5 points, next-to-corner by 10
		if (x == 1 || x == GRID_SIZE - 2)
		{
			rating -= 5
		}

		if (y == 1 || y == GRID_SIZE - 2)
		{
			rating -= 5
		}

		// we cannot rule out a move because of bad location; we must
		// always go somewhere
		if (rating < 1)
		{
			rating = 1
		}
    }

    return(rating)
}

/************************************
	othello
	This routine is called when it is the computer's turn.
************************************/
void othello()
{
    int x       = 0
    int y       = 0
    int best    = 0
    int numbest = 0
    int rating  = 0
    int pick    = 0
    int count   = 0

    // rank each position on the board by the potential flip count
    calcFlipCounts(WHITE)

    // apply rating to each square and count the number of "best" squares
    for(y = 0; y < GRID_SIZE; y++)
    {
		for(x = 0; x < GRID_SIZE; x++)
		{
		    rating = rate(x, y, WHITE)

		    // store the rating back into the board
		    setValue(x, y, WHITE, rating)

		    if (rating == best)
		    {
			    // this square is equal to the best so far
				numbest++
			}
		    else if (rating > best)
		    {
			    // this is the best square so far
				best = rating
				numbest = 1
		    }
		}
    }

    while (numbest > 0)
    {
		// if there were more than one "best" squares then we
		// will need to pick one of these at random.
		pick = random(numbest)

		count = 0

		// loop through all squares again
		for (y = 0; y < GRID_SIZE; y++)
		{
		    for(x = 0; x < GRID_SIZE; x++)
		    {
			    // read it's rating.
				rating = getValue(x, y, WHITE)

				// if this square's rating is equal to the "best"
				if (rating == best)
				{
					// only choose this square if it was randomly selected earlier
				    if (count == pick)
				    {
					    // make the move
						flipDiscs(x, y, WHITE)
						doneTurn()
						return
				    }
				    else
				    {
					    count++
				    }
				}
		    }
		}
    } 

    // if we make it here, then there was nowhere to go
    doneTurn()
}

/************************************
	putDisc
	This is only ever called when Black (human) is playing a turn.
	Check that the user can put a disc on square (x, y).
	If the user can put a disc here then call flipDiscs to make the move.
************************************/
void putDisc(int x, int y)
{
	int flipCount = 0

	// check that it is the human player's turn
    if (turn != BLACK)
    {
	    //print("not your turn\n")
		return
	}

	// check that the squarte is empty
	// user is allowed to click another square
    if (getPlayer(x, y) != EMPTY)
    {
	    //print("square not empty\n")
	    mouseActive = true
		return
	}

	// get the flip count. i.e. how many of the opponent's discs
	// will be flipped by moving here.
    flipCount = numFlips(x, y, BLACK)

    // must flip at least once disc for a legal move
    // user is allowed to click another square
    if (flipCount == 0)
    {
	    mouseActive = true
	    return
    }

    // make the move
    flipDiscs(x, y, BLACK)

    // refresh the display
    doDrawCanvas(dbeCanvas)

    // end of turn
    doneTurn()
}

/************************************
	initialiseGrid
	Initialise the data structures ready for a new game.
************************************/
void initialiseGrid()
{
	int x = 0
	int y = 0

	// empty the board and set flips and values to zero.
	for (x = 0; x < GRID_SIZE; x++)
	{
		for (y = 0; y < GRID_SIZE; y++)
		{
			setPlayer(x, y, EMPTY)

			setValue(x, y, WHITE, 0)
			setValue(x, y, BLACK, 0)

			setFlips(x, y, WHITE, 0)
			setFlips(x, y, BLACK, 0)
		}
	}

	// initial placements of discs
	setPlayer(3, 3, WHITE)
	setPlayer(4, 4, WHITE)
	setPlayer(3, 4, BLACK)
	setPlayer(4, 3, BLACK)

	// initial scores
	score[WHITE] = 2
	score[BLACK] = 2
}

/************************************
	doMouse

	Callback from user mouse click on the canvas.
************************************/
void doMouse(DBE dbeCanvas, int but, bool ctrl, int xPixels, int yPixels)
{
	const int LEFT_MOUSE  = 1
	const int RIGHT_MOUSE = 3

	int x = 0
	int y = 0

	if (!mouseActive)
	{
		//print "inactive "
		return
	}

	//print "active "
	// prevent user making any more mouse clicks until turn is restored.
	mouseActive = false
	//busy(dbMain)

	if (turn != BLACK)
	{
		//print("ignore\n")
		return
	}

	// only react to a left mouse click
	if (but != LEFT_MOUSE)
	{
		mouseActive = true
		return
	}

	x = xPixels / SQUARE_SIZE
	y = yPixels / SQUARE_SIZE

	//print("(" x "," y ") ")

	putDisc(x, y)
	//drawDisc(x, y, WHITE)
}

/************************************
	doInitialise
************************************/
void doInitialise(DB db)
{
	initialiseGrid()
	doDrawCanvas(dbeCanvas)
	mouseActive = true
	turn = BLACK
}

/************************************
	doCloseAction

	Clean up data structures when closing the dialog.
************************************/
void doCloseAction(DB db)
{
	hide(dbMain)
	destroy(dbMain)
	dbMain = null
}

/************************************
	MAIN
************************************/
initialiseGrid()

dbMain = create("Othello", styleCentered | styleFixed)

dbeCanvas = canvas(dbMain,
                   (GRID_SIZE * (SQUARE_SIZE + 2) - 5),
                   (GRID_SIZE * (SQUARE_SIZE + 2) - 5),
                   doDrawCanvas)

dbeStart = apply(dbMain, "New Game", doInitialise)
close(dbMain, true, doCloseAction)

realize dbMain

set(dbeCanvas, doMouse)

doDrawCanvas(dbeCanvas)

show dbMain