Code Sketch


Bixuu catch the egg
By: Mhalsakant School
Category: Programming
import java.awt.{BorderLayout, Color, Dimension, FlowLayout, Font, GradientPaint, Graphics, Graphics2D, GridLayout, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyEvent, WindowAdapter, WindowEvent}
import java.awt.geom.Ellipse2D
import java.io.{File, FileInputStream, FileOutputStream}
import java.util.Properties
import javax.swing.{AbstractAction, BorderFactory, JButton, JComponent, JDialog, JFrame, JLabel, JOptionPane, JPanel, JScrollPane, JPasswordField, JTextField, KeyStroke, SwingUtilities, Timer, WindowConstants}
import javax.swing.border.EmptyBorder
import scala.collection.mutable.ArrayBuffer
import scala.util.Random

/**
 * SPRING SEASON - CATCH THE EGG
 * One-file Scala Swing game for Kojo Code Exchange's Scala script runner.
 * The last line below starts the game when Run is pressed in Code Exchange.
 * Run on a desktop JVM with a graphical display; headless runners cannot show Swing windows.
 */
object SpringSeasonCatchTheEgg {
  private val GameTitle = "SPRING SEASON - CATCH THE EGG"
  private val AdminPassword = "BIXUU@SSCTE"
  private val MaxSpeed = 1000000
  private val random = new Random()

  private case class Biome(
    name: String,
    gate: Int,
    sky: Color,
    ground: Color,
    accent: Color
  )

  // The 22 requested biomes and their speed requirements.
  private val biomes = Vector(
    Biome("HEN FOREST",       10,  new Color(145, 213, 245), new Color(75, 155, 79),  new Color(250, 225, 145)),
    Biome("TIGER JUNGLE",     20,  new Color(117, 190, 151), new Color(35, 105, 57),  new Color(242, 166, 62)),
    Biome("LAKE",             40,  new Color(114, 207, 231), new Color(40, 139, 164), new Color(220, 250, 255)),
    Biome("DESERT",           60,  new Color(250, 211, 137), new Color(194, 145, 73), new Color(255, 236, 166)),
    Biome("ARCTIC",           80,  new Color(211, 238, 250), new Color(129, 182, 211), new Color(255, 255, 255)),
    Biome("WATER",           100,  new Color(76, 160, 213),  new Color(22, 92, 154),  new Color(155, 226, 255)),
    Biome("DRAGON PEAK",     100,  new Color(146, 116, 170), new Color(95, 64, 93),   new Color(255, 126, 91)),
    Biome("CAVE",            120,  new Color(91, 100, 112),  new Color(48, 53, 65),   new Color(178, 190, 205)),
    Biome("VOLCANO",         140,  new Color(104, 57, 53),   new Color(70, 31, 28),   new Color(255, 111, 47)),
    Biome("ABYSS OCEAN",     160,  new Color(30, 61, 111),   new Color(9, 25, 67),    new Color(85, 190, 230)),
    Biome("PREHISTORIC",     180,  new Color(173, 187, 119), new Color(91, 111, 52),  new Color(206, 171, 105)),
    Biome("HEAVEN",          200,  new Color(218, 235, 255), new Color(163, 195, 226),new Color(255, 255, 255)),
    Biome("COSMIC",          220,  new Color(67, 55, 125),   new Color(29, 23, 70),   new Color(184, 135, 255)),
    Biome("TOY BOX",         240,  new Color(255, 193, 211), new Color(126, 188, 232),new Color(255, 237, 115)),
    Biome("DINO VALLEY",     260,  new Color(158, 183, 107), new Color(80, 119, 54),  new Color(219, 185, 119)),
    Biome("SKY CASTLE",      280,  new Color(146, 202, 242), new Color(105, 153, 207), new Color(255, 255, 255)),
    Biome("DISCO",           300,  new Color(101, 69, 153),  new Color(44, 34, 92),   new Color(255, 105, 214)),
    Biome("CANDY KINGDOM",   320,  new Color(255, 190, 216), new Color(232, 139, 180),new Color(255, 245, 160)),
    Biome("CRYSTAL CAVES",   340,  new Color(121, 112, 193), new Color(66, 62, 130),  new Color(176, 239, 255)),
    Biome("SKELETON MARSH",  360,  new Color(113, 133, 99),  new Color(49, 71, 53),   new Color(207, 218, 167)),
    Biome("RAINBOW REALM",   400,  new Color(147, 196, 245), new Color(96, 160, 131), new Color(255, 255, 255)),
    Biome("STEEL FACTORY",   500,  new Color(111, 130, 147), new Color(57, 69, 82),   new Color(255, 184, 84))
  )

