Othello (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