Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -75,6 +75,10 @@ tasks {
}
}

withType<Jar> {
manifest.attributes["Main-Class"] = application.mainClassName
}

javafx {
version = "13"
modules("javafx.controls", "javafx.fxml", "javafx.base", "javafx.graphics")
Expand Down
37 changes: 19 additions & 18 deletions src/main/kotlin/sc/gui/controller/BoardController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<HumanMoveRequest> { event ->
event.gameState.let {
calculateIsPlaceableBoard(it.board, it.currentColor)
}
}
}

private var isHoverableBoard: Array<Array<Boolean>> = Array(Constants.BOARD_SIZE) { Array(Constants.BOARD_SIZE) { true } }
private var isPlaceableBoard: Array<Array<Boolean>> = Array(Constants.BOARD_SIZE) { Array(Constants.BOARD_SIZE) { false } }

Expand All @@ -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...")
Expand Down Expand Up @@ -72,10 +81,6 @@ class BoardController : Controller() {
}

fun isHoverable(x: Int, y: Int, shape: Set<Coordinates>): 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)) {
Expand All @@ -84,20 +89,16 @@ class BoardController : Controller() {
return false
}
}

return true
}

fun isPlaceable(x: Int, y: Int, shape: Set<Coordinates>): 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
}

Expand Down
104 changes: 48 additions & 56 deletions src/main/kotlin/sc/gui/controller/GameController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -134,29 +133,40 @@ class CalculatedShapeBinding(piece: Property<PiecesModel>) : ObjectBinding<Set<C


class GameController : Controller() {
val boardController: BoardController by inject()

val gameState = objectProperty(GameState())

val availableTurns = objectProperty(0)
val currentTurn = objectProperty(0)
val currentRound = objectProperty(0)
val currentColor = objectProperty(Color.RED)
val currentTeam = objectProperty(Team.ONE)
val gameState = objectProperty<GameState?>(null)
val gameResult = objectProperty<GameResult>()
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<Array<String>>()
val gameResult = objectProperty<GameResult>()

val undeployedPieces: Map<Color, ObjectProperty<Collection<PieceShape>>> = 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<Color, ObservableValue<Collection<PieceShape>>> = EnumMap(
Color.values().associateWith { color ->
nonNullObjectBinding(gameState, gameState) { value?.undeployedPieceShapes(color) ?: PieceShape.values().toList() }
})

val validPieces: Map<Color, ObjectProperty<Collection<PieceShape>>> = EnumMap(
Color.values().associateWith { objectProperty(emptyList()) })
Expand All @@ -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<NewGameState> { 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<HumanMoveRequest> { 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<HumanMoveAction> {
isHumanTurn.set(false)
}
subscribe<GameOverEvent> { 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) {
Expand Down
14 changes: 6 additions & 8 deletions src/main/kotlin/sc/gui/model/BoardModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoardView>() {
private var calculatedBlockSize: Double by property<Double>(16.0)
fun calculatedBlockSizeProperty() = getProperty(BoardModel::calculatedBlockSize)

private var board: Board by property<Board>()
fun boardProperty() = getProperty(BoardModel::board)
val calculatedBlockSize = objectProperty(16.0)
val board = objectProperty<Board>()

init {
calculatedBlockSizeProperty().addListener { _, old, new ->
calculatedBlockSize.addListener { _, old, new ->
logger.debug("Blocksize changed $old -> $new")
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/main/kotlin/sc/gui/view/BoardView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class BoardView: View() {
}

private fun paneHoverEnter(x: Int, y: Int) {
if(gameController.gameEnded()) {
if(gameController.gameEnded.value) {
return
}

Expand Down Expand Up @@ -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)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/sc/gui/view/ControlView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/sc/gui/view/GameView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 5 additions & 7 deletions src/main/kotlin/sc/gui/view/PiecesFragment.kt
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading