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.
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.
As Rich suggested, the solution was concurrency. Making the search algorithm a Future-type solved the issue for now. It might not be the cleanest implementation as it is the first time I'm using Future, but this is what it looks like now:
def selectAndSet(): Future[_] = Future(if (!board.gameOver && player.isBot) {
new MoveSelector(this).select() match {
case Success(square) => set(square)
case _ => omitPlayer()
}
selectAndSet()
})(ExecutionContext.global)
Related
I'm trying to use Amazon Rekognition to draw boxes around detected text in an image.
Here's a stripped-down JavaFX app that is supposed to do that:
object TextRekognizeGui extends JFXApp {
lazy val rekognition: Option[AmazonRekognition] =
RekogClient.get(config.AwsConfig.rekogEndpoint, config.AwsConfig.region)
private val config = MainConfig.loadConfig()
stage = new PrimaryStage {
scene = new Scene {
var imgFile: Option[File] = None
content = new HBox {
val imgFile: ObjectProperty[Option[File]] = ObjectProperty(None)
val img: ObjectProperty[Option[Image]] = ObjectProperty(None)
val lines: ObjectProperty[Seq[TextDetection]] = ObjectProperty(Seq.empty)
// These two print statements are always executed when we press the appropriate button.
img.onChange((_, _, newValue) => println("New image"))
lines.onChange((_, _, newValue) => println(s"${newValue.length} new text detections"))
children = Seq(
new VBox {
children = Seq(
new OpenButton(img, imgFile, lines, stage),
new RekogButton(img, imgFile, lines, rekognition)
)
},
new ResultPane(675, 425, img, lines))
}
}
}
stage.show
}
class ResultPane(width: Double, height: Double, img: ObjectProperty[Option[Image]],
textDetections: ObjectProperty[Seq[TextDetection]]) extends AnchorPane { private val emptyPane: Node = Rectangle(0, 0, width, height)
// This print statement, and the one below, are often not executed even when their dependencies change.
private val backdrop = Bindings.createObjectBinding[Node](
() => {println("new backdrop"); img.value.map(new MainImageView(_)).getOrElse(emptyPane)},
img
)
private val outlines = Bindings.createObjectBinding[Seq[Node]](
() => {println("new outlines"); textDetections.value map getTextOutline},
textDetections
)
This looks to me as though I followed the ScalaFX documentation's directions for creating a custom binding. When I click my "RekogButton", I expect to see the message " new text detections" along with the message "new outlines", but more often than not I see only the " new text detections" message.
The behavior appears to be the same throughout the lifetime of the program, but it only manifests on some runs. The same problem happens with the img -> background binding, but it manifests less often.
Why do my properties sometimes fail to call the bindings when they change?
edit 1
JavaFX stores the listeners associated with bindings as a weak reference, meaning they can still be garbage-collected unless another object holds a strong or soft reference to them. So it seems my app fails if the garbage collector happens to run between app initialization and the time the outlines are drawn.
This is odd, because I seem to have a strong reference to the binding -- the "delegate" field of the value "outlines" in the result pane. Looking into it more.
I'm having an issue with a project I'm working on. The original code is too big to poste here but I wrote a short version which has the same issue.
Basically, I wrote some kind of chatbox that adds Labels containing the replies to a BoxPanel, one after another. Ideally the user wouldn't need to scroll manually, the scrolling would happen automatically and show the very last reply of the chat.
The issue is that when using scrollBar.peer.setValue(scrollBar.peer.getMaximum),
the chat doesn't automatically scroll all the way down, it never shows the very last line of the chat, even after I revalidate() the chat before scrolling down.
this is what the code looks like:
import scala.swing._
import scala.swing.event._
class testUI extends MainFrame {
title = "testScroll"
preferredSize = new Dimension(400, 400)
val chat = new BoxPanel(Orientation.Vertical)
val inputField = new TextField {
listenTo(mouse.clicks, this.keys)
reactions += {
case KeyPressed(_, Key.Enter, _, _) => if (this.text != "") { printToChat(this.text) }
}
}
val scrollPanel = new ScrollPane(chat)
var scrollBar = scrollPanel.verticalScrollBar
val window = new BorderPanel {
add(inputField, BorderPanel.Position.North)
add(scrollPanel, BorderPanel.Position.Center)
}
contents = window
def printToChat(s: String) {
chat.contents += new Label(s) {
horizontalAlignment = Alignment.Right
border = Swing.EmptyBorder(20, 20, 0, 0) //(top, left, bottom, right)
}
chat.revalidate()
scrollToBottom()
inputField.text = ""
}
def scrollToBottom() {
scrollBar.peer.setValue(scrollBar.peer.getMaximum)
}
}
object testUI {
def main(args: Array[String]) {
val ui = new testUI
ui.visible = true
}
}
Thank you very much in advance :)
EDIT: I solved my issue thanks to camickr by wrapping my previous line of code with Swing.onEDT:
Swing.onEDT(scrollBar.peer.setValue(scrollBar.peer.getMaximum))
instead of just:
scrollBar.peer.setValue(scrollBar.peer.getMaximum)
I have a main window with some information. I can't start the Alert by timer. I need to show alert every 10 seconds(with working main window) and by the alert's button change the Label's text in the main window.
I have this code, but it's not work:
object main extends JFXApp {
stage = new JFXApp.PrimaryStage() {
scene = new Scene {
val MyLabel = new Label("SomeText")
root = new VBox {
children = MyLabel
}
}
val ButtonTypeOne = new ButtonType("Change the text")
val ButtonTypeTwo = new ButtonType("No")
val alert1 = new Alert(AlertType.Warning) {
initOwner(stage)
title = "Warning!!!"
headerText = "Header"
contentText = "Do you need to change the text?"
buttonTypes = Seq(ButtonTypeOne, ButtonTypeTwo, ButtonType.Cancel)
}
val result = alert1.showAndWait()
val timerA = new PauseTransition(Duration(5000))
timerA.onFinished = { _ =>
result match {
case Some(ButtonTypeOne) => /*Here I need to change text in MyLabel */
case Some(ButtonTypeTwo) => None
case _ => None
}
timerA.playFromStart()
}
timerA.play
}
}
There are a few different problems in your code:
You display the alert only once, regardless of what's happening with the timer.
The code that reacts to the result of the alert in the timer always looks at the same initial result.
Unfortunately, even if you move the val result = alert1.showAndWait inside of the timer's animation update event, JavaFX makes it illegal to call showAndWait within such a routine.
The solution is to create an onHidden event handler for alert1, which reacts to it being closed. The button type used to close the dialog is then stored in alert1.result, so you can use that to determine what action to take.
I've added a StringProperty to assist with the changing of the label value. The following example should be a good starting point for what you want to achieve...
import scalafx.Includes._
import scalafx.animation.PauseTransition
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.StringProperty
import scalafx.scene.Scene
import scalafx.scene.layout.VBox
import scalafx.scene.control.{Alert, ButtonType, Label}
import scalafx.scene.control.Alert.AlertType
import scalafx.util.Duration
object main extends JFXApp {
// String property for the text in myLabel.
val strProp = StringProperty("Some Text")
// Declare this so that it's accessible from timerA.onFinished.
val myLabel = new Label {
// Bind the text property to the value of strProp.
// When strProp's value changes, so does the label's text.
text <== strProp
}
stage = new PrimaryStage() {
scene = new Scene {
root = new VBox {
children = myLabel
}
}
}
// Custom buttons.
val ButtonTypeOne = new ButtonType("Change the text")
val ButtonTypeTwo = new ButtonType("No")
// Create the timer. This goes off after 5,000ms (5 seconds) - after play is called.
val timerA = new PauseTransition(Duration(5000))
// Alert dialog.
// Note: JavaFX forbids use of showAndWait within animation processing, so we must use
// an onHidden event instead.
val alert1 = new Alert(AlertType.Warning) {
initOwner(stage)
title = "Warning!!!"
headerText = "Header"
contentText = "Do you need to change the text?"
buttonTypes = Seq(ButtonTypeOne, ButtonTypeTwo, ButtonType.Cancel)
}
// React to the dialog being closed.
alert1.onHidden = {_ =>
alert1.result.value match {
// If button type one, change the property value.
// Note alert1.result.value is a JavaFX ButtonType, so use .delegate for the match.
case ButtonTypeOne.delegate => strProp.value = "Changed!"
// Otherwise, do nothing.
case _ =>
}
// Start the timer once more.
// This is going to be a very annoying app! ;-)
timerA.playFromStart()
}
// When the timer goes off, show the alert.
timerA.onFinished = {_ =>
alert1.show()
}
// Start the timer for the first time.
timerA.play
}
If I run this example code
class ExampleGUIElement extends Panel
{
preferredSize = new Dimension(100, 100)
val rand : Random = Random
override def paintComponent(g: Graphics2D) = g.drawLine(rand.nextInt(10),rand.nextInt(10),rand.nextInt(90) + 10 ,rand.nextInt(90) + 10)
}
object GUITest extends SimpleSwingApplication
{
def top = new MainFrame
{
contents = new ExampleGUIElement
}
}
It obviously only shows the one line that has been draw initially. If I want a periodic repaint I normally would just make a Timer that calls repaint on the most outer JFrame.
But SimpleSwingApplication does not have this method, at least I can not find it, and calling top.repaint instead just does nothing. So how do you periodically repaint the whole thing?
Consider calling peer on the top (see MainFrame), and then calling repaint on that:
top.peer.repaint()
Also, don't forget to call top.pack().
I realize this is dated, but I extracted your Panel into a val and added a timer to repaint it:
def top = new MainFrame
{
val example = new ExampleGUIElement
contents = example
val timer=new javax.swing.Timer(250, Swing.ActionListener(e =>
{
example.repaint()
}))
timer.start()
}
I'm writing a small GUI program. Everything works except that I want to recognize mouse double-clicks. However, I can't recognize mouse clicks (as such) at all, though I can click buttons and select code from a list.
The following code is adapted from Ingo Maier's "The scala.swing package":
import scala.swing._
import scala.swing.event._
object MouseTest extends SimpleGUIApplication {
def top = new MainFrame {
listenTo(this.mouse) // value mouse is not a member of scala.swing.MainFrame
reactions += {
case e: MouseClicked =>
println("Mouse clicked at " + e.point)
}
}
}
I've tried multiple variations: mouse vs. Mouse, SimpleSwingApplication, importing MouseEvent from java.awt.event, etc. The error message is clear enough--no value mouse in MainFrame--so, where is it then? Help!
Maybe that way?
object App extends SimpleSwingApplication {
lazy val ui = new Panel {
listenTo(mouse.clicks)
reactions += {
case e: MouseClicked =>
println("Mouse clicked at " + e.point)
}
}
def top = new MainFrame {
contents = ui
}
}
BTW, SimpleGUIApplication is deprecated
The MouseClicked event has an attribute clicks, which should be 2 if it was a double-click. Have a look at java.awt.event.MouseEvent for the original source if you're curious.