diff --git a/build.gradle.kts b/build.gradle.kts index 08d43be4..7158470c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ try { } application { - mainClassName = "sc.gui.GuiAppKt" // not migrating from legacy because of https://github.com/johnrengelman/shadow/issues/609 + mainClassName = "sc.gui.GuiAppKt" // not migrating from legacy because of https://github.com/johnrengelman/shadow/issues/609 - waiting for 6.2 release // these are required because of using JDK >8, // see https://github.com/controlsfx/controlsfx/wiki/Using-ControlsFX-with-JDK-9-and-above applicationDefaultJvmArgs = listOf( @@ -75,6 +75,10 @@ tasks { } } + withType { + manifest.attributes["Main-Class"] = application.mainClassName + } + javafx { version = "13" modules("javafx.controls", "javafx.fxml", "javafx.base", "javafx.graphics") diff --git a/src/main/kotlin/sc/gui/controller/BoardController.kt b/src/main/kotlin/sc/gui/controller/BoardController.kt index 8e3a6057..e1558e14 100644 --- a/src/main/kotlin/sc/gui/controller/BoardController.kt +++ b/src/main/kotlin/sc/gui/controller/BoardController.kt @@ -7,14 +7,25 @@ import sc.plugin2021.* import sc.plugin2021.util.Constants import sc.plugin2021.util.GameRuleLogic import tornadofx.Controller +import tornadofx.objectBinding class BoardController : Controller() { var currentHover: Coordinates? = null var hoverable: Boolean = true var currentPlaceable: Boolean = false - val board: BoardModel by inject() + val boardModel: BoardModel by inject() val view: BoardView by inject() val game: GameController by inject() + + init { + boardModel.board.bind(game.gameState.objectBinding { it?.board }) + subscribe { event -> + event.gameState.let { + calculateIsPlaceableBoard(it.board, it.currentColor) + } + } + } + private var isHoverableBoard: Array> = Array(Constants.BOARD_SIZE) { Array(Constants.BOARD_SIZE) { true } } private var isPlaceableBoard: Array> = Array(Constants.BOARD_SIZE) { Array(Constants.BOARD_SIZE) { false } } @@ -24,18 +35,16 @@ class BoardController : Controller() { val color = game.selectedColor.get() val move = SetMove(Piece(color, game.selectedShape.get(), game.selectedRotation.get(), game.selectedFlip.get(), Coordinates(x, y))) - GameRuleLogic.validateSetMove(board.boardProperty().get(), move) + GameRuleLogic.validateSetMove(boardModel.board.get(), move) fire(HumanMoveAction(move)) - game.isHumanTurn.set(false) } else { logger.debug("Set-Move from GUI at [$x,$y] seems invalid") } } - fun hoverInBound(x: Int, y: Int): Boolean { - return x >= 0 && y >= 0 && x < Constants.BOARD_SIZE && y < Constants.BOARD_SIZE - } + fun hoverInBound(x: Int, y: Int): Boolean = + x >= 0 && y >= 0 && x < Constants.BOARD_SIZE && y < Constants.BOARD_SIZE fun calculateIsPlaceableBoard(board: Board, color: Color) { logger.debug("Calculating where pieces can be hovered and placed on the board...") @@ -72,10 +81,6 @@ class BoardController : Controller() { } fun isHoverable(x: Int, y: Int, shape: Set): Boolean { - if (!game.isHumanTurn.get()) { - return false - } - for (place in shape) { // check every adjacent field if it is the same color if (!hoverInBound(x + place.x, y + place.y)) { @@ -84,20 +89,16 @@ class BoardController : Controller() { return false } } - return true } fun isPlaceable(x: Int, y: Int, shape: Set): Boolean { - if (game.isHumanTurn.get()) { - for (place in shape) { - // one field is enough as isHoverable prevents otherwise - if (hoverInBound(x + place.x, y + place.y) && isPlaceableBoard[x + place.x][y + place.y]) { - return true - } + for (place in shape) { + // one field is enough as isHoverable prevents otherwise + if (hoverInBound(x + place.x, y + place.y) && isPlaceableBoard[x + place.x][y + place.y]) { + return true } } - return false } diff --git a/src/main/kotlin/sc/gui/controller/GameController.kt b/src/main/kotlin/sc/gui/controller/GameController.kt index 2cb8e7bf..fbe02dc5 100644 --- a/src/main/kotlin/sc/gui/controller/GameController.kt +++ b/src/main/kotlin/sc/gui/controller/GameController.kt @@ -4,16 +4,15 @@ import javafx.beans.binding.BooleanBinding import javafx.beans.binding.ObjectBinding import javafx.beans.property.ObjectProperty import javafx.beans.property.Property +import javafx.beans.value.ObservableValue import org.slf4j.LoggerFactory import sc.gui.model.PiecesModel import sc.gui.view.PiecesFragment import sc.plugin2021.* import sc.plugin2021.util.GameRuleLogic import sc.shared.GameResult -import tornadofx.Controller -import tornadofx.nonNullObjectBinding -import tornadofx.objectProperty -import java.util.* +import tornadofx.* +import java.util.EnumMap import kotlin.math.max // The following *Binding-classes are necessary to automatically unbind and rebind to a new piece (when switched) @@ -134,29 +133,40 @@ class CalculatedShapeBinding(piece: Property) : ObjectBinding(null) + val gameResult = objectProperty() val isHumanTurn = objectProperty(false) - val canSkip = objectProperty(false) - val previousColor = objectProperty(Color.RED) - val teamOneScore = objectProperty(0) - val teamTwoScore = objectProperty(0) + + val currentTurn = nonNullObjectBinding(gameState) { value?.turn ?: 0 } + val currentRound = nonNullObjectBinding(gameState) { value?.round ?: 0 } + val currentColor = nonNullObjectBinding(gameState) { value?.currentColor ?: Color.RED } + val currentTeam = nonNullObjectBinding(gameState) { value?.currentTeam ?: Team.ONE } + val teamScores = gameState.objectBinding { state -> + Team.values().map { state?.getPointsForPlayer(it) } + } + + val availableTurns = objectProperty(0).also { avTurns -> + currentTurn.addListener { _, _, turn -> + avTurns.set(turn?.let { max(it, avTurns.value) }) } + } val started = nonNullObjectBinding(currentTurn, isHumanTurn) { value > 0 || isHumanTurn.value } - val playerNames = objectProperty>() - val gameResult = objectProperty() - - val undeployedPieces: Map>> = EnumMap( - Color.values().associateWith { objectProperty(PieceShape.shapes.values) }) + val playerNames = gameState.objectBinding { it?.playerNames } + val gameEnded = gameResult.booleanBinding { it != null } + + val canSkip = isHumanTurn.booleanBinding(gameEnded) { humanTurn -> + (humanTurn == true && + !gameEnded.value && + gameState.value?.let { GameRuleLogic.isFirstMove(it) } == false + ).also { logger.debug("Human turn $humanTurn - canSkip $it") } + } + + val undeployedPieces: Map>> = EnumMap( + Color.values().associateWith { color -> + nonNullObjectBinding(gameState, gameState) { value?.undeployedPieceShapes(color) ?: PieceShape.values().toList() } + }) val validPieces: Map>> = EnumMap( Color.values().associateWith { objectProperty(emptyList()) }) @@ -171,64 +181,46 @@ class GameController : Controller() { val selectedFlip: FlipBinding = FlipBinding(currentPiece) val selectedCalculatedShape: CalculatedShapeBinding = CalculatedShapeBinding(currentPiece) - fun isValidColor(color: Color): Boolean = gameState.get().isValid(color) + fun isValidColor(color: Color): Boolean = + gameState.get()?.isValid(color) != false init { + // TODO this event is received repeatedly subscribe { event -> - logger.debug("New game state") - val state = event.gameState + logger.debug("New state: $state") + if(logger.isTraceEnabled) + logger.trace(state.longString()) gameState.set(state) - canSkip.set(false) - - previousColor.set(currentColor.get()) - currentColor.set(state.currentColor) - currentTeam.set(state.currentTeam) - boardController.board.boardProperty().set(state.board) - undeployedPieces.forEach { (color, pieces) -> - pieces.set(state.undeployedPieceShapes(color)) - } - validPieces.forEach { (_, pieces) -> - pieces.set(emptyList()) - } - - availableTurns.set(max(availableTurns.get(), state.turn)) - playerNames.set(state.playerNames) - currentTurn.set(state.turn) - currentRound.set(state.round) - teamOneScore.set(state.getPointsForPlayer(Team.ONE)) - teamTwoScore.set(state.getPointsForPlayer(Team.TWO)) } subscribe { event -> val state = event.gameState - val moves = state.undeployedPieceShapes().map { - it to GameRuleLogic.getPossibleMovesForShape(state, it) - }.toMap() + val moves = EnumMap( + state.undeployedPieceShapes().associateWith { + GameRuleLogic.getPossibleMovesForShape(state, it) + }) logger.debug("Human move request for {} - {} possible moves", state.currentColor, moves.values.sumBy { it.size }) - + + gameState.set(event.gameState) isHumanTurn.set(true) - canSkip.set(!gameEnded() && isHumanTurn.get() && !GameRuleLogic.isFirstMove(state)) - boardController.calculateIsPlaceableBoard(state.board, state.currentColor) validPieces.getValue(state.currentColor) .set(moves.filterValues { it.isNotEmpty() }.keys) } + subscribe { + isHumanTurn.set(false) + } subscribe { event -> gameResult.set(event.result) } } - fun gameEnded(): Boolean = gameResult.isNotNull.get() - fun clearGame() { + gameState.set(null) gameResult.set(null) - boardController.board.boardProperty().set(Board()) availableTurns.set(0) - currentTurn.set(0) - currentRound.set(0) - undeployedPieces.forEach { (_, pieces) -> pieces.set(PieceShape.values().toList()) } } fun selectPiece(piece: PiecesModel) { diff --git a/src/main/kotlin/sc/gui/model/BoardModel.kt b/src/main/kotlin/sc/gui/model/BoardModel.kt index c6f7bd9f..93508b09 100644 --- a/src/main/kotlin/sc/gui/model/BoardModel.kt +++ b/src/main/kotlin/sc/gui/model/BoardModel.kt @@ -2,18 +2,16 @@ package sc.gui.model import org.slf4j.LoggerFactory import sc.gui.view.BoardView -import sc.plugin2021.* -import tornadofx.* +import sc.plugin2021.Board +import tornadofx.ItemViewModel +import tornadofx.objectProperty class BoardModel : ItemViewModel() { - private var calculatedBlockSize: Double by property(16.0) - fun calculatedBlockSizeProperty() = getProperty(BoardModel::calculatedBlockSize) - - private var board: Board by property() - fun boardProperty() = getProperty(BoardModel::board) + val calculatedBlockSize = objectProperty(16.0) + val board = objectProperty() init { - calculatedBlockSizeProperty().addListener { _, old, new -> + calculatedBlockSize.addListener { _, old, new -> logger.debug("Blocksize changed $old -> $new") } } diff --git a/src/main/kotlin/sc/gui/view/BoardView.kt b/src/main/kotlin/sc/gui/view/BoardView.kt index 34480a50..0c2a0e46 100644 --- a/src/main/kotlin/sc/gui/view/BoardView.kt +++ b/src/main/kotlin/sc/gui/view/BoardView.kt @@ -136,7 +136,7 @@ class BoardView: View() { } private fun paneHoverEnter(x: Int, y: Int) { - if(gameController.gameEnded()) { + if(gameController.gameEnded.value) { return } @@ -180,12 +180,13 @@ class BoardView: View() { private fun paneFromField(field: Field): HBox { val x = field.coordinates.x val y = field.coordinates.y - val image = BlockImage(controller.board.calculatedBlockSizeProperty()) - image.fitWidthProperty().bind(controller.board.calculatedBlockSizeProperty()) - image.fitHeightProperty().bind(controller.board.calculatedBlockSizeProperty()) - model.boardProperty().addListener { _, oldBoard, newBoard -> - if(oldBoard == null || oldBoard[x, y].content != newBoard[x, y].content) { - image.updateImage(newBoard[x, y].content) + val image = BlockImage(controller.boardModel.calculatedBlockSize) + image.fitWidthProperty().bind(controller.boardModel.calculatedBlockSize) + image.fitHeightProperty().bind(controller.boardModel.calculatedBlockSize) + controller.boardModel.board.addListener { _, oldBoard, newBoard -> + val newContent = newBoard?.let { it[x, y].content } ?: FieldContent.EMPTY + if(oldBoard == null || oldBoard[x, y].content != newContent) { + image.updateImage(newContent) } } diff --git a/src/main/kotlin/sc/gui/view/ControlView.kt b/src/main/kotlin/sc/gui/view/ControlView.kt index 2c014be3..909c1893 100644 --- a/src/main/kotlin/sc/gui/view/ControlView.kt +++ b/src/main/kotlin/sc/gui/view/ControlView.kt @@ -97,9 +97,9 @@ class ControlView : View() { playPauseSkipButton.setOnMouseClicked { when { gameController.canSkip.get() -> { - fire(HumanMoveAction(SkipMove(gameController.currentColor.get()))) + fire(HumanMoveAction(SkipMove(gameController.currentColor.value))) } - gameController.gameEnded() -> { + gameController.gameEnded.value -> { appController.changeViewTo(ViewType.START) gameController.clearGame() } @@ -113,7 +113,7 @@ class ControlView : View() { // When the game is paused externally e.g. when rewinding arrayOf(gameController.currentTurn, gameController.started, gameController.gameResult).forEach { it.addListener { _, _, _ -> - if (gameController.gameEnded()) { + if (gameController.gameEnded.value) { playPauseSkipButton.text = "Spiel beenden" } else { updatePauseState(!gameController.started.value) diff --git a/src/main/kotlin/sc/gui/view/GameView.kt b/src/main/kotlin/sc/gui/view/GameView.kt index 7a31dad1..bb0a6189 100644 --- a/src/main/kotlin/sc/gui/view/GameView.kt +++ b/src/main/kotlin/sc/gui/view/GameView.kt @@ -10,7 +10,7 @@ import sc.plugin2021.Rotation import sc.plugin2021.Team import sc.plugin2021.util.Constants import tornadofx.* -import java.util.* +import java.util.EnumMap class GameView : View() { private val gameController: GameController by inject() @@ -102,7 +102,7 @@ class GameView : View() { val board = find(BoardView::class) board.grid.setMaxSize(size, size) board.grid.setMinSize(size, size) - board.model.calculatedBlockSizeProperty().set(size / Constants.BOARD_SIZE) + board.model.calculatedBlockSize.set(size / Constants.BOARD_SIZE) } init { diff --git a/src/main/kotlin/sc/gui/view/PiecesFragment.kt b/src/main/kotlin/sc/gui/view/PiecesFragment.kt index e6dbb30d..be2c2da3 100644 --- a/src/main/kotlin/sc/gui/view/PiecesFragment.kt +++ b/src/main/kotlin/sc/gui/view/PiecesFragment.kt @@ -1,18 +1,16 @@ package sc.gui.view -import javafx.scene.SnapshotParameters -import javafx.scene.canvas.Canvas import javafx.scene.image.Image import javafx.scene.image.ImageView -import javafx.util.Duration -import sc.gui.GuiApp import sc.gui.controller.* import sc.gui.model.PiecesModel import sc.plugin2021.Color import sc.plugin2021.PieceShape import sc.plugin2021.Rotation -import tornadofx.* -import java.io.File +import tornadofx.Fragment +import tornadofx.hbox +import tornadofx.plusAssign +import tornadofx.tooltip class PiecesFragment(color: Color, shape: PieceShape) : Fragment() { private val boardController: BoardController by inject() @@ -46,7 +44,7 @@ class PiecesFragment(color: Color, shape: PieceShape) : Fragment() { fun updateImage() { val imagePath = "/graphics/blokus/${model.colorProperty().get().name.toLowerCase()}/${model.shapeProperty().get().name.toLowerCase()}.png" - val size = boardController.board.calculatedBlockSizeProperty().get() * 2 + val size = boardController.boardModel.calculatedBlockSize.get() * 2 image.image = Image(PiecesFragment::class.java.getResource(imagePath).toExternalForm(), size, size, true, false) // apply rotation to imageview diff --git a/src/main/kotlin/sc/gui/view/StatusView.kt b/src/main/kotlin/sc/gui/view/StatusView.kt index a0d6fa85..6f048cbd 100644 --- a/src/main/kotlin/sc/gui/view/StatusView.kt +++ b/src/main/kotlin/sc/gui/view/StatusView.kt @@ -47,19 +47,19 @@ class StatusBinding(private val game: GameController) : StringBinding() { ${winner(gameResult)} ${irregularities(gameResult)} """.trimIndent() - } ?: "${game.playerNames.get()?.get(game.currentTeam.get().index) ?: game.currentTeam.get()}, ${game.currentColor.get()} ist dran" + } ?: "${game.currentTeam.value?.index?.let { game.playerNames.value?.get(it) } ?: game.currentTeam.value}, ${game.currentColor.value} ist dran" } } class ScoreBinding(private val game: GameController) : StringBinding() { init { bind(game.currentRound) - bind(game.teamOneScore) - bind(game.teamTwoScore) + bind(game.teamScores) } override fun computeValue(): String { - return "Runde ${game.currentRound.get()} - ${game.teamOneScore.get()} : ${game.teamTwoScore.get()}" + return "Runde ${game.currentRound.get()} - " + + game.teamScores.value?.joinToString(" : ") } } diff --git a/src/main/kotlin/sc/gui/view/UndeployedPiecesFragment.kt b/src/main/kotlin/sc/gui/view/UndeployedPiecesFragment.kt index 968eba20..7377d44e 100644 --- a/src/main/kotlin/sc/gui/view/UndeployedPiecesFragment.kt +++ b/src/main/kotlin/sc/gui/view/UndeployedPiecesFragment.kt @@ -1,6 +1,6 @@ package sc.gui.view -import javafx.beans.property.ObjectProperty +import javafx.beans.value.ObservableValue import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.geometry.Pos @@ -17,8 +17,8 @@ import tornadofx.* class UndeployedPiecesFragment( private val color: Color, - undeployedPieces: ObjectProperty>, - validPieces: ObjectProperty> + undeployedPieces: ObservableValue>, + validPieces: ObservableValue> ) : Fragment() { val controller: GameController by inject() private val boardController: BoardController by inject() @@ -37,6 +37,10 @@ class UndeployedPiecesFragment( font = Font(20.0) } isVisible = false + + visibleProperty().bind(controller.currentTurn.booleanBinding { + it != 0 && !controller.isValidColor(color) + }) } init { @@ -84,7 +88,7 @@ class UndeployedPiecesFragment( } } } - boardController.board.calculatedBlockSizeProperty().addListener { _, _, _ -> + boardController.boardModel.calculatedBlockSize.addListener { _, _, _ -> pieces.forEach { it.value.updateImage() } @@ -107,26 +111,31 @@ class UndeployedPiecesFragment( } } - controller.currentTurn.addListener { _, _, turn -> - unplayableNotice.isVisible = turn != 0 && !controller.isValidColor(color) - } - - validPieces.addListener { _, _, value -> - piecesList.forEach { (piece, box) -> - if(value.contains(piece)) { - box.removeClass(AppStyle.pieceUnselectable) - } else if (!box.hasClass(AppStyle.pieceUnselectable)) { - box.addClass(AppStyle.pieceUnselectable) + arrayOf(validPieces, controller.currentColor).map { it.onChange { + val vp = validPieces.value + if(logger.isTraceEnabled) + logger.trace("$color (current: ${controller.currentColor.value}) can place $vp") + if (controller.currentColor.value == color) { + piecesList.forEach { (piece, box) -> + when { + vp.contains(piece) -> { + box.removeClass(AppStyle.pieceUnselectable) + } + !box.hasClass(AppStyle.pieceUnselectable) -> { + box.addClass(AppStyle.pieceUnselectable) + } + } } - } - if (controller.currentColor.get() == color) { - logger.debug("Current color ${color.name} can place $value") - if (value.isNotEmpty()) { - controller.selectPiece(pieces.filterKeys { it in value }.values.last().model) + logger.debug("Current color ${color.name} can place $vp") + if (vp.isNotEmpty()) { + controller.selectPiece(pieces.filterKeys { it in vp }.values.last().model) } + } else { + piecesList.forEach { (_, box) -> + box.addClass(AppStyle.pieceUnselectable) } } - } + }} } override val root = stackpane {