Code Sketch


Bixuu spring season event 9
By: Mhalsakant School
Category: Programming
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Font
import java.awt.GradientPaint
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Polygon
import java.awt.RenderingHints
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import javax.swing.AbstractAction
import javax.swing.BorderFactory
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTextField
import javax.swing.JComponent
import javax.swing.KeyStroke
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.WindowConstants
import scala.collection.mutable.ArrayBuffer
import scala.util.Random

final case class HorsemanCandy(x: Double, y: Double, kind: Int)

class HorsemanLanPeer(
  hostMode: Boolean,
  hostAddress: String,
  port: Int,
  localPacket: () => String,
  receivePacket: String => Unit,
  reportStatus: String => Unit
) {
  @volatile private var running = true
  @volatile private var serverSocket: ServerSocket = null
  @volatile private var peerSocket: Socket = null

  def start(): Unit = {
    val worker = new Thread(new Runnable {
      override def run(): Unit = {
        try {
          if (hostMode) runHost() else runClient()
        } catch {
          case ex: Exception =>
            if (running) reportStatus("LAN unavailable: " + ex.getMessage + ". Continuing locally.")
        }
      }
    }, "Horseman-LAN")
    worker.setDaemon(true)
    worker.start()
  }

  private def runHost(): Unit = {
    serverSocket = new ServerSocket(port)
    reportStatus("LAN Host ready. Ask the other player to Join using your local IP, port " + port + ".")
    peerSocket = serverSocket.accept()
    peerSocket.setSoTimeout(5000)
    peerSocket.setTcpNoDelay(true)
    communicateAsHost(peerSocket)
  }

  private def communicateAsHost(socket: Socket): Unit = {
    val input = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
    val output = new PrintWriter(new OutputStreamWriter(socket.getOutputStream, "UTF-8"), true)
    reportStatus("LAN connected: player position sharing is active.")
    var line = input.readLine()
    while (running && line != null) {
      receivePacket(line)
      output.println(localPacket())
      line = input.readLine()
    }
    if (running) reportStatus("LAN player disconnected. You can keep playing solo.")
  }

  private def runClient(): Unit = {
    peerSocket = new Socket()
    peerSocket.connect(new InetSocketAddress(hostAddress, port), 5000)
    peerSocket.setSoTimeout(5000)
    peerSocket.setTcpNoDelay(true)
    val input = new BufferedReader(new InputStreamReader(peerSocket.getInputStream, "UTF-8"))
    val output = new PrintWriter(new OutputStreamWriter(peerSocket.getOutputStream, "UTF-8"), true)
    reportStatus("Connected to LAN host. Sharing player position.")
    while (running) {
      output.println(localPacket())
      val line = input.readLine()
      if (line == null) {
        running = false
      } else {
        receivePacket(line)
        Thread.sleep(90L)
      }
    }
  }

  def stop(): Unit = {
    running = false
    try { if (peerSocket != null) peerSocket.close() } catch { case _: Exception => () }
    try { if (serverSocket != null) serverSocket.close() } catch { case _: Exception => () }
  }
}

class HorsemanWorldPanel(playerName: String, hudLabel: JLabel, statusLabel: JLabel) extends JPanel {
  private val random = new Random()
  private val worldWidth = 1000.0
  private val worldHeight = 560.0
  private val baseX = 132.0
  private val baseY = 455.0

  @volatile private var playerX = baseX
  @volatile private var playerY = baseY
  private var targetX = -1.0
  private var targetY = -1.0
  private var moveUp = false
  private var moveDown = false
  private var moveLeft = false
  private var moveRight = false

  private var candyCount = 0
  private var coins = 0
  private var bankedCandy = 0
  private var capturedHorsemen = 0
  private var netCharges = 3
  private var carryingSeal = false
  private var ghostMode = false
  private var paused = false
  private var dashTicks = 0
  private var dashCooldown = 0
  private var netFlashTicks = 0
  private var tickCount = 0

  private var horsemanOneActive = false
  private var horsemanOneX = 865.0
  private var horsemanOneY = 155.0
  private var horsemanTwoActive = false
  private var horsemanTwoX = 930.0
  private var horsemanTwoY = 205.0
  private var horsemanTwoTicks = 0

  private var candyRushActive = false
  private var candyRushTicks = 0
  private var candyRushCountdown = 18 * 30
  private var candyRushCount = 0
  private var candyRushKind = 0
  private var candyRushSpawnTicks = 0
  private val candies = new ArrayBuffer[HorsemanCandy]()

  @volatile private var remoteVisible = false
  @volatile private var remoteX = 800.0
  @volatile private var remoteY = 430.0
  @volatile private var remoteCandy = 0
  @volatile private var remoteName = "LAN Player"
  @volatile private var remoteGhost = false