  private val saveDirectory = new File(System.getProperty("user.home"), ".spring-season-catch-egg")
  private val saveFile = new File(saveDirectory, "player.properties")
  private val saveProperties = new Properties()

  private var playerName = "Player"
  private var coins = 0L
  private var speed = 0
  private var eggsCaught = 0L
  private var springEventActive = true
  private var selectedBiome = 0

  private var loginFrame: JFrame = _
  private var gameFrame: JFrame = _
  private var gamePanel: GamePanel = _
  private var statsLabel: JLabel = _
  private var statsTimer: Timer = _

  def main(args: Array[String]): Unit = {
    if (java.awt.GraphicsEnvironment.isHeadless) {
      System.err.println("Spring Season needs a desktop display to open its Swing window.")
      System.err.println("Use Kojo on your Windows desktop, not a headless runner.")
      return
    }
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        try {
          loadSave()
          showLoginWindow()
        } catch {
          case ex: Throwable =>
            ex.printStackTrace()
            JOptionPane.showMessageDialog(
              null,
              "Game startup failed:\n" + ex.toString +
                "\n\nSend this complete error message.",
              GameTitle,
              JOptionPane.ERROR_MESSAGE
            )
        }
      }
    })
  }

  private def loadSave(): Unit = {
    if (saveFile.isFile) {
      var in: FileInputStream = null
      try {
        in = new FileInputStream(saveFile)
        saveProperties.load(in)
        playerName = saveProperties.getProperty("playerName", "Player")
        coins = readLong("coins", 0L)
        speed = math.max(0, math.min(MaxSpeed, readInt("speed", 0)))
        eggsCaught = readLong("eggsCaught", 0L)
        selectedBiome = math.max(0, math.min(biomes.length - 1, readInt("selectedBiome", 0)))
      } catch {
        case ex: Exception => System.err.println("Save load warning: " + ex.toString)
      } finally {
        if (in != null) in.close()
      }
    }
  }

  private def readLong(key: String, defaultValue: Long): Long = {
    try saveProperties.getProperty(key, defaultValue.toString).toLong
    catch { case _: Exception => defaultValue }
  }

  private def readInt(key: String, defaultValue: Int): Int = {
    try saveProperties.getProperty(key, defaultValue.toString).toInt
    catch { case _: Exception => defaultValue }
  }

  private def saveGame(): Unit = {
    var out: FileOutputStream = null
    try {
      if (!saveDirectory.exists()) saveDirectory.mkdirs()
      saveProperties.setProperty("playerName", playerName)
      saveProperties.setProperty("coins", coins.toString)
      saveProperties.setProperty("speed", speed.toString)
      saveProperties.setProperty("eggsCaught", eggsCaught.toString)
      saveProperties.setProperty("selectedBiome", selectedBiome.toString)
      out = new FileOutputStream(saveFile)
      saveProperties.store(out, GameTitle)
    } catch {
      case ex: Exception => System.err.println("Save warning: " + ex.toString)
    } finally {
      if (out != null) out.close()
    }
  }

  // ---------------- LOGIN ----------------

  private def showLoginWindow(): Unit = {
    loginFrame = new JFrame(GameTitle + " - SIGN IN")
    loginFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
    loginFrame.setSize(680, 450)
    loginFrame.setMinimumSize(new Dimension(560, 390))
    loginFrame.setLocationRelativeTo(null)

    val background = new JPanel(new BorderLayout())
    background.setBackground(new Color(29, 93, 68))
    background.setBorder(new EmptyBorder(28, 28, 28, 28))

    val card = new JPanel()
    card.setLayout(new javax.swing.BoxLayout(card, javax.swing.BoxLayout.Y_AXIS))
    card.setBackground(new Color(255, 250, 226))
    card.setBorder(new EmptyBorder(30, 42, 30, 42))

    val title = new JLabel("SPRING SEASON")
    title.setFont(new Font("Arial", Font.BOLD, 38))
    title.setForeground(new Color(30, 125, 75))
    title.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val subtitle = new JLabel("CATCH THE EGG")
    subtitle.setFont(new Font("Arial", Font.BOLD, 26))
    subtitle.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val prompt = new JLabel("Enter your player name")
    prompt.setFont(new Font("Arial", Font.BOLD, 17))
    prompt.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val nameField = new JTextField(playerName, 18)
    nameField.setFont(new Font("Arial", Font.PLAIN, 18))
    nameField.setMaximumSize(new Dimension(300, 42))

    val signIn = new JButton("SIGN IN AND PLAY")
    signIn.setFont(new Font("Arial", Font.BOLD, 18))
    signIn.setBackground(new Color(55, 165, 95))
    signIn.setForeground(Color.WHITE)
    signIn.setFocusPainted(false)
    signIn.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val help = new JLabel("Catch eggs  |  Earn coins  |  Train speed  |  Explore 22 biomes")
    help.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val action = new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val entered = nameField.getText.trim
        if (entered.isEmpty) {
          JOptionPane.showMessageDialog(loginFrame, "Please enter your player name.")
        } else {
          playerName = entered.take(20)
          saveGame()
          openMainGameWindow()
        }
      }
    }
    signIn.addActionListener(action)
    nameField.addActionListener(action)

    card.add(title)
    card.add(javax.swing.Box.createVerticalStrut(8))
    card.add(subtitle)
    card.add(javax.swing.Box.createVerticalStrut(28))
    card.add(prompt)
    card.add(javax.swing.Box.createVerticalStrut(10))
    card.add(nameField)
    card.add(javax.swing.Box.createVerticalStrut(20))
    card.add(signIn)
    card.add(javax.swing.Box.createVerticalStrut(22))
    card.add(help)

    background.add(card, BorderLayout.CENTER)
    loginFrame.setContentPane(background)
    loginFrame.setLocationRelativeTo(null)
    loginFrame.setVisible(true)
    loginFrame.toFront()
  }

  // ---------------- GAME WINDOW ----------------

  private def openMainGameWindow(): Unit = {
    gameFrame = new JFrame(GameTitle)
    gameFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
    gameFrame.setSize(1180, 790)
    gameFrame.setMinimumSize(new Dimension(900, 620))
    gameFrame.setLocationRelativeTo(null)

    val main = new JPanel(new BorderLayout())
    main.add(createToolbar(), BorderLayout.NORTH)
    gamePanel = new GamePanel()
    main.add(gamePanel, BorderLayout.CENTER)
    gameFrame.setContentPane(main)

    gameFrame.addWindowListener(new WindowAdapter {
      override def windowClosing(e: WindowEvent): Unit = {
        saveGame()
        if (statsTimer != null) statsTimer.stop()
        if (gamePanel != null) gamePanel.stopGame()
        gameFrame.dispose()
        System.exit(0)
      }
    })

    gameFrame.setLocationRelativeTo(null)
    gameFrame.setVisible(true)
    gameFrame.toFront()
    gameFrame.requestFocus()
    gamePanel.startGame()
    if (loginFrame != null) loginFrame.dispose()
  }

  private def createToolbar(): JPanel = {
    val bar = new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 8))
    bar.setBackground(new Color(24, 43, 66))

    val name = new JLabel("PLAYER: " + playerName)
    name.setForeground(Color.WHITE)
    name.setFont(new Font("Arial", Font.BOLD, 13))

    statsLabel = new JLabel(statsText())
    statsLabel.setForeground(new Color(255, 225, 120))
    statsLabel.setFont(new Font("Arial", Font.BOLD, 13))

    val train = new JButton("TREADMILL +1")
    train.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = trainSpeed()
    })

    val biomeButton = new JButton("BIOMES [M]")
    biomeButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showBiomes()
    })

    val shop = new JButton("SPEED SHOP")
    shop.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = openSpeedShop()
    })

    val save = new JButton("SAVE")
    save.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        saveGame()
        JOptionPane.showMessageDialog(gameFrame, "Game saved successfully.")
      }
    })

    val admin = new JButton("GAME CREATOR / ADMIN")
    admin.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = openAdminPanel()
    })

    statsTimer = new Timer(300, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (statsLabel != null) statsLabel.setText(statsText())
      }
    })
    statsTimer.start()

    bar.add(name)
    bar.add(statsLabel)
    bar.add(train)
    bar.add(biomeButton)
    bar.add(shop)
    bar.add(save)
    bar.add(admin)
    bar
  }

  private def statsText(): String = {
    "Coins: " + coins + "   Speed: " + speed + "   Eggs: " + eggsCaught +
      "   Biome: " + biomes(selectedBiome).name
  }

  private def trainSpeed(): Unit = {
    if (speed < MaxSpeed) {
      speed = math.min(MaxSpeed, speed + 1)
      saveGame()
      if (gamePanel != null) gamePanel.repaint()
    }
  }

  private def openSpeedShop(): Unit = {
    val cost = 100L + speed.toLong
    val answer = JOptionPane.showConfirmDialog(
      gameFrame,
      "Buy +10 speed?\nCurrent speed: " + speed + "\nCost: " + cost + " coins",
      "SPEED SHOP",
      JOptionPane.YES_NO_OPTION
    )
    if (answer == JOptionPane.YES_OPTION) {
      if (coins >= cost && speed < MaxSpeed) {
        coins -= cost
        speed = math.min(MaxSpeed, speed + 10)
        saveGame()
        if (gamePanel != null) gamePanel.repaint()
      } else {
        JOptionPane.showMessageDialog(gameFrame, "Not enough coins or maximum speed reached.")
      }
    }
  }

  // ---------------- BIOME DISCOVERY WINDOW ----------------

  private def showBiomes(): Unit = {
    val dialog = new JDialog(gameFrame, "BIOME DISCOVERY", true)
    dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)

    val outer = new JPanel(new BorderLayout(10, 10))
    outer.setBackground(new Color(12, 20, 38))
    outer.setBorder(new EmptyBorder(16, 18, 16, 18))

    val header = new JPanel(new GridLayout(2, 1, 4, 4))
    header.setOpaque(false)
    val heading = new JLabel("BIOME DISCOVERY  -  22 WORLDS")
    heading.setForeground(new Color(255, 211, 99))
    heading.setFont(new Font("Arial", Font.BOLD, 25))
    val instruction = new JLabel(
      "Choose any unlocked biome. Train your speed to unlock more areas. Current speed: " + speed
    )
    instruction.setForeground(new Color(195, 216, 238))
    instruction.setFont(new Font("Arial", Font.PLAIN, 13))
    header.add(heading)
    header.add(instruction)
    outer.add(header, BorderLayout.NORTH)

    val list = new JPanel(new GridLayout(0, 2, 10, 10))
    list.setBackground(new Color(12, 20, 38))

    var i = 0
    while (i < biomes.length) {
      val index = i
      val biome = biomes(index)
      val unlocked = index == 0 || speed >= biome.gate
      val card = new JPanel(new BorderLayout(8, 5))
      card.setBackground(new Color(25, 38, 61))
      val borderColor = if (selectedBiome == index) new Color(255, 211, 99) else new Color(75, 100, 130)
      card.setBorder(BorderFactory.createCompoundBorder(
        BorderFactory.createLineBorder(borderColor, 2),
        new EmptyBorder(10, 12, 10, 12)
      ))

      val name = new JLabel(f"${index + 1}%02d  ${biome.name}")
      name.setForeground(Color.WHITE)
      name.setFont(new Font("Arial", Font.BOLD, 15))

      val required = new JLabel(
        "Required Speed: " + biome.gate +
          (if (index == 0) "  |  STARTER AREA" else "")
      )
      required.setForeground(new Color(185, 205, 228))
      required.setFont(new Font("Arial", Font.PLAIN, 12))

      val status = if (unlocked) {
        if (selectedBiome == index) "CURRENT BIOME" else "UNLOCKED - ENTER"
      } else {
        "LOCKED - NEED " + math.max(0, biome.gate - speed) + " MORE SPEED"
      }
      val choose = new JButton(status)
      choose.setEnabled(unlocked)
      choose.setFont(new Font("Arial", Font.BOLD, 11))
      choose.setBackground(if (unlocked) new Color(53, 145, 104) else new Color(75, 78, 90))
      choose.setForeground(Color.WHITE)
      choose.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          selectedBiome = index
          saveGame()
          if (gamePanel != null) gamePanel.repaint()
          dialog.dispose()
        }
      })

      val labels = new JPanel(new GridLayout(2, 1, 2, 2))
      labels.setOpaque(false)
      labels.add(name)
      labels.add(required)
      card.add(labels, BorderLayout.CENTER)
      card.add(choose, BorderLayout.SOUTH)
      list.add(card)
      i += 1
    }

    val scroll = new JScrollPane(list)
    scroll.setBorder(BorderFactory.createEmptyBorder())
    scroll.getViewport.setBackground(new Color(12, 20, 38))
    scroll.getVerticalScrollBar.setUnitIncrement(18)
    outer.add(scroll, BorderLayout.CENTER)

    val close = new JButton("CLOSE")
    close.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = dialog.dispose()
    })
    val footer = new JPanel(new FlowLayout(FlowLayout.RIGHT))
    footer.setOpaque(false)
    footer.add(close)
    outer.add(footer, BorderLayout.SOUTH)

    dialog.setContentPane(outer)
    dialog.setSize(820, 650)
    dialog.setLocationRelativeTo(gameFrame)
    dialog.setVisible(true)
  }

  // ---------------- ADMIN PANEL ----------------

  private def openAdminPanel(): Unit = {
    val passwordField = new JPasswordField(18)
    val answer = JOptionPane.showConfirmDialog(
      gameFrame, passwordField, "GAME CREATOR ADMIN LOGIN", JOptionPane.OK_CANCEL_OPTION
    )
    if (answer != JOptionPane.OK_OPTION) return

    val entered = new String(passwordField.getPassword)
    if (entered != AdminPassword) {
      JOptionPane.showMessageDialog(
        gameFrame, "Incorrect admin password.", "Access denied", JOptionPane.ERROR_MESSAGE
      )
      return
    }

    val choices = Array[AnyRef](
      "Give coins", "Set player speed", "Choose any biome", "Start spring event",
      "Stop spring event", "View player stats", "Close admin panel"
    )
    var open = true

    while (open) {
      val choice = JOptionPane.showOptionDialog(
        gameFrame,
        "SPRING SEASON - GAME CREATOR PANEL\nPlayer: " + playerName +
          "\nCoins: " + coins + "\nSpeed: " + speed +
          "\nBiome: " + biomes(selectedBiome).name,
        "ADMIN PANEL", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE,
        null, choices, choices(0)
      )
      choice match {
        case 0 =>
          val text = JOptionPane.showInputDialog(gameFrame, "Coins to add:")
          if (text != null) {
            try {
              val amount = text.trim.toLong
              if (amount > 0L) {
                coins = if (Long.MaxValue - coins < amount) Long.MaxValue else coins + amount
                saveGame()
              }
            } catch {
              case _: NumberFormatException => JOptionPane.showMessageDialog(gameFrame, "Enter a valid whole number.")
            }
          }
        case 1 =>
          val text = JOptionPane.showInputDialog(gameFrame, "Set speed (0 to " + MaxSpeed + "):")
          if (text != null) {
            try {
              val value = text.trim.toInt
              if (value >= 0 && value <= MaxSpeed) {
                speed = value
                saveGame()
                if (gamePanel != null) gamePanel.repaint()
              } else {
                JOptionPane.showMessageDialog(gameFrame, "Speed must be between 0 and " + MaxSpeed + ".")
              }
            } catch {
              case _: NumberFormatException => JOptionPane.showMessageDialog(gameFrame, "Enter a valid whole number.")
            }
          }
        case 2 => showBiomes()
        case 3 => springEventActive = true
        case 4 => springEventActive = false
        case 5 =>
          JOptionPane.showMessageDialog(
            gameFrame,
            "Player: " + playerName + "\nCoins: " + coins +
              "\nSpeed: " + speed + "\nEggs caught: " + eggsCaught +
              "\nCurrent biome: " + biomes(selectedBiome).name +
              "\nSpring event: " + springEventActive
          )
        case _ => open = false
      }
      if (gamePanel != null) gamePanel.repaint()
    }
  }

  // ---------------- GAME WORLD ----------------

  private case class Egg(var x: Double, var y: Double, color: Color, size: Int)

  private class GamePanel extends JPanel {
    private val eggs = ArrayBuffer.empty[Egg]
    private var playerX = 500.0
    private var ticks = 0
    private var leftDown = false
    private var rightDown = false
    private var running = false

    setPreferredSize(new Dimension(1100, 650))
    setBackground(new Color(145, 215, 250))
    setFocusable(true)
    installKeyboardControls()

    private val gameTimer = new Timer(16, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = updateGame()
    })

    private def installKeyboardControls(): Unit = {
      val input = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
      val actions = getActionMap
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "leftDown")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "leftDown")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "leftUp")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "leftUp")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "rightDown")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "rightDown")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "rightUp")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "rightUp")
      input.put(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0, false), "showBiomes")

      actions.put("leftDown", new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = leftDown = true
      })
      actions.put("leftUp", new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = leftDown = false
      })
      actions.put("rightDown", new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = rightDown = true
      })
      actions.put("rightUp", new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = rightDown = false
      })
      actions.put("showBiomes", new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = SpringSeasonCatchTheEgg.showBiomes()
      })
    }

    def startGame(): Unit = {
      running = true
      playerX = math.max(50.0, getWidth / 2.0)
      gameTimer.start()
      requestFocusInWindow()
    }

    def stopGame(): Unit = {
      running = false
      gameTimer.stop()
    }

    private def updateGame(): Unit = {
      if (!running || getWidth <= 0 || getHeight <= 0) return

      val movement = 4.5 + math.min(13.0, speed.toDouble / 75000.0)
      if (leftDown) playerX -= movement
      if (rightDown) playerX += movement
      playerX = math.max(45.0, math.min(getWidth.toDouble - 45.0, playerX))

      ticks += 1
      val spawnEvery = if (springEventActive) 38 else 72
      if (ticks % spawnEvery == 0) {
        val x = random.nextInt(math.max(1, getWidth - 40)).toDouble
        val b = biomes(selectedBiome)
        val palette = Array(
          new Color(255, 245, 245), new Color(255, 230, 120),
          new Color(180, 235, 255), new Color(255, 190, 220), b.accent
        )
        eggs += Egg(x, -35.0, palette(random.nextInt(palette.length)), 30)
      }

      val basketY = getHeight - 108.0
      val fallSpeed = 3.5 + math.min(8.0, speed.toDouble / 125000.0)
      var i = eggs.length - 1
      while (i >= 0) {
        val egg = eggs(i)
        egg.y += fallSpeed
        val overlaps = egg.x + egg.size >= playerX - 43.0 && egg.x <= playerX + 43.0
        val touches = egg.y + egg.size >= basketY && egg.y <= basketY + 23.0
        if (overlaps && touches) {
          if (coins <= Long.MaxValue - 10L) coins += 10L
          eggsCaught += 1L
          eggs.remove(i)
          saveGame()
        } else if (egg.y > getHeight + 10.0) {
          eggs.remove(i)
        }
        i -= 1
      }
      repaint()
    }

    override protected def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      try {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        val w = getWidth
        val h = getHeight
        val biome = biomes(selectedBiome)

        g.setPaint(new GradientPaint(0, 0, biome.sky, 0, h, biome.ground))
        g.fillRect(0, 0, w, h)

        // Biome-specific atmosphere and landscape decorations.
        if (selectedBiome == 4 || selectedBiome == 5 || selectedBiome == 9 || selectedBiome == 18) {
          g.setColor(new Color(235, 250, 255, 190))
          var sx = 20
          while (sx < w) {
            g.fillOval(sx, 75 + (sx % 4) * 20, 4, 4)
            sx += 65
          }
        } else {
          g.setColor(new Color(255, 235, 125, 220))
          g.fillOval(w - 120, 28, 72, 72)
        }

        drawCloud(g, 100, 78)
        drawCloud(g, 370, 52)
        drawCloud(g, 710, 90)

        // Distant hills.
        g.setColor(new Color(biome.ground.getRed, biome.ground.getGreen, biome.ground.getBlue, 150))
        g.fillOval(-90, h - 210, w / 2 + 170, 190)
        g.fillOval(w / 3, h - 230, w / 2 + 180, 210)
        g.setColor(biome.ground)
        g.fillRect(0, h - 70, w, 70)
        drawBiomeDecorations(g, w, h, selectedBiome, biome.accent)

        g.setColor(new Color(8, 18, 30, 185))
        g.fillRoundRect(14, 12, 440, 100, 14, 14)
        g.setColor(new Color(255, 218, 112))
        g.setFont(new Font("Arial", Font.BOLD, 21))
        g.drawString("SPRING SEASON - CATCH THE EGG", 27, 39)
        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.BOLD, 14))
        g.drawString("BIOME: " + biome.name, 27, 63)
        g.drawString("COINS: " + coins + "   SPEED: " + speed + "   EGGS: " + eggsCaught, 27, 87)
        g.setColor(new Color(255, 255, 255, 225))
        g.setFont(new Font("Arial", Font.BOLD, 14))
        g.drawString(if (springEventActive) "SPRING EVENT ACTIVE" else "SPRING EVENT PAUSED", w - 230, 35)

        var i = 0
        while (i < eggs.length) {
          drawEgg(g, eggs(i))
          i += 1
        }
        drawPlayer(g, h)

        g.setColor(new Color(5, 15, 25, 185))
        g.fillRoundRect(14, h - 48, math.min(w - 28, 760), 34, 12, 12)
        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.BOLD, 12))
        g.drawString("MOVE: LEFT / RIGHT or A / D     M: BIOMES     TREADMILL: +1 SPEED", 25, h - 26)
      } finally {
        g.dispose()
      }
    }

    private def drawCloud(g: Graphics2D, x: Int, y: Int): Unit = {
      g.setColor(new Color(255, 255, 255, 220))
      g.fillOval(x, y + 15, 65, 35)
      g.fillOval(x + 25, y, 58, 48)
      g.fillOval(x + 52, y + 15, 66, 35)
    }

    private def drawBiomeDecorations(g: Graphics2D, w: Int, h: Int, index: Int, accent: Color): Unit = {
      var x = 18
      while (x < w) {
        if (index == 0 || index == 1 || index == 10 || index == 14 || index == 19) {
          // Forest / prehistoric tree silhouettes.
          g.setColor(new Color(45, 82, 47, 190))
          g.fillRect(x + 12, h - 125, 9, 58)
          g.fillOval(x - 3, h - 155, 40, 45)
        } else if (index == 3 || index == 8) {
          // Desert rocks / volcano glow.
          g.setColor(new Color(accent.getRed, accent.getGreen, accent.getBlue, 180))
          g.fillOval(x, h - 102, 34, 28)
          if (index == 8) g.fillOval(x + 38, h - 94, 23, 19)
        } else if (index == 17 || index == 13 || index == 16 || index == 20) {
          // Candy / toy / disco / rainbow decorations.
          g.setColor(accent)
          g.fillOval(x + 4, h - 95 - (x % 3) * 5, 13, 13)
          g.setColor(Color.WHITE)
          g.fillOval(x + 18, h - 81, 8, 8)
        } else {
          // Small biome-colored flowers / crystals.
          g.setColor(accent)
          g.fillOval(x + 5, h - 92, 12, 12)
          g.setColor(new Color(255, 255, 255, 170))
          g.fillOval(x + 17, h - 83, 7, 7)
        }
        x += 95
      }
    }

    private def drawEgg(g: Graphics2D, egg: Egg): Unit = {
      g.setColor(new Color(40, 40, 40, 65))
      g.fillOval(egg.x.toInt + 3, egg.y.toInt + 4, egg.size, egg.size + 9)
      g.setColor(egg.color)
      g.fill(new Ellipse2D.Double(egg.x, egg.y, egg.size.toDouble, egg.size + 9.0))
      g.setColor(new Color(222, 110, 145))
      g.drawOval(egg.x.toInt + 7, egg.y.toInt + 10, 7, 7)
      g.drawOval(egg.x.toInt + 17, egg.y.toInt + 21, 6, 6)
    }

    private def drawPlayer(g: Graphics2D, h: Int): Unit = {
      val x = playerX.toInt
      val y = h - 205
      g.setColor(new Color(45, 55, 110))
      g.fillRoundRect(x - 22, y + 69, 17, 28, 8, 8)
      g.fillRoundRect(x + 5, y + 69, 17, 28, 8, 8)
      g.setColor(new Color(55, 45, 35))
      g.fillRoundRect(x - 25, y + 91, 22, 9, 6, 6)
      g.fillRoundRect(x + 3, y + 91, 22, 9, 6, 6)
      g.setColor(new Color(50, 110, 230))
      g.fillRoundRect(x - 28, y + 25, 56, 52, 14, 14)
      g.setColor(new Color(255, 200, 150))
      g.fillRoundRect(x - 38, y + 31, 13, 40, 9, 9)
      g.fillRoundRect(x + 25, y + 31, 13, 40, 9, 9)
      g.fillOval(x - 21, y - 15, 42, 44)
      g.setColor(new Color(65, 42, 25))
      g.fillArc(x - 22, y - 20, 44, 28, 0, 180)
      g.setColor(Color.BLACK)
      g.fillOval(x - 9, y + 2, 4, 5)
      g.fillOval(x + 7, y + 2, 4, 5)
      g.setColor(new Color(130, 80, 35))
      g.fillRoundRect(x - 45, h - 108, 90, 22, 7, 7)
      g.setColor(new Color(245, 205, 120))
      g.drawArc(x - 38, h - 125, 76, 32, 0, 180)
      g.setColor(Color.BLACK)
      g.setFont(new Font("Arial", Font.BOLD, 13))
      g.drawString(playerName, x - playerName.length * 4, y - 23)
    }
  }
}

// Kojo Code Exchange script entry point. This line is required when running as a Code Exchange sketch.
SpringSeasonCatchTheEgg.main(Array.empty[String])