Search code examples
scalaswingrepaint

Scala Swing skips repaint of the Frame


I am currently working on an implementation of the game Othello in Scala and so far it's working pretty nicely. When implementing the GUI though (using Scala Swing) I've stumbled across an issue that I can't seem to fix.

When playing against the computer opponent the Frame seems to be repainted only when the bot is done making its move.

The game is also playable through the terminal and doing so updates the Frame properly every time regardless of the player configuration (Player vs Player or Player vs Computer). Also playing player vs player using the GUI exclusively presents no issues at all.

It might be an oversight on my behalf, but so far I am unable to find a solution and would greatly appreciate any help.

So far I have tried various combinations of revalidating and repainting the individual panels, adding and removing listeners, changing my implementation of the reactor pattern to the one provided by Scala Swing, adding Thread.sleep to see if there could be a scheduling conflict of sorts.

import java.awt.Color

import othello.controller.Controller
import javax.swing.ImageIcon
import javax.swing.border.LineBorder

import scala.swing.event.MouseClicked
import scala.swing.{BorderPanel, BoxPanel, Dimension, FlowPanel, GridPanel, Label, Orientation}

class TablePanel(controller: Controller) extends FlowPanel {

  val sides = 32
  val sidesColor: Color = Color.lightGray
  val squareSize = 52

  def tableSize: Int = controller.board.size

  def edgeLength: Int = tableSize * squareSize

  def rows: BoxPanel = new BoxPanel(Orientation.Vertical) {
    background = sidesColor
    preferredSize = new Dimension(sides, edgeLength)
    contents += new Label {
      preferredSize = new Dimension(sides, sides)
    }
    contents += new GridPanel(tableSize, 1) {
      background = sidesColor
      for { i <- 1 to rows } contents += new Label(s"$i")
    }
  }

  def columns: GridPanel = new GridPanel(1, tableSize) {
    background = sidesColor
    preferredSize = new Dimension(edgeLength, sides)
    for { i <- 0 until columns } contents += new Label(s"${(i + 65).toChar}")
  }

  def table: GridPanel = new GridPanel(tableSize, tableSize) {
    background = new Color(10, 90, 10)
    for {
      col <- 0 until columns
      row <- 0 until rows
    } contents += square(col, row)
  }

  def square(row: Int, col: Int): Label = new Label {
    border = new LineBorder(new Color(30, 30, 30, 140), 1)
    preferredSize = new Dimension(squareSize, squareSize)
    icon = controller.board.valueOf(col, row) match {
      case -1 => new ImageIcon("resources/big_dot.png")
      case 0  => new ImageIcon("resources/empty.png")
      case 1  => new ImageIcon("resources/black_shadow.png")
      case 2  => new ImageIcon("resources/white_shadow.png")
    }
    listenTo(mouse.clicks)
    reactions += {
      case _: MouseClicked =>
        if (controller.options.contains((col, row))) controller.set(col, row)
        else if (controller.board.gameOver) controller.newGame()
        else controller.highlight()
    }
  }

  def redraw(): Unit = {
    contents.clear
    contents += new BorderPanel {
      add(rows, BorderPanel.Position.West)
      add(new BoxPanel(Orientation.Vertical) {
        contents += columns
        contents += table
      }, BorderPanel.Position.East)
    }
    repaint
  }
}
import scala.swing._
import othello.controller._
import othello.util.Observer

import scala.swing.event.Key

class SwingGui(controller: Controller) extends Frame with Observer {

  controller.add(this)

  lazy val tablePanel = new TablePanel(controller)

  lazy val mainFrame: MainFrame = new MainFrame {
    title = "Othello"
    menuBar = menus
    contents = tablePanel
    centerOnScreen
    // peer.setAlwaysOnTop(true)
    resizable = false
    visible = true
  }

  def menus: MenuBar = new MenuBar {
    contents += new Menu("File") {
      mnemonic = Key.F
      contents += new MenuItem(Action("New Game") {
        controller.newGame()
      })
      contents += new MenuItem(Action("Quit") {
        controller.exit()
      })
    }
    contents += new Menu("Edit") {
      mnemonic = Key.E
      contents += new MenuItem(Action("Undo") {
        controller.undo()
      })
      contents += new MenuItem(Action("Redo") {
        controller.redo()
      })
    }
    contents += new Menu("Options") {
      mnemonic = Key.O
      contents += new MenuItem(Action("Highlight possible moves") {
        controller.highlight()
      })
      contents += new MenuItem(Action("Reduce board size") {
        controller.resizeBoard("-")
      })
      contents += new MenuItem(Action("Increase board size") {
        controller.resizeBoard("+")
      })
      contents += new MenuItem(Action("Reset board size") {
        controller.resizeBoard(".")
      })
      contents += new Menu("Game mode") {
        contents += new MenuItem(Action("Player vs. Computer") {
          controller.setupPlayers("1")
        })
        contents += new MenuItem(Action("Player vs. Player") {
          controller.setupPlayers("2")
        })
      }
    }
  }

  def update: Boolean = {
    tablePanel.redraw()
    mainFrame.pack
    mainFrame.centerOnScreen
    mainFrame.repaint
    true
  }
}

The expected behavior is a repainted Frame on every turn. The actual result is the Frame only being repainted after the opponent made a move. It only happens when playing player vs bot exclusively through clicking the UI.


Solution

  • I don't think the problem is in the code you have shown, but I would bet that you are blocking the "event dispatch thread" (the UI thread) with your AI computation for the computer player.

    In a Swing app, there is a special thread called the "event dispatch thread" that is responsible for handling messages from the O/S including dealing with repaint messages. All UI event handlers will be invoked on this thread. If you use that thread to do any computations which take a long time (such as a computer move in a game like this), any UI updates will be blocked until the thread becomes free.

    This tutorial has more info: https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html

    You need to move the AI onto a background thread and release the event dispatch thread to deal with repaints. This might be tricky to implement if you are not familiar with multi-threaded programs! Good luck.