  setPreferredSize(new Dimension(1000, 560))
  setMinimumSize(new Dimension(800, 450))
  setFocusable(true)
  setBackground(new Color(10, 12, 32))
  setToolTipText("WASD or arrow keys to move. Click the ground to walk.")

  addMouseListener(new MouseAdapter {
    override def mousePressed(event: MouseEvent): Unit = {
      if (!paused) {
        val sx = worldWidth / math.max(1, getWidth).toDouble
        val sy = worldHeight / math.max(1, getHeight).toDouble
        targetX = event.getX.toDouble * sx
        targetY = event.getY.toDouble * sy
      }
      requestFocusInWindow()
    }
  })

  private val gameTimer = new Timer(33, new ActionListener {
    override def actionPerformed(event: ActionEvent): Unit = tickGame()
  })

  def startGame(): Unit = {
    candies.clear()
    var i = 0
    while (i < 18) {
      spawnOneCandy()
      i += 1
    }
    gameTimer.start()
    updateHud()
    repaint()
  }

  def stopGame(): Unit = gameTimer.stop()

  def setDirection(direction: String, pressed: Boolean): Unit = {
    direction match {
      case "up" => moveUp = pressed
      case "down" => moveDown = pressed
      case "left" => moveLeft = pressed
      case "right" => moveRight = pressed
      case _ =>
    }
    if (pressed) {
      targetX = -1.0
      targetY = -1.0
    }
  }

  def releaseHorseman(): Unit = {
    if (!horsemanOneActive && !carryingSeal) {
      horsemanOneX = 875.0
      horsemanOneY = 150.0 + random.nextInt(180).toDouble
      horsemanOneActive = true
      statusLabel.setText("The first Headless Horseman is released and chasing you. Collect Candy and escape!")
    } else if (carryingSeal) {
      statusLabel.setText("Take the captured seal back to your Base before releasing another Horseman.")
    } else {
      statusLabel.setText("The first Horseman is already chasing you!")
    }
    repaint()
  }

  def useMagicNet(): Unit = {
    if (paused) return
    if (netCharges <= 0) {
      statusLabel.setText("No Magic Net charges left. Return to Base to recharge.")
      return
    }
    val d1 = if (horsemanOneActive) distance(playerX, playerY, horsemanOneX, horsemanOneY) else 9999.0
    val d2 = if (horsemanTwoActive) distance(playerX, playerY, horsemanTwoX, horsemanTwoY) else 9999.0
    if (d1 < 145.0 || d2 < 145.0) {
      netCharges -= 1
      carryingSeal = true
      if (d1 <= d2) horsemanOneActive = false else horsemanTwoActive = false
      netFlashTicks = 24
      statusLabel.setText("MAGIC NET! Horseman sealed. Return to the glowing Base to claim your reward.")
    } else {
      statusLabel.setText("Move closer to a Horseman before using the Magic Net.")
    }
    updateHud()
    repaint()
  }

  def dash(): Unit = {
    if (!paused && dashCooldown <= 0) {
      dashTicks = 12
      dashCooldown = 150
      statusLabel.setText("COSMIC DASH! Keep moving toward the Base or Candy!")
    }
  }

  def returnToBase(): Unit = {
    targetX = baseX
    targetY = baseY
    moveUp = false
    moveDown = false
    moveLeft = false
    moveRight = false
    statusLabel.setText("Returning to Base. Reach the glowing circle to become safe and bank a captured seal.")
  }

  def togglePause(): Unit = {
    paused = !paused
    statusLabel.setText(if (paused) "Paused. Press P or Resume to continue." else "Run! Collect Candy, avoid the Horsemen and return to Base.")
    repaint()
  }

  def packetSnapshot(): String = {
    val safeName = playerName.replace("|", " ").replace("\n", " ")
    safeName + "|" + playerX + "|" + playerY + "|" + candyCount + "|" + ghostMode
  }

  def updateRemote(packet: String): Unit = {
    try {
      val parts = packet.split("\\|", -1)
      if (parts.length >= 5) {
        remoteName = parts(0)
        remoteX = parts(1).toDouble
        remoteY = parts(2).toDouble
        remoteCandy = parts(3).toInt
        remoteGhost = parts(4).toBoolean
        remoteVisible = true
      }
    } catch {
      case _: Exception => ()
    }
  }

  private def isInBase(x: Double, y: Double): Boolean = {
    x >= 42.0 && x <= 220.0 && y >= 385.0 && y <= 535.0
  }

  private def playerSpeed: Double = {
    val candyBoost = math.min(2.3, candyCount.toDouble * 0.045)
    val normal = 2.5 + candyBoost
    if (ghostMode) math.max(1.05, normal * 0.48) else normal
  }

  private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
    val dx = x1 - x2
    val dy = y1 - y2
    math.sqrt(dx * dx + dy * dy)
  }

  private def spawnOneCandy(): Unit = {
    val x = 230.0 + random.nextInt(720).toDouble
    val y = 260.0 + random.nextInt(245).toDouble
    val kind = random.nextInt(4)
    candies += HorsemanCandy(x, y, kind)
  }

  private def startCandyRush(): Unit = {
    candyRushActive = true
    candyRushCount += 1
    candyRushKind = random.nextInt(4)
    candyRushTicks = (14 + random.nextInt(8)) * 30
    candyRushSpawnTicks = 0
    var i = 0
    while (i < 12) {
      spawnOneCandy()
      i += 1
    }
    val eventName = candyRushKind match {
      case 0 => "PUMPKIN CANDY RUSH"
      case 1 => "MOON CANDY RUSH"
      case 2 => "SHADOW CANDY RUSH"
      case _ => "STAR CANDY RUSH"
    }
    statusLabel.setText(eventName + "! Collect as much Candy as you can. Every two Rush events summon Horseman Two for 20 seconds!")
    if (candyRushCount % 2 == 0) {
      horsemanTwoActive = true
      horsemanTwoX = 930.0
      horsemanTwoY = 145.0 + random.nextInt(220).toDouble
      horsemanTwoTicks = 20 * 30
    }
  }

  private def moveToward(x: Double, y: Double, tx: Double, ty: Double, speed: Double): (Double, Double) = {
    val dx = tx - x
    val dy = ty - y
    val length = math.sqrt(dx * dx + dy * dy)
    if (length < 0.001) (x, y)
    else (x + dx / length * speed, y + dy / length * speed)
  }

  private def tickGame(): Unit = {
    if (!paused) {
      tickCount += 1
      if (dashCooldown > 0) dashCooldown -= 1
      if (dashTicks > 0) dashTicks -= 1
      if (netFlashTicks > 0) netFlashTicks -= 1

      var dx = 0.0
      var dy = 0.0
      if (moveLeft) dx -= 1.0
      if (moveRight) dx += 1.0
      if (moveUp) dy -= 1.0
      if (moveDown) dy += 1.0

      if (dx == 0.0 && dy == 0.0 && targetX >= 0.0) {
        dx = targetX - playerX
        dy = targetY - playerY
        val length = math.sqrt(dx * dx + dy * dy)
        if (length < 5.0) {
          targetX = -1.0
          targetY = -1.0
          dx = 0.0
          dy = 0.0
        } else {
          dx = dx / length
          dy = dy / length
        }
      } else if (dx != 0.0 || dy != 0.0) {
        val length = math.sqrt(dx * dx + dy * dy)
        dx = dx / length
        dy = dy / length
      }

      var speed = playerSpeed
      if (dashTicks > 0) speed *= 2.25
      playerX = math.max(28.0, math.min(worldWidth - 28.0, playerX + dx * speed))
      playerY = math.max(235.0, math.min(worldHeight - 24.0, playerY + dy * speed))

      var candyIndex = candies.length - 1
      while (candyIndex >= 0) {
        val candy = candies(candyIndex)
        if (distance(playerX, playerY, candy.x, candy.y) < 23.0) {
          val reward = candy.kind match {
            case 0 => 1
            case 1 => 2
            case 2 => 3
            case _ => 4
          }
          candyCount += reward
          candies.remove(candyIndex)
          if (candyRushActive) coins += reward * 2 else coins += reward
        }
        candyIndex -= 1
      }

      if (tickCount % 95 == 0 && candies.length < 24) spawnOneCandy()

      if (candyRushActive) {
        candyRushTicks -= 1
        candyRushSpawnTicks += 1
        if (candyRushSpawnTicks >= 24 && candies.length < 38) {
          candyRushSpawnTicks = 0
          var i = 0
          while (i < 3) {
            spawnOneCandy()
            i += 1
          }
        }
        if (candyRushTicks <= 0) {
          candyRushActive = false
          candyRushCountdown = (24 + random.nextInt(23)) * 30
          statusLabel.setText("Candy Rush ended. Next event time is randomized. Keep collecting Candy!")
        }
      } else {
        candyRushCountdown -= 1
        if (candyRushCountdown <= 0) startCandyRush()
      }

      if (horsemanTwoActive) {
        horsemanTwoTicks -= 1
        if (horsemanTwoTicks <= 0) {
          horsemanTwoActive = false
          statusLabel.setText("Horseman Two vanished after 20 seconds. Prepare for the next Candy Rush!")
        }
      }

      val safe = isInBase(playerX, playerY)
      if (safe && ghostMode) {
        ghostMode = false
        statusLabel.setText("You reached Base! The Ghost Curse is cleared and your full Candy speed is restored.")
      }
      if (safe && carryingSeal) {
        carryingSeal = false
        capturedHorsemen += 1
        bankedCandy += 80
        candyCount += 20
        coins += 150
        netCharges = math.min(5, netCharges + 2)
        statusLabel.setText("SEAL DELIVERED! +80 Candy, +150 Coins, +20 speed Candy and 2 Net charges.")
      }

      if (horsemanOneActive && !safe) {
        val p = moveToward(horsemanOneX, horsemanOneY, playerX, playerY, 1.75 + math.min(0.8, candyRushCount * 0.04))
        horsemanOneX = p._1
        horsemanOneY = p._2
        if (distance(playerX, playerY, horsemanOneX, horsemanOneY) < 29.0) becomeGhost("The first Horseman caught you!")
      }

      if (horsemanTwoActive && !safe) {
        val p = moveToward(horsemanTwoX, horsemanTwoY, playerX, playerY, 2.15)
        horsemanTwoX = p._1
        horsemanTwoY = p._2
        if (distance(playerX, playerY, horsemanTwoX, horsemanTwoY) < 31.0) {
          horsemanTwoActive = false
          becomeGhost("Horseman Two caught you! Ghost Curse: your speed is reduced. Reach Base to recover.")
        }
      }

      if (tickCount % 5 == 0) updateHud()
      repaint()
    }
  }

  private def becomeGhost(message: String): Unit = {
    ghostMode = true
    playerX = 520.0
    playerY = 315.0
    targetX = -1.0
    targetY = -1.0
    moveUp = false
    moveDown = false
    moveLeft = false
    moveRight = false
    horsemanOneActive = false
    horsemanTwoActive = false
    candyCount = math.max(0, candyCount - 8)
    statusLabel.setText(message + " Run to the glowing Base!")
  }

  private def updateHud(): Unit = {
    val eventText = if (candyRushActive) {
      val seconds = math.max(0, candyRushTicks / 30)
      "CANDY RUSH " + seconds + "s"
    } else {
      "Next Candy Rush: " + math.max(0, candyRushCountdown / 30) + "s"
    }
    val secondText = if (horsemanTwoActive) " | Horseman 2: " + math.max(0, horsemanTwoTicks / 30) + "s" else ""
    val ghostText = if (ghostMode) " | GHOST CURSE: SPEED DRAINED" else ""
    val sealText = if (carryingSeal) " | CARRYING SEAL: RETURN TO BASE" else ""
    hudLabel.setText("Player: " + playerName + "   |   Candy: " + candyCount + "   |   Speed: " + playerSpeed.toInt.toString + "   |   Coins: " + coins + "   |   Nets: " + netCharges + "   |   " + eventText + secondText + ghostText + sealText)
  }

  private def colorForCandy(kind: Int): Color = {
    kind match {
      case 0 => new Color(255, 145, 50)
      case 1 => new Color(125, 180, 255)
      case 2 => new Color(195, 115, 255)
      case _ => new Color(255, 225, 115)
    }
  }

  private def drawCandy(g: Graphics2D, candy: HorsemanCandy): Unit = {
    val scale = 0.62 + candy.y / 560.0 * 0.55
    val size = 12.0 * scale
    g.setColor(new Color(0, 0, 0, 90))
    g.fillOval((candy.x - size).toInt, (candy.y + size * 0.7).toInt, (size * 2.2).toInt, (size * 0.7).toInt)
    g.setColor(colorForCandy(candy.kind))
    g.fillOval((candy.x - size).toInt, (candy.y - size).toInt, (size * 2).toInt, (size * 2).toInt)
    g.setColor(new Color(255, 255, 255, 190))
    g.fillOval((candy.x - size * 0.45).toInt, (candy.y - size * 0.55).toInt, (size * 0.55).toInt, (size * 0.55).toInt)
  }

  private def drawBase(g: Graphics2D): Unit = {
    g.setColor(new Color(40, 210, 255, 45))
    g.fillOval(45, 392, 185, 128)
    g.setColor(new Color(75, 225, 255, 210))
    g.setStroke(new java.awt.BasicStroke(4.0f))
    g.drawOval(45, 392, 185, 128)
    g.setColor(new Color(170, 245, 255))
    g.setFont(new Font("SansSerif", Font.BOLD, 17))
    g.drawString("SAFE BASE", 88, 447)
    g.setFont(new Font("SansSerif", Font.PLAIN, 11))
    g.drawString("Return here to clear Ghost Curse", 58, 466)
  }

  private def drawPlayer(g: Graphics2D, x: Double, y: Double, ghost: Boolean, name: String): Unit = {
    val s = 0.62 + y / 560.0 * 0.62
    val w = 25.0 * s
    val h = 42.0 * s
    g.setColor(new Color(0, 0, 0, 95))
    g.fillOval((x - w).toInt, (y + h * 0.54).toInt, (w * 2.2).toInt, (w * 0.68).toInt)
    val robeColor = if (ghost) new Color(120, 220, 255, 185) else new Color(65, 165, 255)
    g.setColor(robeColor)
    val robe = new Polygon(
      Array((x - w * 0.55).toInt, (x + w * 0.55).toInt, (x + w).toInt, (x - w).toInt),
      Array((y - h * 0.05).toInt, (y - h * 0.05).toInt, (y + h).toInt, (y + h).toInt), 4
    )
    g.fillPolygon(robe)
    g.setColor(new Color(255, 220, 180))
    g.fillOval((x - w * 0.42).toInt, (y - h * 0.82).toInt, (w * 0.84).toInt, (w * 0.84).toInt)
    g.setColor(new Color(235, 245, 255))
    g.setFont(new Font("SansSerif", Font.BOLD, math.max(10, (12 * s).toInt)))
    g.drawString(name.take(12), (x - w).toInt, (y - h).toInt)
    if (ghost) {
      g.setColor(new Color(150, 245, 255, 120))
      g.drawOval((x - w * 1.45).toInt, (y - h * 1.05).toInt, (w * 2.9).toInt, (h * 2.2).toInt)
    }
  }

  private def drawHorseman(g: Graphics2D, x: Double, y: Double, second: Boolean): Unit = {
    val s = 0.68 + y / 560.0 * 0.62
    val w = 30.0 * s
    val bodyColor = if (second) new Color(115, 150, 220, 180) else new Color(42, 35, 58)
    g.setColor(new Color(0, 0, 0, 110))
    g.fillOval((x - w * 1.2).toInt, (y + w * 0.7).toInt, (w * 2.8).toInt, (w * 0.8).toInt)
    g.setColor(bodyColor)
    g.fillOval((x - w).toInt, (y - w * 0.25).toInt, (w * 2.1).toInt, (w * 1.15).toInt)
    g.fillRect((x - w * 0.75).toInt, (y + w * 0.4).toInt, (w * 0.35).toInt, (w * 0.9).toInt)
    g.fillRect((x + w * 0.45).toInt, (y + w * 0.4).toInt, (w * 0.35).toInt, (w * 0.9).toInt)
    g.setColor(if (second) new Color(90, 190, 255, 210) else new Color(24, 18, 34))
    val cloak = new Polygon(
      Array((x - w * 0.45).toInt, (x + w * 0.55).toInt, (x + w * 0.8).toInt, (x - w * 0.9).toInt),
      Array((y - w * 1.6).toInt, (y - w * 1.6).toInt, (y + w * 0.8).toInt, (y + w * 0.8).toInt), 4
    )
    g.fillPolygon(cloak)
    g.setColor(new Color(255, 130, 45))
    g.fillOval((x - w * 0.22).toInt, (y - w * 2.15).toInt, (w * 0.65).toInt, (w * 0.75).toInt)
    g.setColor(new Color(255, 220, 110))
    g.fillOval((x - w * 0.06).toInt, (y - w * 2.0).toInt, (w * 0.23).toInt, (w * 0.28).toInt)
    g.setColor(if (second) new Color(140, 220, 255) else new Color(255, 100, 55))
    g.setFont(new Font("SansSerif", Font.BOLD, math.max(10, (11 * s).toInt)))
    g.drawString(if (second) "HORSEMAN 2" else "HORSEMAN 1", (x - w * 1.1).toInt, (y - w * 2.35).toInt)
  }

  override protected def paintComponent(graphics: Graphics): Unit = {
    super.paintComponent(graphics)
    val g = graphics.create().asInstanceOf[Graphics2D]
    val sx = getWidth.toDouble / worldWidth
    val sy = getHeight.toDouble / worldHeight
    g.scale(sx, sy)
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)

    g.setPaint(new GradientPaint(0.0f, 0.0f, new Color(9, 12, 42), 0.0f, 270.0f, new Color(74, 32, 93)))
    g.fillRect(0, 0, 1000, 560)

    var star = 0
    while (star < 85) {
      val x = (star * 137 + 41) % 1000
      val y = (star * 73 + 19) % 205
      val r = if (star % 7 == 0) 3 else 1
      g.setColor(if (star % 4 == 0) new Color(255, 215, 150, 200) else new Color(205, 220, 255, 175))
      g.fillOval(x, y, r, r)
      star += 1
    }

    g.setColor(new Color(160, 90, 210, 70))
    g.fillOval(660, 28, 220, 220)
    g.setColor(new Color(255, 170, 90, 210))
    g.fillOval(720, 76, 115, 115)
    g.setColor(new Color(18, 15, 45))
    g.fillOval(760, 55, 112, 112)
    g.setColor(new Color(255, 195, 120, 130))
    g.drawOval(702, 60, 150, 150)

    val ridgeOne = new Polygon(Array(0, 0, 120, 220, 340, 470, 600, 740, 860, 1000, 1000), Array(250, 210, 170, 225, 165, 225, 175, 230, 170, 220, 290), 11)
    g.setColor(new Color(35, 28, 72))
    g.fillPolygon(ridgeOne)
    val ridgeTwo = new Polygon(Array(0, 120, 250, 390, 520, 660, 800, 1000, 1000, 0), Array(285, 240, 270, 215, 275, 230, 275, 235, 560, 560), 10)
    g.setColor(new Color(27, 41, 70))
    g.fillPolygon(ridgeTwo)

    val ground = new Polygon(Array(0, 1000, 1000, 0), Array(260, 260, 560, 560), 4)
    g.setPaint(new GradientPaint(0.0f, 260.0f, new Color(35, 65, 78), 0.0f, 560.0f, new Color(9, 36, 44)))
    g.fillPolygon(ground)
    g.setColor(new Color(95, 165, 155, 65))
    var line = 0
    while (line < 13) {
      val yy = 280 + line * 23
      g.drawLine(0, yy, 1000, yy)
      line += 1
    }
    line = 0
    while (line < 17) {
      val xx = line * 62
      g.drawLine(500, 260, xx, 560)
      line += 1
    }

    drawBase(g)
    if (candyRushActive) {
      g.setColor(new Color(255, 190, 85, 35))
      g.fillRect(0, 250, 1000, 310)
      g.setColor(new Color(255, 215, 130))
      g.setFont(new Font("SansSerif", Font.BOLD, 17))
      g.drawString("CANDY RUSH IS LIVE!", 395, 285)
    }

    var i = 0
    while (i < candies.length) {
      drawCandy(g, candies(i))
      i += 1
    }

    if (horsemanOneActive) drawHorseman(g, horsemanOneX, horsemanOneY, false)
    if (horsemanTwoActive) drawHorseman(g, horsemanTwoX, horsemanTwoY, true)
    if (remoteVisible) drawPlayer(g, remoteX, remoteY, remoteGhost, remoteName)
    drawPlayer(g, playerX, playerY, ghostMode, playerName)

    if (carryingSeal) {
      g.setColor(new Color(130, 245, 255))
      g.setFont(new Font("SansSerif", Font.BOLD, 15))
      g.drawString("HORSEMAN SEAL - RETURN TO BASE", 330, 530)
    }
    if (netFlashTicks > 0) {
      g.setColor(new Color(115, 245, 255, 170))
      g.setStroke(new java.awt.BasicStroke(4.0f))
      g.drawOval((playerX - 52).toInt, (playerY - 63).toInt, 104, 104)
    }
    if (ghostMode) {
      g.setColor(new Color(125, 225, 255, 190))
      g.setFont(new Font("SansSerif", Font.BOLD, 19))
      g.drawString("GHOST CURSE! RUN TO BASE!", 360, 45)
    }
    if (paused) {
      g.setColor(new Color(0, 0, 0, 155))
      g.fillRect(0, 0, 1000, 560)
      g.setColor(Color.WHITE)
      g.setFont(new Font("SansSerif", Font.BOLD, 42))
      g.drawString("PAUSED", 410, 285)
    }
    g.dispose()
  }
}

