• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jakob63 / WizardSE / 21065675640

16 Jan 2026 11:50AM UTC coverage: 0.0%. Remained the same
21065675640

Pull #60

github

web-flow
Merge 402e031d1 into 9cf3957ca
Pull Request #60: File input

0 of 475 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 1452 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
src/main/scala/wizard/aView/aView_GUI/WizardGUI.scala
1
package wizard.aView.aView_GUI
2

3
import scalafx.application.JFXApp3
4
import scalafx.geometry.{Insets, Pos}
5
import scalafx.scene.Scene
6
import scalafx.scene.control.{Button, Label, TextField, Tooltip, TableView, TableColumn}
7
import scalafx.beans.property.{StringProperty, IntegerProperty}
8
import scalafx.collections.ObservableBuffer
9
import scalafx.scene.image.{Image, ImageView}
10
import scalafx.scene.layout.{VBox, StackPane, HBox, BorderPane}
11
import scalafx.scene.layout.StackPane.setAlignment
12
import scalafx.scene.text.{Font, FontWeight}
13
import scalafx.Includes._
14
import scalafx.application.Platform
15
import javafx.beans.binding.Bindings
16
import wizard.controller.{GameLogic, PlayerSnapshot}
17
import wizard.actionmanagement.{Observer, Debug, InputRouter}
18
import wizard.model.player.{PlayerFactory, PlayerType, Player}
19
import wizard.model.cards.{Card, Color, Value}
20
import scalafx.scene.Node
21
import java.util.NoSuchElementException
22

