Perhaps we also want to allow the user to play the game with a keyboard. Let's say we number the fields from 1 to 9 (top-left to bottom-right), and pressing the number should be the same as clicking with the mouse in the field.
To handle this, the Canvas needs to listen to the keyboard:
listenTo(keys)
We then have to add a reaction to the scala.swing.event.KeyTyped event. Its four parameters are the source component, the Char, the modifiers, and the location (used to distinguish multiple keys such as left and right Shift keys or the keys on the numeric keypad). We only use the Char parameter:
listenTo(mouse.clicks) listenTo(keys) reactions += { case MouseClicked(_, p, _, _, _) => mouseClick(p.x, p.y) case KeyTyped(_, c, _, _) => if ('1' <= c && c <= '9') { val idx = c - '1' publish(TicTacToeEvent(idx % 3, idx / 3)) } }
In addition to scala.swing.event.KeyTyped, there are scala.swing.event.KeyPressed and scala.swing.event.KeyReleased, which allow you to detect when the key is pressed in and when it is released again.
If you run the program like this, the keyboard doesn't work yet. The reason is that the Canvas object does not have the keyboard focus—currently, only the two buttons can receive keyboard input.
We fix this by setting the focusable attribute of the Canvas, and now everything works.
Here is again the full program tictactoe4.scala:
import scala.swing._ import scala.swing.event._ import java.awt.{Color,Graphics2D,BasicStroke} import java.awt.geom._ // -------------------------------------------------------------------- class Board { private var player = 1 private val grid = Array(0, 0, 0, 0, 0, 0, 0, 0, 0) def apply(x: Int, y: Int): Int = grid(3 * y + x) def currentPlayer: Int = player def play(x: Int, y: Int) { if (this(x, y) == 0) { grid(3 * y + x) = player player = 3 - player } } def restart() { for (i <- 0 until 9) grid(i) = 0 player = 1 } } // -------------------------------------------------------------------- case class TicTacToeEvent(x: Int, y: Int) extends Event // -------------------------------------------------------------------- class Canvas(val board: Board) extends Component { preferredSize = new Dimension(320, 320) focusable = true listenTo(mouse.clicks) listenTo(keys) reactions += { case MouseClicked(_, p, _, _, _) => mouseClick(p.x, p.y) case KeyTyped(_, c, _, _) => if ('1' <= c && c <= '9') { val idx = c - '1' publish(TicTacToeEvent(idx % 3, idx / 3)) } } // returns squareSide, x0, y0, wid private def squareGeometry: (Int, Int, Int, Int) = { val d = size val squareSide = d.height min d.width val x0 = (d.width - squareSide)/2 val y0 = (d.height - squareSide)/2 (squareSide, x0, y0, squareSide/3) } private def mouseClick(x: Int, y: Int) { val (squareSide, x0, y0, wid) = squareGeometry if (x0 <= x && x < x0 + squareSide && y0 <= y && y < y0 + squareSide) { val col = (x - x0) / wid val row = (y - y0) / wid publish(TicTacToeEvent(col, row)) } } override def paintComponent(g : Graphics2D) { g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON) g.setColor(Color.WHITE); val d = size g.fillRect(0,0, d.width, d.height) val (squareSide, x0, y0, wid) = squareGeometry g.setColor(Color.BLACK) // vertical lines for (x <- 1 to 2) g.draw(new Line2D.Double(x0 + x * wid, y0, x0 + x * wid, y0 + squareSide)) // horizontal lines for (y <- 1 to 2) g.draw(new Line2D.Double(x0, y0 + y * wid, x0 + squareSide, y0 + y * wid)) g.setStroke(new BasicStroke(3f)) for (x <- 0 until 3) { for (y <- 0 until 3) { board(x, y) match { case 1 => g.setColor(Color.RED) g.draw(new Ellipse2D.Double(x0 + x * wid + 10, y0 + y * wid + 10, wid - 20, wid - 20)) case 2 => g.setColor(new Color(0, 160, 0)) val x1 = x0 + x * wid + 10 val y1 = y0 + y * wid + 10 g.draw(new Line2D.Double(x1, y1, x1 + wid - 20, y1 + wid - 20)) g.draw(new Line2D.Double(x1, y1 + wid - 20, x1 + wid - 20, y1)) case _ => // draw nothing } } } } } // -------------------------------------------------------------------- class UI(val board: Board) extends MainFrame { private def restrictHeight(s: Component) { s.maximumSize = new Dimension(Short.MaxValue, s.preferredSize.height) } title = "Tic Tac Toe #4" val canvas = new Canvas(board) val newGameButton = Button("New Game") { newGame() } val turnLabel = new Label("Player 1's turn") turnLabel.foreground = Color.BLUE val quitButton = Button("Quit") { sys.exit(0) } val buttonLine = new BoxPanel(Orientation.Horizontal) { contents += newGameButton contents += Swing.HGlue contents += turnLabel contents += Swing.HGlue contents += quitButton } // make sure that resizing only changes the TicTacToeDisplay restrictHeight(buttonLine) contents = new BoxPanel(Orientation.Vertical) { contents += canvas contents += Swing.VStrut(10) contents += buttonLine border = Swing.EmptyBorder(10, 10, 10, 10) } listenTo(canvas) reactions += { case TicTacToeEvent(x, y) => board.play(x, y) updateLabelAndBoard() } def updateLabelAndBoard() { turnLabel.text = "Player %d's turn".format(board.currentPlayer) canvas.repaint() } def newGame() { board.restart() updateLabelAndBoard() } } object TicTacToeFour { def main(args: Array[String]) { val board = new Board val ui = new UI(board) ui.visible = true } }
This was the last version of our TicTacToe game. Here are two suggestions for how you could improve the game:
Extend the program so that it can recognize when the game has ended, either because one player has three in a row, or because the board is full.
Add a method winner to the Board class that:
Use this method to check the board after every move (the best place would be in updateLabelAndBoard). If the game has ended, pop up a dialog with a message declaring the winner. After the user has closed the dialog, the game should restart automatically.
Modify the game so that Player 2 is played by the computer. Every time the user makes a move, check if the game has ended because player 1 has three in a row or because the board is full. If so, display a dialog, after which the game restarts.
Otherwise, compute a move for player 2 and play the move. If the computer has won, display a dialog (after which the game restarts).
Keep a counter showing how often the human and the computer have won a game at the bottom of the screen (where it currently displays whose turn it is—we don't need that anymore).
To make it more interesting (and complicated), you could switch roles after each round. (So in round one the computer plays player 2, in round two the computer plays player 1, then again player 2, and so on.)