class HorsemanGameScreen(
  name: String,
  mode: String,
  onBack: () => Unit,
  statusCallback: String => Unit
) extends JPanel(new BorderLayout()) {
  val statusLabel = new JLabel("Collect Candy to increase your speed. Press R to release Horseman One.", SwingConstants.CENTER)
  val hudLabel = new JLabel("", SwingConstants.CENTER)
  val world = new HorsemanWorldPanel(name, hudLabel, statusLabel)

  hudLabel.setOpaque(true)
  hudLabel.setBackground(new Color(20, 18, 45))
  hudLabel.setForeground(new Color(245, 230, 255))
  hudLabel.setFont(new Font("SansSerif", Font.BOLD, 13))
  hudLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 8, 6))

  statusLabel.setOpaque(true)
  statusLabel.setBackground(new Color(35, 28, 58))
  statusLabel.setForeground(new Color(245, 230, 255))
  statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 12))
  statusLabel.setBorder(BorderFactory.createEmptyBorder(7, 8, 7, 8))

  private val buttons = new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 7))
  buttons.setBackground(new Color(24, 20, 48))

  private def addGameButton(text: String, action: () => Unit): Unit = {
    val button = new JButton(text)
    button.setFont(new Font("SansSerif", Font.BOLD, 12))
    button.addActionListener(new ActionListener {
      override def actionPerformed(event: ActionEvent): Unit = action()
    })
    buttons.add(button)
  }

  addGameButton("RELEASE HORSEMAN [R]", () => world.releaseHorseman())
  addGameButton("MAGIC NET [F]", () => world.useMagicNet())
  addGameButton("COSMIC DASH [SPACE]", () => world.dash())
  addGameButton("RETURN TO BASE [B]", () => world.returnToBase())
  addGameButton("PAUSE [P]", () => world.togglePause())
  addGameButton("BACK TO LOGIN", () => onBack())

  add(hudLabel, BorderLayout.NORTH)
  add(world, BorderLayout.CENTER)
  val south = new JPanel(new BorderLayout())
  south.setBackground(new Color(24, 20, 48))
  south.add(buttons, BorderLayout.CENTER)
  south.add(statusLabel, BorderLayout.SOUTH)
  add(south, BorderLayout.SOUTH)
  setPreferredSize(new Dimension(1020, 690))

  def start(): Unit = world.startGame()
  def stop(): Unit = world.stopGame()
  def reportLan(message: String): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        statusLabel.setText(message)
        statusCallback(message)
      }
    })
  }
}

object HeadlessHorsemanChaseEvent9 {
  private val lanPort = 48888
  private var frame: JFrame = null
  private var currentGame: HorsemanGameScreen = null
  private var lanPeer: HorsemanLanPeer = null
  private var currentPlayerName = "Player"

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = showLogin()
    })
  }

  private def showLogin(): Unit = {
    if (frame == null) {
      frame = new JFrame("SPRING SEASON EVENT 9 - THE HEADLESS HORSEMAN CHASE")
      frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
      frame.setResizable(true)
      frame.addWindowListener(new WindowAdapter {
        override def windowClosing(event: WindowEvent): Unit = cleanup()
      })
    }
    val root = new JPanel(new BorderLayout(12, 12))
    root.setBackground(new Color(12, 13, 38))
    root.setBorder(BorderFactory.createEmptyBorder(24, 28, 24, 28))

    val title = new JLabel("THE HEADLESS HORSEMAN CHASE", SwingConstants.CENTER)
    title.setForeground(new Color(255, 198, 120))
    title.setFont(new Font("SansSerif", Font.BOLD, 30))
    val subtitle = new JLabel("SPRING SEASON EVENT 9  |  CANDY RUSH  ?  MAGIC NET  ?  ESCAPE TO BASE", SwingConstants.CENTER)
    subtitle.setForeground(new Color(195, 210, 245))
    subtitle.setFont(new Font("SansSerif", Font.BOLD, 13))

    val header = new JPanel(new BorderLayout(4, 8))
    header.setOpaque(false)
    header.add(title, BorderLayout.CENTER)
    header.add(subtitle, BorderLayout.SOUTH)
    root.add(header, BorderLayout.NORTH)

    val form = new JPanel(new java.awt.GridLayout(0, 1, 8, 8))
    form.setOpaque(false)
    val nameLabel = new JLabel("PLAYER NAME", SwingConstants.CENTER)
    nameLabel.setForeground(Color.WHITE)
    nameLabel.setFont(new Font("SansSerif", Font.BOLD, 15))
    val nameField = new JTextField("Player", 20)
    nameField.setHorizontalAlignment(SwingConstants.CENTER)
    nameField.setFont(new Font("SansSerif", Font.PLAIN, 18))
    val ipLabel = new JLabel("HOST IP FOR JOIN LAN (example: 192.168.1.10)", SwingConstants.CENTER)
    ipLabel.setForeground(new Color(200, 215, 245))
    val ipField = new JTextField("127.0.0.1", 20)
    ipField.setHorizontalAlignment(SwingConstants.CENTER)
    ipField.setFont(new Font("SansSerif", Font.PLAIN, 16))

    form.add(nameLabel)
    form.add(nameField)
    form.add(ipLabel)
    form.add(ipField)

    val info = new JLabel("Candy makes you faster. Every second Candy Rush brings Horseman Two for 20 seconds.", SwingConstants.CENTER)
    info.setForeground(new Color(255, 215, 155))
    form.add(info)
    root.add(form, BorderLayout.CENTER)

    val controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 14, 12))
    controls.setOpaque(false)
    def makeButton(text: String, action: () => Unit): JButton = {
      val button = new JButton(text)
      button.setFont(new Font("SansSerif", Font.BOLD, 14))
      button.addActionListener(new ActionListener {
        override def actionPerformed(event: ActionEvent): Unit = action()
      })
      button
    }
    controls.add(makeButton("SINGLE PLAYER", () => beginGame(nameField.getText, "solo", "")))
    controls.add(makeButton("HOST LAN", () => beginGame(nameField.getText, "host", "")))
    controls.add(makeButton("JOIN LAN", () => beginGame(nameField.getText, "client", ipField.getText.trim)))
    root.add(controls, BorderLayout.SOUTH)

    frame.setContentPane(root)
    frame.setPreferredSize(new Dimension(900, 520))
    frame.pack()
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)
  }

  private def beginGame(rawName: String, mode: String, hostAddress: String): Unit = {
    val trimmed = if (rawName == null) "" else rawName.trim
    currentPlayerName = if (trimmed.isEmpty) "Player" else trimmed.take(16)
    if (lanPeer != null) lanPeer.stop()
    if (currentGame != null) currentGame.stop()

    currentGame = new HorsemanGameScreen(
      currentPlayerName,
      mode,
      () => returnToLogin(),
      message => ()
    )
    frame.setContentPane(currentGame)
    frame.setPreferredSize(new Dimension(1050, 760))
    frame.pack()
    frame.setLocationRelativeTo(null)
    installKeyBindings(currentGame)
    currentGame.start()

    if (mode == "host") {
      lanPeer = new HorsemanLanPeer(
        true,
        "",
        lanPort,
        () => currentGame.world.packetSnapshot(),
        packet => currentGame.world.updateRemote(packet),
        message => currentGame.reportLan(message)
      )
      lanPeer.start()
    } else if (mode == "client") {
      lanPeer = new HorsemanLanPeer(
        false,
        hostAddress,
        lanPort,
        () => currentGame.world.packetSnapshot(),
        packet => currentGame.world.updateRemote(packet),
        message => currentGame.reportLan(message)
      )
      lanPeer.start()
    }
    currentGame.requestFocusInWindow()
  }

  private def bindKey(stroke: String, actionName: String)(body: => Unit): Unit = {
    val input = frame.getRootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
    val actions = frame.getRootPane.getActionMap
    input.put(KeyStroke.getKeyStroke(stroke), actionName)
    actions.put(actionName, new AbstractAction {
      override def actionPerformed(event: ActionEvent): Unit = body
    })
  }

  private def installKeyBindings(game: HorsemanGameScreen): Unit = {
    val world = game.world
    bindKey("pressed W", "moveUp") { world.setDirection("up", true) }
    bindKey("released W", "stopUp") { world.setDirection("up", false) }
    bindKey("pressed S", "moveDown") { world.setDirection("down", true) }
    bindKey("released S", "stopDown") { world.setDirection("down", false) }
    bindKey("pressed A", "moveLeft") { world.setDirection("left", true) }
    bindKey("released A", "stopLeft") { world.setDirection("left", false) }
    bindKey("pressed D", "moveRight") { world.setDirection("right", true) }
    bindKey("released D", "stopRight") { world.setDirection("right", false) }
    bindKey("pressed UP", "moveUpArrow") { world.setDirection("up", true) }
    bindKey("released UP", "stopUpArrow") { world.setDirection("up", false) }
    bindKey("pressed DOWN", "moveDownArrow") { world.setDirection("down", true) }
    bindKey("released DOWN", "stopDownArrow") { world.setDirection("down", false) }
    bindKey("pressed LEFT", "moveLeftArrow") { world.setDirection("left", true) }
    bindKey("released LEFT", "stopLeftArrow") { world.setDirection("left", false) }
    bindKey("pressed RIGHT", "moveRightArrow") { world.setDirection("right", true) }
    bindKey("released RIGHT", "stopRightArrow") { world.setDirection("right", false) }
    bindKey("pressed SPACE", "dash") { world.dash() }
    bindKey("pressed F", "magicNet") { world.useMagicNet() }
    bindKey("pressed R", "releaseHorseman") { world.releaseHorseman() }
    bindKey("pressed B", "returnBase") { world.returnToBase() }
    bindKey("pressed P", "pause") { world.togglePause() }
    bindKey("pressed ESCAPE", "backToLogin") { returnToLogin() }
  }

  private def returnToLogin(): Unit = {
    if (lanPeer != null) {
      lanPeer.stop()
      lanPeer = null
    }
    if (currentGame != null) {
      currentGame.stop()
      currentGame = null
    }
    showLogin()
  }

  private def cleanup(): Unit = {
    if (lanPeer != null) lanPeer.stop()
    if (currentGame != null) currentGame.stop()
  }
}

HeadlessHorsemanChaseEvent9.main(Array[String]())