23
class WizardGUI(val gameController: GameLogic) extends JFXApp3 with Observer {
24
  private var rootPane: Option[StackPane] = None
25
  private var undoRedoBar: Option[HBox] = None
26
  private var pendingPlayerCount: Option[Int] = None
27
  private var selectedPlayerCount: Option[Int] = None
28
  private var contentBox: Option[VBox] = None
29
  private var currentScreen: String = "PlayerCount"
30
  private var navEpoch: Int = 0
31
  private var gameRoot: Option[BorderPane] = None
32
  private var trumpView: Option[ImageView] = None
33
  private var handBar: Option[HBox] = None
34
  private var bidOverlay: Option[HBox] = None
35
  private var currentPlayers: List[Player] = Nil
36
  private var scoresTable: Option[TableView[PlayerRow]] = None
37
  private var activePlayerName: Option[String] = None
38
  private var trickBar: Option[HBox] = None
39
  @volatile private var currentTrickCards: List[Card] = Nil
40

41
  case class PlayerRow(nameProp: String, bidProp: String, pointsProp: String) {
42
    val name = new StringProperty(this, "name", nameProp)
×
43
    val bid = new StringProperty(this, "bid", bidProp)
×
44
    val points = new StringProperty(this, "points", pointsProp)
×
45
  }
46
  private val inputFieldStyle: String = "-fx-control-inner-background: #2B2B2B; -fx-text-fill: white; -fx-prompt-text-fill: rgba(255,255,255,0.6);"
47
  private val buttonStyle: String = "-fx-background-color: #2B2B2B; -fx-text-fill: white;"
48
  gameController.add(this)
×
49
  Debug.log("WizardGUI constructed and registered as observer")
×
50

51
  private def hideUndoRedoBar(): Unit = {
×
52
    (for { rp <- rootPane; bar <- undoRedoBar } yield (rp, bar)) foreach { case (rp, bar) =>
×
53
      Platform.runLater {
×
54
        try { rp.children.remove(bar) } catch { case _: Throwable => () }
×
55
      }
56
    }
57
    undoRedoBar = None
58
  }
59
  private def createUndoRedoBar(): HBox = {
×
60
    import wizard.undo.UndoService
61
    val undoBtn = new Button("↶") {
62
      tooltip = Tooltip("Undo")
×
63
      style = buttonStyle + "; -fx-font-size: 10px; -fx-padding: 2 5 2 5;"
×
64
    }
65
    val redoBtn = new Button("↷") {
66
      tooltip = Tooltip("Redo")
×
67
      style = buttonStyle + "; -fx-font-size: 10px; -fx-padding: 2 5 2 5;"
×
68
    }
69
    val saveBtn = new Button("Save Game") {
70
      style = buttonStyle + "; -fx-font-size: 10px; -fx-padding: 2 5 2 5;"
×
71
      onAction = _ => showSaveDialog()
×
72
    }
73
    undoBtn.onAction = _ => {
×
74
      val t = new Thread(new Runnable {
×
75
        override def run(): Unit = {
×
76
          try {
×
77
            if (currentScreen == "PlayerNames") {
×
78
              try { gameController.resetPlayerCountSelection() } catch { case _: Throwable => () }
×
79
            } else {
×
80
              gameController.undo()
×
81
            }
82
          } catch {
83
            case _: Throwable => ()
×
84
          }
85
        }
86
      })
87
      t.setDaemon(true); t.start()
×
88
    }
89
    redoBtn.onAction = _ => {
×
90
      val t = new Thread(new Runnable { override def run(): Unit = try { gameController.redo() } catch { case _: Throwable => () } })
×
91
      t.setDaemon(true); t.start()
×
92
    }
93
    new HBox(6) { alignment = Pos.TopLeft; padding = Insets(8); children = Seq(undoBtn, redoBtn, saveBtn) }
×
94
  }
95
  private def ensureUndoRedoBarVisible(): Unit = {
×
96
    if (undoRedoBar.isEmpty) {
×
97
      val bar = createUndoRedoBar()
×
98
      try { bar.pickOnBounds = false } catch { case _: Throwable => () }
×
99
      undoRedoBar = Some(bar)
100
    }
×
101
    
102
    undoRedoBar.foreach { bar =>
×
103
      rootPane.foreach { rp =>
×
104
        Platform.runLater {
×
105
          val kids = rp.children
×
106
          if (!kids.contains(bar)) {
×
107
            try {
×
108
              val oldParent = bar.delegate.getParent
×
109
              if (oldParent != null && oldParent.isInstanceOf[javafx.scene.layout.Pane]) {
×
110
                oldParent.asInstanceOf[javafx.scene.layout.Pane].getChildren.remove(bar.delegate)
×
111
              }
×
112
            } catch { case _: Throwable => () }
×
113
            
114
            rp.children.add(bar)
×
115
            setAlignment(bar, Pos.TopLeft)
×
116
          }
×
117
          bar.toFront()
×
118
        }
119
      }
120
    }
121
  }
122

123
  private def goBackToPlayerCountLocal(): Unit = {
×
124
    navEpoch += 1 
125
    val thisEpoch = navEpoch
126
    Platform.runLater {
×
127
      Debug.log(s"WizardGUI.localBackToPlayerCount at epoch=$thisEpoch")
×
128
      currentScreen = "PlayerCount"
129
      contentBox match {
130
        case Some(box) =>
×
131
          box.children = buildPlayerCountChildren(box)
×
132
        case None =>
×
133
          if (stage != null && stage.scene() != null) {
×
134
            stage.scene().root = createInitialScreen()
×
135
          }
×
136
      }
137
    }
138
  }
139

140
  private def showSaveDialog(): Unit = {
×
141
    Platform.runLater {
×
142
      var saveBoxRef: VBox = null
143
      saveBoxRef = new VBox(10) {
144
        alignment = Pos.Center
×
145
        padding = Insets(20)
×
146
        style = "-fx-background-color: rgba(0,0,0,0.8); -fx-background-radius: 10;"
×
147
        maxWidth = 300
×
148
        maxHeight = 200
×
149
        children = Seq(
×
150
          new Label("Game title") { style = "-fx-text-fill: white; -fx-font-size: 16px;" },
×
151
          new TextField {
×
152
            id = "saveTitleField"
×
153
            promptText = "Enter title..."
×
154
            style = inputFieldStyle
×
155
            onAction = _ => {
×
156
              val title = this.text.value
×
157
              if (title.nonEmpty) {
×
158
                gameController.save(title)
×
159
                rootPane.foreach(_.children.remove(saveBoxRef))
×
160
              }
×
161
            }
162
          },
163
          new HBox(10) {
164
            alignment = Pos.Center
×
165
            children = Seq(
×
166
              new Button("Save") {
167
                style = buttonStyle
×
168
                onAction = _ => {
×
169
                  val field = saveBoxRef.lookup("#saveTitleField")
×
170
                  val title = (field: Any) match {
171
                    case tf: javafx.scene.control.TextField => tf.getText
×
172
                    case sn: scalafx.scene.Node if sn.delegate.isInstanceOf[javafx.scene.control.TextField] => 
×
173
                      sn.delegate.asInstanceOf[javafx.scene.control.TextField].getText
×
174
                    case _ => ""
×
175
                  }
176
                  if (title != null && title.nonEmpty) {
×
177
                    gameController.save(title)
×
178
                    rootPane.foreach(_.children.remove(saveBoxRef))
×
179
                  }
×
180
                }
181
              },
182
              new Button("Cancel") {
183
                style = buttonStyle
×
184
                onAction = _ => rootPane.foreach(_.children.remove(saveBoxRef))
×
185
              }
186
            )
187
          }
188
        )
189
      }
190
      rootPane.foreach(_.children.add(saveBoxRef))
×
191
    }
192
  }
193

194
  override def start(): Unit = {
×
195
    Debug.log("WizardGUI.start -> creating stage and initial screen")
×
196
    stage = new JFXApp3.PrimaryStage {
197
      title = "Wizard Card Game"
×
198
      width = 600
×
199
      height = 400
×
200
      scene = new Scene {
×
201
        root = createInitialScreen()
×
202
      }
203
    }
204
    gameController.add(this)
×
205
    Debug.log("WizardGUI.start -> ensured observer registration and starting controller")
×
206
    val controllerThread = new Thread(new Runnable { override def run(): Unit = gameController.start() })
×
207
    controllerThread.setDaemon(true)
×
208
    controllerThread.start()
×
209
    pendingPlayerCount.foreach { cnt =>
×
210
      Platform.runLater {
×
211
        val stageReady = stage != null && stage.scene() != null
×
212
        if (stageReady) {
×
213
          contentBox match {
214
            case Some(box) =>
×
215
              box.children = createPlayerNameScreen(cnt)
×
216
            case None =>
×
217
              stage.scene().root = createPlayerNameRoot(cnt)
×
218
          }
219
          Debug.log(s"WizardGUI.start -> applied pending PlayerCountSelected($cnt)")
×
220
          pendingPlayerCount = None
221
        }
×
222
      }
223
    }
224
  }
225

226
  private def buildPlayerCountChildren(ui: VBox): List[scalafx.scene.Node] = {
×
227
    val titleLabel = new Label("Willkommen bei Wizard") {
228
      font = Font.font(null, FontWeight.Bold, 24)
×
229
      style = "-fx-text-fill: #39FF14;" 
×
230
    }
231

232
    val playerCountLabel = new Label("Spieleranzahl (3-6)") {
233
      font = Font.font(20)
×
234
      style = "-fx-text-fill: black;"
×
235
      translateY = -12
×
236
    }
237

238
    val playerCountField = new TextField() {
×
239
      alignment = Pos.Center
×
240
      style = inputFieldStyle
×
241
    }
242

243
    val nextButton = new Button("Weiter") {
244
      style = buttonStyle
×
245
    }
246

247
    playerCountField.prefWidth <== ui.width * 0.3
×
248
    nextButton.prefWidth       <== ui.width * 0.3
×
249
    playerCountField.maxWidth  <== playerCountField.prefWidth
×
250
    nextButton.maxWidth        <== nextButton.prefWidth
×
251

252
    titleLabel.style <== Bindings.createStringBinding(
×
253
      () => s"-fx-text-fill: #39FF14; -fx-font-weight: bold; -fx-font-size: ${ui.width.value / 25}px;",
×
254
      ui.width
×
255
    )
256
    playerCountLabel.style <== Bindings.createStringBinding(
×
257
      () => s"-fx-text-fill: black; -fx-font-size: ${ui.width.value / 35}px;",
×
258
      ui.width
×
259
    )
260
    playerCountField.style <== Bindings.createStringBinding(
×
261
      () => s"$inputFieldStyle -fx-font-size: ${ui.width.value / 40}px;",
×
262
      ui.width
×
263
    )
264
    nextButton.style <== Bindings.createStringBinding(
×
265
      () => s"$buttonStyle -fx-font-size: ${ui.width.value / 40}px;",
×
266
      ui.width
×
267
    )
268

269
    nextButton.onAction = _ => {
×
270
      val playerCount = playerCountField.text.value
×
271
      if (playerCount.matches("[3-6]")) {
×
272
        val t = new Thread(new Runnable { override def run(): Unit = gameController.playerCountSelected(playerCount.toInt) })
×
273
        t.setDaemon(true)
×
274
        t.start()
×
275
        ui.children = createPlayerNameScreen(playerCount.toInt)
×
276
        currentScreen = "PlayerNames"
277
        ensureUndoRedoBarVisible()
×
278
      }
×
279
    }
280

281
    val resumeButton = new Button("Resume Game") {
282
      style = buttonStyle
×
283
      onAction = _ => showResumeDialog()
×
284
    }
285

286
    List(titleLabel, playerCountLabel, playerCountField, nextButton, resumeButton)
×
287
  }
288

289
  private def showResumeDialog(): Unit = {
×
290
    Platform.runLater {
×
291
      var resumeBoxRef: VBox = null
292
      resumeBoxRef = new VBox(10) {
293
        alignment = Pos.Center
×
294
        padding = Insets(20)
×
295
        style = "-fx-background-color: rgba(0,0,0,0.8); -fx-background-radius: 10;"
×
296
        maxWidth = 300
×
297
        maxHeight = 200
×
298
        children = Seq(
×
299
          new Label("Resume Game") { style = "-fx-text-fill: white; -fx-font-size: 16px;" },
×
300
          new Label("Enter save name:") { style = "-fx-text-fill: white;" },
×
301
          new TextField {
×
302
            id = "resumeTitleField"
×
303
            promptText = "Enter title..."
×
304
            style = inputFieldStyle
×
305
            onAction = _ => {
×
306
              val title = this.text.value
×
307
              if (title.nonEmpty) {
×
308
                gameController.load(title)
×
309
                rootPane.foreach(_.children.remove(resumeBoxRef))
×
310
              }
×
311
            }
312
          },
313
          new HBox(10) {
314
            alignment = Pos.Center
×
315
            children = Seq(
×
316
              new Button("Resume") {
317
                style = buttonStyle
×
318
                onAction = _ => {
×
319
                  val field = resumeBoxRef.lookup("#resumeTitleField")
×
320
                  val title = (field: Any) match {
321
                    case tf: javafx.scene.control.TextField => tf.getText
×
322
                    case sn: scalafx.scene.Node if sn.delegate.isInstanceOf[javafx.scene.control.TextField] => 
×
323
                      sn.delegate.asInstanceOf[javafx.scene.control.TextField].getText
×
324
                    case _ => ""
×
325
                  }
326
                  if (title != null && title.nonEmpty) {
×
327
                    gameController.load(title)
×
328
                    rootPane.foreach(_.children.remove(resumeBoxRef))
×
329
                  }
×
330
                }
331
              },
332
              new Button("Cancel") {
333
                style = buttonStyle
×
334
                onAction = _ => rootPane.foreach(_.children.remove(resumeBoxRef))
×
335
              }
336
            )
337
          }
338
        )
339
      }
340
      rootPane.foreach(_.children.add(resumeBoxRef))
×
341
    }
342
  }
343

344
  private def createInitialScreen(): StackPane = {
×
345
    val ui = new VBox(20) {
346
      alignment = Pos.Center
×
347
      padding = Insets(20)
×
348
    }
349
    contentBox = Some(ui)
350
    currentScreen = "PlayerCount"
351

352
    ui.children = buildPlayerCountChildren(ui)
×
353

354
    val bgRes = getClass.getResource("/images/Wizard_game_background2_GUI.png")
×
355
    val root = new StackPane
×
356
    rootPane = Some(root)
357
    if (bgRes != null) {
×
358
      val bgView = new ImageView(new Image(bgRes.toExternalForm)) { preserveRatio = false }
×
359
      root.children = Seq(bgView, ui)
×
360
      bgView.fitWidth  <== root.width
×
361
      bgView.fitHeight <== root.height
×
362
    } else {
×
363
      root.children = Seq(ui)
×
364
    }
365

366
    ui.prefWidth <== root.width * 0.8
×
367
    ui.maxWidth  <== root.width * 0.8
×
368
    ui.spacing   <== root.height * 0.03
×
369

370
    ensureUndoRedoBarVisible()
×
371

372
    root
373
  }
374

375
  private def createPlayerNameRoot(playerCount: Int): StackPane = {
×
376
    val ui = new VBox(20) {
377
      alignment = Pos.Center
×
378
      padding = Insets(20)
×
379
    }
380

381
    contentBox = Some(ui)
382
    currentScreen = "PlayerNames"
383
    ui.children = createPlayerNameScreen(playerCount)
×
384

385
    val bgRes = getClass.getResource("/images/Wizard_game_background2_GUI.png")
×
386
    val root = new StackPane
×
387
    rootPane = Some(root)
388
    if (bgRes != null) {
×
389
      val bgView = new ImageView(new Image(bgRes.toExternalForm)) { preserveRatio = false }
×
390
      root.children = Seq(bgView, ui)
×
391
      bgView.fitWidth <== root.width
×
392
      bgView.fitHeight <== root.height
×
393
    } else {
×
394
      root.children = Seq(ui)
×
395
    }
396

397
    ensureUndoRedoBarVisible()
×
398

399
    ui.prefWidth <== root.width * 0.8
×
400
    ui.maxWidth  <== root.width * 0.8
×
401
    ui.spacing   <== root.height * 0.03
×
402

403
    root
404
  }
405

406
  private def createPlayerNameScreen(playerCount: Int): List[scalafx.scene.Node] = {
×
407
    val titleLabel = new Label("Spielernamen:") {
408
      font = Font.font(null, FontWeight.Bold, 20)
×
409
    }
410

411
    val playerFields = (1 to playerCount).map { i =>
×
412
      new TextField() {
×
413
        promptText = s"Spieler $i"
×
414
        style = inputFieldStyle
×
415
      }
416
    }.toList
×
417

418
    val startButton = new Button("START") {
419
      style = buttonStyle
×
420
    }
421

422
    contentBox.foreach { box =>
×
423
      box.spacing <== box.height * 0.03
×
424
      val compWidth = box.width * 0.35
×
425
      playerFields.foreach { tf =>
×
426
        tf.prefWidth <== compWidth
×
427
        tf.maxWidth  <== tf.prefWidth
×
428
      }
429
      startButton.prefWidth <== compWidth
×
430
      startButton.maxWidth  <== startButton.prefWidth
×
431

432
      titleLabel.style <== Bindings.createStringBinding(
×
433
        () => s"-fx-font-weight: bold; -fx-font-size: ${box.width.value / 30}px;",
×
434
        box.width
×
435
      )
436
      playerFields.foreach(_.style <== Bindings.createStringBinding(
×
437
        () => s"$inputFieldStyle -fx-font-size: ${box.width.value / 40}px;",
×
438
        box.width
×
439
      ))
440
      startButton.style <== Bindings.createStringBinding(
×
441
        () => s"$buttonStyle -fx-font-size: ${box.width.value / 40}px;",
×
442
        box.width
×
443
      )
444
    }
445

446
    startButton.onAction = _ => {
×
447
      val playerNames = playerFields.map(_.text.value.trim).filter(_.nonEmpty)
×
448
      if (playerNames.length == playerCount) {
×
449
        val players = playerNames.map(name => PlayerFactory.createPlayer(Some(name), PlayerType.Human))
×
450
        gameController.setPlayers(players)
×
451
      }
×
452
    }
453

454
    List(titleLabel) ++ playerFields :+ startButton
×
455
  }
456

457
  private def updateScores(snapshot: Option[List[Any]] = None): Unit = {
×
458
    (scoresTable, Option(currentPlayers)) match {
×
459
      case (Some(table), Some(players)) =>
460
        val rows = snapshot match {
×
461
            case Some(list) => 
×
462
                list.map {
×
463
                    case p: Player => 
464
                        val displayBid = if (p.roundBids == -1) "0" else p.roundBids.toString
×
465
                        PlayerRow(p.name, displayBid, p.points.toString)
466
                    case s: PlayerSnapshot => 
467
                        val displayBid = if (s.roundBids == -1) "0" else s.roundBids.toString
×
468
                        PlayerRow(s.name, displayBid, s.points.toString)
469
                    case _ => PlayerRow("?", "?", "?")
×
470
                }
471
            case None =>
×
472
                players.map { p =>
×
473
                    val displayBid = if (p.roundBids == -1) "0" else p.roundBids.toString
×
474
                    PlayerRow(p.name, displayBid, p.points.toString)
475
                }
476
        }
477
        table.items = ObservableBuffer.from(rows)
×
478
        table.refresh()
×
479
      case _ => ()
×
480
    }
481
  }
482

483
  private def updateCurrentBids(p: Player): Unit = {
×
484
    updateScores()
×
485
  }
486

487
  private def ensureGameTableRoot(): Unit = {
×
488
    if (gameRoot.isDefined) return
×
489

490
    val trump = new ImageView() { preserveRatio = true }
×
491
    val hand = new HBox(10) { alignment = Pos.Center; padding = Insets(10) }
×
492

493
    val centerLabel = new Label("") 
×
494
    val trickBox = new HBox(10) { alignment = Pos.Center }
×
495
    trickBar = Some(trickBox)
496

497
    val table = new TableView[PlayerRow]() {
×
498
      columns ++= List(
×
499
        new TableColumn[PlayerRow, String] {
×
500
          text = "Name"
×
501
          cellValueFactory = { _.value.name }
×
502
          prefWidth = 100
×
503
        },
504
        new TableColumn[PlayerRow, String] {
×
505
          text = "Bid"
×
506
          cellValueFactory = { _.value.bid }
×
507
          prefWidth = 50
×
508
        },
509
        new TableColumn[PlayerRow, String] {
×
510
          text = "Pkt"
×
511
          cellValueFactory = { _.value.points }
×
512
          prefWidth = 50
×
513
        }
514
      )
515
      prefHeight = 150
×
516
      maxWidth = 210
×
517
      columnResizePolicy = TableView.ConstrainedResizePolicy
×
518
      selectionModel().cellSelectionEnabled = false
×
519
      selectionModel().selectionMode = javafx.scene.control.SelectionMode.SINGLE // Selection will be hidden via CSS anyway
×
520
      
521
      rowFactory = { _ =>
×
522
        val row = new javafx.scene.control.TableRow[PlayerRow]()
×
523
        row.itemProperty().addListener((_, _, newItem) => {
×
524
          if (newItem != null && activePlayerName.contains(newItem.name.value)) {
×
525
            if (!row.getStyleClass.contains("current-player-row")) {
×
526
              row.getStyleClass.add("current-player-row")
×
527
            }
×
528
          } else {
×
529
            row.getStyleClass.remove("current-player-row")
×
530
          }
531
        })
532
        row
×
533
      }
534

535
      style = """
×
536
        -fx-background-color: transparent;
537
        -fx-control-inner-background: transparent;
538
        -fx-table-cell-border-color: transparent;
539
        -fx-table-header-border-color: transparent;
540
        -fx-padding: 0;
541
      """
542
    }
543
    val cssPath = getClass.getResource("/table_transparent.css")
×
544
    if (cssPath != null) {
×
545
      table.stylesheets.add(cssPath.toExternalForm)
×
546
    }
×
547
    scoresTable = Some(table)
548

549
    val tablePane = new BorderPane {
×
550
      top = new HBox { alignment = Pos.Center; padding = Insets(10); children = Seq(trump) }
×
551
      center = new StackPane { children = Seq(centerLabel, trickBox) }
×
552
      right = new VBox(20) { alignment = Pos.TopCenter; padding = Insets(-20, 10, 10, 10); children = Seq(table) }
×
553
      bottom = hand
×
554
      padding = Insets(10)
×
555
    }
556

557
    trumpView = Some(trump)
558
    handBar = Some(hand)
559

560
    currentScreen = "Game"
561

562
    val bgRes = getClass.getResource("/images/Wizard_game_background2_GUI.png")
×
563
    val root = new StackPane
×
564
    rootPane = Some(root)
565
    if (bgRes != null) {
×
566
      val bgView = new ImageView(new Image(bgRes.toExternalForm)) { preserveRatio = false }
×
567
      root.children = Seq(bgView, tablePane)
×
568
      bgView.fitWidth  <== root.width
×
569
      bgView.fitHeight <== root.height
×
570
    } else {
×
571
      root.children = Seq(tablePane)
×
572
    }
573

574
    ensureUndoRedoBarVisible()
×
575

576
    gameRoot = Some(tablePane)
577

578
    Platform.runLater {
×
579
      if (stage != null && stage.scene() != null) stage.scene().root = root
×
580
    }
581
  }
582

583
  private def setTrump(card: Card): Unit = {
×
584
    ensureGameTableRoot()
×
585
    val url = cardToImageUrl(card)
×
586
    trumpView.foreach { iv =>
×
587
      if (url != null) iv.image = new Image(url) else iv.image = null
×
588
      iv.fitHeight = 120
×
589
    }
590
  }
591

592
  private def renderHand(player: Player): Unit = {
×
593
    ensureGameTableRoot()
×
594
    val bar = handBar.get
×
595
    val images = player.hand.cards.zipWithIndex.map { case (c, idx) =>
×
596
      val iv = new ImageView()
×
597
      val url = cardToImageUrl(c)
×
598
      if (url != null) iv.image = new Image(url)
×
599
      iv.fitHeight = 140
×
600
      iv.preserveRatio = true
×
601
      iv.onMouseClicked = _ => {
×
602
        InputRouter.offer((idx + 1).toString)
×
603
        bar.children.clear()
×
604
      }
605
      iv
606
    }
607
    bar.children = images
×
608
  }
609

610
  private def renderTrick(): Unit = {
×
611
    ensureGameTableRoot()
×
612
    Debug.log(s"WizardGUI.renderTrick -> cards: ${currentTrickCards.size}")
×
613
    trickBar.foreach { bar =>
×
614
      val images = currentTrickCards.map { card =>
×
615
        val iv = new ImageView()
×
616
        val url = cardToImageUrl(card)
×
617
        if (url != null) iv.image = new Image(url)
×
618
        iv.fitHeight = 160
×
619
        iv.preserveRatio = true
×
620
        iv
621
      }
622
      bar.children = images
×
623
    }
624
  }
625

626
  private def showBidPrompt(player: Player): Unit = {
×
627
    ensureGameTableRoot()
×
628
    for { hb <- handBar; ov <- bidOverlay } yield hb.children.remove(ov)
×
629

630
    val tf = new TextField() { promptText = s"${player.name}: Stichzahl"; style = inputFieldStyle }
×
631
    val ok = new Button("OK") { style = buttonStyle }
×
632
    val overlay = new HBox(10) { alignment = Pos.Center; children = Seq(tf, ok) }
×
633
    
634
    ok.onAction = _ => {
×
635
      val text = tf.text.value
×
636
      if (text != null && text.matches("\\d+")) {
×
637
        InputRouter.offer(text)
×
638
        handBar.foreach(_.children.clear())
×
639
        bidOverlay = None
640
      }
×
641
    }
642
    handBar.foreach { hb => hb.children.insert(0, overlay) }
×
643
    bidOverlay = Some(overlay)
644
  }
645

646
  private def cardToImageUrl(card: Card): String = {
×
647
    val base = "/images/cards/"
648
    val name = card.value match {
649
      case Value.WizardKarte => "Wizard.png"
×
650
      case Value.Chester => "Jester.png"
×
651
      case v => s"${card.color.toString}_${v.cardType()}.png"
×
652
    }
653
    val res = getClass.getResource(base + name)
×
654
    if (res != null) res.toExternalForm else null
×
655
  }
656

657
  private def switchToPlayerNames(count: Int, capturedEpoch: Int): Unit = {
×
658
    if (capturedEpoch != navEpoch) {
×
659
      Debug.log(s"WizardGUI.switchToPlayerNames skipped due to stale epoch (captured=$capturedEpoch, current=$navEpoch)")
×
660
      return
661
    }
×
662
    Debug.log(s"WizardGUI.switchToPlayerNames applying for count=$count at epoch=$capturedEpoch")
×
663
    contentBox match {
664
      case Some(box) =>
665
        box.children = createPlayerNameScreen(count)
×
666
        currentScreen = "PlayerNames"
667
        ensureUndoRedoBarVisible()
×
668
      case None =>
×
669
        if (stage != null && stage.scene() != null) {
×
670
          stage.scene().root = createPlayerNameRoot(count)
×
671
        }
×
672
    }
673
  }
674

675
  override def update(updateMSG: String, obj: Any*): Any = {
×
676
    Debug.log(s"WizardGUI.update('$updateMSG') received on JavaFX?=${Platform.isFxApplicationThread}")
×
677
    updateMSG match {
678
      case "AskForPlayerCount" =>
679
        navEpoch += 1
×
680
        val thisEpoch = navEpoch
681
        Platform.runLater {
×
682
          Debug.log(s"WizardGUI.update -> handling AskForPlayerCount at epoch=$thisEpoch")
×
683
          currentScreen = "PlayerCount"
684
          contentBox match {
685
            case Some(box) =>
×
686
              box.children = buildPlayerCountChildren(box)
×
687
            case None =>
×
688
              if (stage != null && stage.scene() != null) {
×
689
                stage.scene().root = createInitialScreen()
×
690
              }
×
691
          }
692
          ensureUndoRedoBarVisible()
×
693
        }
694
        ()
695
      case "AskForPlayerNames" =>
696
        Platform.runLater {
×
697
          Debug.log("WizardGUI.update -> handling AskForPlayerNames")
×
698
          gameRoot = None 
699
          currentScreen = "PlayerNames"
700
          val count = selectedPlayerCount.getOrElse(3)
×
701
          if (stage != null && stage.scene() != null) {
×
702
            stage.scene().root = createPlayerNameRoot(count)
×
703
          }
×
704
          ensureUndoRedoBarVisible()
×
705
        }
706
        ()
707
      case "PlayerCountSelected" =>
708
        val count = obj.headOption match {
×
709
          case Some(pcs: wizard.actionmanagement.PlayerCountSelected) => pcs.count
×
710
          case Some(i: Int) => i
×
711
          case _ => 0
×
712
        }
713
        Debug.log(s"WizardGUI.update -> PlayerCountSelected($count)")
×
714
        if (count >= 3 && count <= 6) {
×
715
          selectedPlayerCount = Some(count)
716
          if (stage == null || stage.scene() == null) {
×
717
            Debug.log("WizardGUI.update -> stage not ready, buffering player count")
×
718
            pendingPlayerCount = Some(count)
719
          } else {
×
720
            val epoch = navEpoch
721
            Platform.runLater {
×
722
              Debug.log("WizardGUI.update -> attempting switch to player name screen")
×
723
              switchToPlayerNames(count, epoch)
×
724
              ensureUndoRedoBarVisible()
×
725
            }
726
          }
727
        } else {
×
728
          ()
729
        }
730
        ()
731
      case "CardsDealt" =>
732
        obj.headOption.collect { case cd: wizard.actionmanagement.CardsDealt => cd.players }.foreach { ps => currentPlayers = ps }
×
733
        Platform.runLater({
×
734
          ensureGameTableRoot()
×
735
          updateScores()
×
736
          renderTrick()
×
737
          ensureUndoRedoBarVisible()
×
738
        })
739
        ()
740
      case "card played" =>
741
        obj.headOption.collect { case c: Card => c }.foreach { card =>
×
742
          Debug.log(s"WizardGUI.update('card played') -> $card")
×
743
          Platform.runLater({
×
744
            if (!currentTrickCards.contains(card)) {
×
745
              currentTrickCards = currentTrickCards :+ card
×
746
            }
×
747
            renderTrick()
×
748
            
749
            handBar.foreach(_.children.clear())
×
750
          })
751
        }
752
        ()
753
      case "print trump card" =>
754
        obj.headOption.collect { case c: Card => c }.foreach { c => Platform.runLater(setTrump(c)) }
×
755
        ()
756
      case "ShowHand" =>
757
        val playerOpt: Option[Player] = obj.headOption match {
×
758
          case Some(sh: wizard.actionmanagement.ShowHand) => Some(sh.player)
×
759
          case Some(p: Player) => Some(p)
×
760
          case _ => None
×
761
        }
762
        playerOpt.foreach(p => Platform.runLater({
×
763
          activePlayerName = Some(p.name)
764
          updateCurrentBids(p)
×
765
          
766
          handBar.foreach { bar =>
×
767
            bar.children.clear()
×
768
            val nextBtn = new Button("Next Player: " + p.name) {
×
769
              style = buttonStyle
×
770
              onAction = _ => {
×
771
                gameController.setCanSave(false)
×
772
                renderHand(p)
×
773
              }
774
            }
775
            bar.children = Seq(nextBtn)
×
776
          }
777
        }))
778
        ()
779
      case "which card" =>
780
        obj.headOption.collect { case p: Player => p }.foreach { p => Platform.runLater({
×
781
          val targetText = "Next Player: " + p.name
×
782
          Debug.log(s"WizardGUI.update('which card') for ${p.name}. target: $targetText")
×
783
          activePlayerName = Some(p.name)
784
          updateCurrentBids(p)
×
785
          
786
          handBar.foreach { bar =>
×
787
            val currentlyShowingNext = bar.children.exists(node => node.isInstanceOf[javafx.scene.control.Button] && node.asInstanceOf[javafx.scene.control.Button].getText.startsWith("Next Player"))
×
788
            val showingTarget = bar.children.exists(node => node.isInstanceOf[javafx.scene.control.Button] && node.asInstanceOf[javafx.scene.control.Button].getText == targetText)
×
789
            
790
            if (!showingTarget) {
×
791
                Debug.log(s"WizardGUI -> showing Next Player button for ${p.name}")
×
792
                bar.children.clear()
×
793
                val nextBtn = new Button(targetText) {
794
                  style = buttonStyle
×
795
                  onAction = _ => {
×
796
                    gameController.setCanSave(false)
×
797
                    renderHand(p)
×
798
                  }
799
                }
800
                bar.children = Seq(nextBtn)
×
801
            } else {
×
802
                Debug.log(s"WizardGUI -> already showing button for ${p.name}")
×
803
            }
804
          }
805
        }) }
806
        ()
807
      case "which bid" =>
808
        obj.headOption.collect { case p: Player => p }.foreach { p =>
×
809
          Platform.runLater({
×
810
            activePlayerName = Some(p.name)
811
            updateCurrentBids(p)
×
812
            
813
            handBar.foreach { bar =>
×
814
              bar.children.clear()
×
815
              val nextBtn = new Button("Next Player: " + p.name) {
×
816
                style = buttonStyle
×
817
                onAction = _ => {
×
818
                  gameController.setCanSave(false)
×
819
                  renderHand(p)
×
820
                  showBidPrompt(p)
×
821
                }
822
              }
823
              bar.children = Seq(nextBtn)
×
824
            }
825
          })
826
        }
827
        ()
828
      case "TrickUpdated" =>
829
        obj.headOption.map(_.asInstanceOf[List[Card]]).foreach { cards =>
×
830
          Debug.log(s"WizardGUI.update('TrickUpdated') -> ${cards.size} cards: ${cards.mkString(", ")}")
×
831
          Platform.runLater({
×
832
            currentTrickCards = cards
833
            renderTrick()
×
834
          })
835
        }
836
        ()
837
      case "LoadFailed" =>
838
        val title = obj.headOption.collect { case s: String => s }.getOrElse("Unknown")
×
839
        Platform.runLater {
×
840
          val alertBox = new VBox(10) {
841
            alignment = Pos.Center
×
842
            padding = Insets(20)
×
843
            style = "-fx-background-color: rgba(200,0,0,0.9); -fx-background-radius: 10;"
×
844
            maxWidth = 250
×
845
            children = Seq(
×
846
              new Label(s"'$title' not found") { style = "-fx-text-fill: white; -fx-font-weight: bold;" },
×
847
              new Button("OK") {
848
                style = buttonStyle
×
849
                onAction = _ => rootPane.foreach(_.children.remove(this.parent.value))
×
850
              }
851
            )
852
          }
853
          rootPane.foreach(_.children.add(alertBox))
×
854
        }
855
        ()
856
      case "SaveNotAllowed" =>
857
        Platform.runLater {
×
858
          val alertBox = new VBox(2) {
859
            alignment = Pos.Center
×
860
            padding = Insets(2, 10, 2, 10)
×
861
            style = "-fx-background-color: rgba(0,0,0,0.8); -fx-background-radius: 10; -fx-border-color: white; -fx-border-radius: 10;"
×
862
            maxWidth = 200
×
863
            children = Seq(
×
864
              new Label("Bitte spiele diese Runde erst zuende.") { 
865
                  style = "-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 11;" 
×
866
                  wrapText = true
×
867
                  alignment = Pos.Center
×
868
              },
869
              new Button("OK") {
870
                style = buttonStyle + "; -fx-font-size: 10px; -fx-padding: 2 5 2 5;"
×
871
                onAction = _ => rootPane.foreach(_.children.remove(this.parent.value))
×
872
              }
873
            )
874
          }
875
          rootPane.foreach { rp =>
×
876
            StackPane.setAlignment(alertBox, Pos.Center)
×
877
            rp.children.add(alertBox)
×
878
          }
879
        }
880
        ()
881
      case "GameLoaded" =>
882
        obj.headOption.collect { case g: wizard.model.Game => g }.foreach { game =>
×
883
          Platform.runLater({
×
884
            currentPlayers = game.players
885
            currentTrickCards = game.currentTrick
886
            ensureGameTableRoot()
×
887
            updateScores()
×
888
            renderTrick()
×
889
            ensureUndoRedoBarVisible()
×
890
          })
891
        }
892
        ()
893
      case "UndoPerformed" | "RedoPerformed" =>
894
        Platform.runLater({
×
895
          updateScores()
×
896
          if (currentScreen == "Game") {
×
897
            currentPlayers.find(_.name == activePlayerName.getOrElse("")).foreach { p =>
×
898
              renderHand(p)
×
899
            }
900
          }
×
901
        })
902
        ()
903
      case "print points all players" =>
904
        val snapshot = obj.headOption.collect { case l: List[Any] => l }
×
905
        Platform.runLater {
×
906
          updateScores(snapshot)
×
907
        }
908
        ()
909
      case _ => ()
×
910
    }
911
  }
912

913
  private[aView] def testBuildPlayerNameRoot(count: Int): StackPane = createPlayerNameRoot(count)
×
914
  private[aView] def testUndoRedoBarPresent: Boolean = undoRedoBar.isDefined
×
915
  private[aView] def testContentBoxRef: AnyRef = contentBox.orNull
×
916
  private[aView] def testCurrentScreen: String = currentScreen
×
917
  private[aView] def testGetNavEpoch: Int = navEpoch
×
918
  private[aView] def testSwitchToPlayerNames(count: Int, capturedEpoch: Int): Unit = switchToPlayerNames(count, capturedEpoch)
×
919
  private[aView] def testSimulateUndoFromNames(): Unit = {
×
920
    if (currentScreen == "PlayerNames") {
×
921
      currentScreen = "PlayerCount"
922
      contentBox.foreach { box =>
×
923
        box.children = buildPlayerCountChildren(box)
×
924
      }
925
    }
×
926
  }
927
  private[aView] def testLocalBackToPlayerCount(): Unit = goBackToPlayerCountLocal()
×
928
}
929

930
object WizardGUI {
931
  def main(args: Array[String]): Unit = {
×
932
    val controller = new GameLogic
×
933
    val app = new WizardGUI(controller)
×
934
    app.main(args)
×
935
  }
936
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc