Code Sketch


bixuu
By: Mhalsakant School
Category: Programming

import java.awt._
import java.awt.event._
import java.io._
import java.net._
import java.security.MessageDigest
import java.util.{Base64, Properties, Locale}
import javax.swing._
import scala.collection.mutable
import scala.util.Random
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import javax.imageio.ImageIO
setPenColor(blue)
object BixuuCardsBattle {

  // ======================================================
  // GAME DATA
  // ======================================================

  case class Fighter(
    name: String,
    power: Int,
    speed: Int,
    defense: Int,
    chakra: Int,
    special: String,
    color: Color
  )

  class PlayerAccount(
    val username: String,
    val passwordHash: String
  ) {
    var wins = 0
    var losses = 0
    var battles = 0
    val cards = mutable.ArrayBuffer[String]()
  }

  val random = new Random()

  val backgroundColor = new Color(14, 16, 30)
  val panelColor = new Color(28, 30, 48)
  val goldColor = new Color(255, 190, 60)
  val whiteColor = new Color(102, 51, 255)

  val seriesNames = Vector(
    "Naruto Shippuden",
    "Demon Slayer",
    "Jujutsu Kaisen",
    "Pokemon"
  )

  val stats = Vector("POWER", "SPEED", "DEFENSE", "CHAKRA")

  // ======================================================
  // NARUTO
  // ======================================================

  val narutoCards = Vector(
    Fighter("Naruto Uzumaki",96,94,90,99,"RASENGAN",new Color(255,140,30)),
    Fighter("Sasuke Uchiha",97,96,88,98,"CHIDORI",new Color(90,100,220)),
    Fighter("Sakura Haruno",86,80,94,78,"SUPER PUNCH",new Color(235,100,150)),
    Fighter("Kakashi Hatake",92,93,90,94,"LIGHTNING BLADE",new Color(80,155,195)),
    Fighter("Itachi Uchiha",98,95,90,99,"TSUKUYOMI",new Color(170,35,45)),
    Fighter("Madara Uchiha",100,96,99,100,"PERFECT SUSANOO",new Color(155,45,50)),
    Fighter("Obito Uchiha",97,92,96,98,"KAMUI",new Color(100,75,160)),
    Fighter("Pain",96,88,94,98,"ALMIGHTY PUSH",new Color(180,70,75)),
    Fighter("Minato Namikaze",95,100,87,96,"FLYING RAJIN",new Color(235,190,55)),
    Fighter("Gaara",90,78,99,92,"SAND SHIELD",new Color(175,105,75)),
    Fighter("Rock Lee",92,99,88,65,"LOTUS ATTACK",new Color(70,165,95)),
    Fighter("Shikamaru Nara",82,78,83,80,"SHADOW JUTSU",new Color(90,130,90)),
    Fighter("Jiraiya",94,85,92,96,"SAGE RASENGAN",new Color(200,95,45)),
    Fighter("Orochimaru",93,90,94,97,"SNAKE JUTSU",new Color(150,190,130)),
    Fighter("Neji Hyuga",88,92,85,88,"GENTLE FIST",new Color(180,195,210)),
    Fighter("Tsunade",94,79,98,90,"HEALING POWER",new Color(195,125,100))
  )

  // ======================================================
  // DEMON SLAYER
  // ======================================================

  val demonCards = Vector(
    Fighter("Tanjiro Kamado",94,93,88,94,"WATER BREATHING",new Color(50,135,180)),
    Fighter("Nezuko Kamado",93,91,92,90,"BLOOD DEMON ART",new Color(190,80,125)),
    Fighter("Zenitsu Agatsuma",96,100,78,83,"THUNDERCLAP",new Color(235,195,50)),
    Fighter("Inosuke Hashibira",92,96,85,80,"BEAST BREATHING",new Color(100,165,125)),
    Fighter("Giyu Tomioka",96,94,94,95,"WATER HASHIRA",new Color(65,100,185)),
    Fighter("Shinobu Kocho",88,97,81,91,"INSECT BREATHING",new Color(165,115,210)),
    Fighter("Kyojuro Rengoku",97,94,92,94,"FLAME BREATHING",new Color(225,100,35)),
    Fighter("Tengen Uzui",94,98,91,89,"SOUND BREATHING",new Color(180,180,190)),
    Fighter("Mitsuri Kanroji",93,91,89,90,"LOVE BREATHING",new Color(225,110,160)),
    Fighter("Muichiro Tokito",95,98,88,94,"MIST BREATHING",new Color(90,170,175)),
    Fighter("Akaza",98,96,96,93,"DESTRUCTIVE DEATH",new Color(195,90,100)),
    Fighter("Kokushibo",100,97,98,99,"MOON BREATHING",new Color(145,70,105))
  )

  // ======================================================
  // JUJUTSU KAISEN
  // ======================================================

  val jujutsuCards = Vector(
    Fighter("Yuji Itadori",91,93,89,88,"BLACK FLASH",new Color(190,80,105)),
    Fighter("Megumi Fushiguro",91,92,86,94,"TEN SHADOWS",new Color(75,95,160)),
    Fighter("Nobara Kugisaki",87,88,85,87,"RESONANCE",new Color(195,115,65)),
    Fighter("Satoru Gojo",100,99,99,100,"INFINITY",new Color(90,165,230)),
    Fighter("Ryomen Sukuna",100,98,99,100,"MALEVOLENT SHRINE",new Color(170,65,70)),
    Fighter("Yuta Okkotsu",97,91,93,99,"CURSED ENERGY",new Color(115,155,190)),
    Fighter("Maki Zenin",92,98,91,72,"CURSED TOOLS",new Color(75,145,100)),
    Fighter("Toge Inumaki",88,89,80,96,"CURSED SPEECH",new Color(170,155,190)),
    Fighter("Kento Nanami",93,85,94,88,"OVERTIME",new Color(190,165,90)),
    Fighter("Mahito",96,94,91,98,"IDLE TRANSFIGURATION",new Color(145,100,175))
  )

  // ======================================================
  // POKEMON
  // ======================================================

  val pokemonCards = Vector(
    Fighter("Pikachu",88,100,70,87,"THUNDERBOLT",new Color(240,205,45)),
    Fighter("Charizard",97,92,91,95,"FIRE BLAST",new Color(235,100,50)),
    Fighter("Mewtwo",100,96,92,100,"PSYSTRIKE",new Color(145,115,190)),
    Fighter("Eevee",72,85,70,75,"QUICK ATTACK",new Color(180,135,95)),
    Fighter("Lucario",93,94,89,92,"AURA SPHERE",new Color(65,115,190)),
    Fighter("Greninja",94,100,82,91,"WATER SHURIKEN",new Color(65,145,200)),
    Fighter("Gengar",91,92,80,95,"SHADOW BALL",new Color(120,85,165)),
    Fighter("Dragonite",96,86,95,94,"DRAGON CLAW",new Color(225,150,75)),
    Fighter("Blastoise",93,78,98,93,"HYDRO PUMP",new Color(65,125,200)),
    Fighter("Venusaur",91,75,96,90,"SOLAR BEAM",new Color(85,160,100)),
    Fighter("Snorlax",88,48,100,85,"BODY SLAM",new Color(110,155,170)),
    Fighter("Garchomp",97,95,93,91,"DRAGON RUSH",new Color(110,115,180))
  )

  def cardsFor(series: String): Vector[Fighter] = {
    if (series == "Naruto Shippuden") narutoCards
    else if (series == "Demon Slayer") demonCards
    else if (series == "Jujutsu Kaisen") jujutsuCards
    else pokemonCards
  }

  def findFighter(series: String, name: String): Fighter = {
    cardsFor(series).find(_.name == name).getOrElse(cardsFor(series).head)
  }

  // ======================================================
  // SAVED ACCOUNTS
  // ======================================================

  val dataDir = new File(
    System.getProperty("user.home"),
    ".bixuu_anime_fighting_cards"
  )

  var currentAccount: PlayerAccount = _

  def sha256(value: String): String = {
    val bytes = MessageDigest.getInstance("SHA-256")
      .digest(value.getBytes("UTF-8"))

    bytes.map(b => "%02x".format(b & 0xff)).mkString
  }

  def accountFile(username: String): File = {
    new File(
      dataDir,
      sha256(username.trim.toLowerCase(Locale.ROOT)) + ".properties"
    )
  }

  def encodeText(text: String): String = {
    Base64.getEncoder.encodeToString(text.getBytes("UTF-8"))
  }

  def decodeText(text: String): String = {
    new String(Base64.getDecoder.decode(text), "UTF-8")
  }

  def saveAccount(a: PlayerAccount): Unit = {
    dataDir.mkdirs()

    val p = new Properties()
    p.setProperty("username", a.username)
    p.setProperty("passwordHash", a.passwordHash)
    p.setProperty("wins", a.wins.toString)
    p.setProperty("losses", a.losses.toString)
    p.setProperty("battles", a.battles.toString)
    p.setProperty("cards", a.cards.map(encodeText).mkString(","))

    val out = new FileOutputStream(accountFile(a.username))

    try {
      p.store(out, "Bixuu Anime Fighting Cards")
    } finally {
      out.close()
    }
  }

  def loadAccount(username: String): PlayerAccount = {
    val file = accountFile(username)
    if (!file.exists()) return null

    val p = new Properties()
    val in = new FileInputStream(file)

    try {
      p.load(in)
    } finally {
      in.close()
    }

    val savedName = p.getProperty("username", "")
    val savedHash = p.getProperty("passwordHash", "")

    if (savedName.isEmpty || savedHash.isEmpty) return null

    val a = new PlayerAccount(savedName, savedHash)

    a.wins = p.getProperty("wins", "0").toInt
    a.losses = p.getProperty("losses", "0").toInt
    a.battles = p.getProperty("battles", "0").toInt

    val storedCards = p.getProperty("cards", "")

    if (storedCards.nonEmpty) {
      storedCards.split(",").filter(_.nonEmpty).foreach { item =>
        try {
          a.cards += decodeText(item)
        } catch {
          case _: Exception => ()
        }
      }
    }

    a
  }

  // ======================================================
  // MAIN WINDOW AND UI HELPERS
  // ======================================================

  var frame: JFrame = _

  def label(text: String, size: Int, color: Color): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Arial", Font.BOLD, size))
    l.setForeground(color)
    l
  }

  def button(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("Arial", Font.BOLD, 14))
    b.setForeground(Color.WHITE)
    b.setBackground(new Color(48,48,72))
    b.setFocusPainted(false)
    b.setBorder(BorderFactory.createLineBorder(goldColor, 2))
    b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
    b
  }

  def title(text: String): JLabel = {
    val l = label(text, 25, goldColor)
    l.setBorder(BorderFactory.createEmptyBorder(12,5,12,5))
    l
  }

  def plainPanel(layout: LayoutManager): JPanel = {
    val p = new JPanel(layout)
    p.setOpaque(false)
    p
  }

  def screen(p: JPanel, width: Int, height: Int): Unit = {
    frame.setContentPane(p)
    frame.setSize(width, height)
    frame.setLocationRelativeTo(null)
    frame.revalidate()
    frame.repaint()
    frame.setVisible(true)
  }

  def showMessage(
    text: String,
    caption: String = "BIXUU",
    kind: Int = JOptionPane.INFORMATION_MESSAGE
  ): Unit = {
    JOptionPane.showMessageDialog(frame, text, caption, kind)
  }

  // ======================================================
  // LOGIN / CREATE ACCOUNT
  // ======================================================

  def showLogin(): Unit = {
    val page = new JPanel(new BorderLayout(12,12))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(20,30,20,30))

    page.add(title("THE PLAY BATTLE OF CARDS CREATOR BIG 2"), BorderLayout.NORTH)

    val form = new JPanel(new GridLayout(4,1,8,8))
    form.setOpaque(false)
    form.setBorder(BorderFactory.createEmptyBorder(20,100,20,100))

    val username = new JTextField()
    val password = new JPasswordField()

    username.setFont(new Font("Arial", Font.PLAIN, 18))
    password.setFont(new Font("Arial", Font.PLAIN, 18))

    form.add(label("PLAYER ID / USERNAME",15,whiteColor))
    form.add(username)
    form.add(label("PASSWORD",15,whiteColor))
    form.add(password)

    page.add(form, BorderLayout.CENTER)

    val footer = plainPanel(new BorderLayout(8,8))

    footer.add(
      label("Create an account first, or log in with your existing ID.",12,Color.LIGHT_GRAY),
      BorderLayout.NORTH
    )

    val buttons = new JPanel(new GridLayout(1,2,8,8))
    buttons.setOpaque(false)

    val login = button("LOGIN")
    val create = button("CREATE ACCOUNT")

    login.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val name = username.getText.trim
        val pass = new String(password.getPassword)

        if (name.isEmpty || pass.isEmpty) {
          showMessage("Enter your ID and password.","LOGIN",JOptionPane.WARNING_MESSAGE)
        } else {
          try {
            val a = loadAccount(name)

            if (a == null) {
              showMessage(
                "Account not found. Create an account first, or restore an old backup.",
                "LOGIN FAILED",
                JOptionPane.ERROR_MESSAGE
              )
            } else if (a.passwordHash != sha256(pass)) {
              showMessage("Incorrect password.","LOGIN FAILED",JOptionPane.ERROR_MESSAGE)
            } else {
              currentAccount = a
              showHome()
            }
          } catch {
            case ex: Exception =>
              showMessage("Login error: " + ex.getMessage,"ERROR",JOptionPane.ERROR_MESSAGE)
          }
        }
      }
    })

    create.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val name = username.getText.trim
        val pass = new String(password.getPassword)

        if (!name.matches("[A-Za-z0-9_. -]{3,24}")) {
          showMessage("Use 3-24 characters: letters, numbers, spaces, _ . or -","INVALID ID",JOptionPane.WARNING_MESSAGE)
        } else if (pass.length < 4) {
          showMessage("Password must contain at least 4 characters.","PASSWORD",JOptionPane.WARNING_MESSAGE)
        } else if (accountFile(name).exists()) {
          showMessage("This ID already exists. Log in using its password.","ACCOUNT EXISTS",JOptionPane.WARNING_MESSAGE)
        } else {
          try {
            currentAccount = new PlayerAccount(name, sha256(pass))
            saveAccount(currentAccount)
            showMessage("Account created and saved! Welcome, " + name + ".")
            showHome()
          } catch {
            case ex: Exception =>
              showMessage("Could not save account: " + ex.getMessage,"SAVE ERROR",JOptionPane.ERROR_MESSAGE)
          }
        }
      }
    })

    buttons.add(login)
    buttons.add(create)
    footer.add(buttons, BorderLayout.CENTER)

    val restore = button("RESTORE OLD ID BACKUP")
    restore.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = restoreBackup()
    })

    footer.add(restore, BorderLayout.SOUTH)
    page.add(footer, BorderLayout.SOUTH)

    screen(page,850,620)
  }

  // ======================================================
  // ACCOUNT BACKUP / RESTORE
  // ======================================================

  def exportBackup(): Unit = {
    if (currentAccount == null) return

    try {
      saveAccount(currentAccount)

      val chooser = new JFileChooser()
      chooser.setDialogTitle("Export Account Backup")
      chooser.setSelectedFile(new File(currentAccount.username + ".bixuu"))

      if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
        Files.copy(
          accountFile(currentAccount.username).toPath,
          chooser.getSelectedFile.toPath,
          StandardCopyOption.REPLACE_EXISTING
        )

        showMessage("Backup exported. Keep the file safe.","BACKUP")
      }
    } catch {
      case ex: Exception =>
        showMessage("Backup failed: " + ex.getMessage,"ERROR",JOptionPane.ERROR_MESSAGE)
    }
  }

  def restoreBackup(): Unit = {
    val chooser = new JFileChooser()
    chooser.setDialogTitle("Restore Old Bixuu Account")

    if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
      try {
        val p = new Properties()
        val in = new FileInputStream(chooser.getSelectedFile)

        try {
          p.load(in)
        } finally {
          in.close()
        }

        val username = p.getProperty("username","").trim
        val hash = p.getProperty("passwordHash","")

        if (username.isEmpty || hash.isEmpty) {
          throw new IOException("Invalid Bixuu account backup.")
        }

        dataDir.mkdirs()

        val destination = accountFile(username)

        if (destination.exists()) {
          val choice = JOptionPane.showConfirmDialog(
            frame,
            "This ID already exists here. Replace it with the backup?",
            "RESTORE ACCOUNT",
            JOptionPane.YES_NO_OPTION
          )

          if (choice != JOptionPane.YES_OPTION) return
        }

        Files.copy(
          chooser.getSelectedFile.toPath,
          destination.toPath,
          StandardCopyOption.REPLACE_EXISTING
        )

        showMessage("Old account restored. Now log in using its original ID and password.","RESTORE")
      } catch {
        case ex: Exception =>
          showMessage("Restore failed: " + ex.getMessage,"ERROR",JOptionPane.ERROR_MESSAGE)
      }
    }
  }

  // ======================================================
  // HOME MENU
  // ======================================================

  def showHome(): Unit = {
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(18,24,18,24))

    page.add(title("THE PLAY BATTLE OF CARDS CREATOR BIG 2"), BorderLayout.NORTH)

    val center = plainPanel(new BorderLayout(8,8))

    center.add(
      label("WELCOME, " + currentAccount.username.toUpperCase(Locale.ROOT),17,Color.WHITE),
      BorderLayout.NORTH
    )

    val menu = new JPanel(new GridLayout(4,2,10,10))
    menu.setOpaque(false)
    menu.setBorder(BorderFactory.createEmptyBorder(15,50,15,50))

    val anime = button("ANIME FIGHTING CARDS")
    val pokemon = button("POKEMON FIGHTING CARDS")
    val multiplayer = button("MULTIPLAYER ROOMS - UP TO 10 PLAYERS")
    val uno = button("UNO CARD GAME")
    val profile = button("MY PROFILE")
    val collection = button("CARD COLLECTION")
    val backup = button("EXPORT ACCOUNT BACKUP")
    val logout = button("LOG OUT")

    anime.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showSeriesMenu()
    })

    pokemon.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showDifficulty("Pokemon")
    })

    multiplayer.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showMultiplayerMenu()
    })

    uno.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = startUno()
    })

    profile.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showProfile()
    })

    collection.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showCollection()
    })

    backup.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = exportBackup()
    })

    logout.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        closePeer()
        currentAccount = null
        showLogin()
      }
    })

    menu.add(anime)
    menu.add(pokemon)
    menu.add(multiplayer)
    menu.add(uno)
    menu.add(profile)
    menu.add(collection)
    menu.add(backup)
    menu.add(logout)

    center.add(menu, BorderLayout.CENTER)
    page.add(center, BorderLayout.CENTER)

    screen(page,900,720)
  }

  // ======================================================
  // ANIME SERIES SELECT
  // ======================================================

  def showSeriesMenu(): Unit = {
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.add(title("CHOOSE YOUR ANIME"), BorderLayout.NORTH)

    val options = new JPanel(new GridLayout(4,1,10,10))
    options.setOpaque(false)
    options.setBorder(BorderFactory.createEmptyBorder(20,100,20,100))

    val naruto = button("NARUTO SHIPPUDEN")
    val demon = button("DEMON SLAYER")
    val jujutsu = button("JUJUTSU KAISEN")
    val back = button("BACK TO HOME")

    naruto.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showDifficulty("Naruto Shippuden")
    })

    demon.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showDifficulty("Demon Slayer")
    })

    jujutsu.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showDifficulty("Jujutsu Kaisen")
    })

    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showHome()
    })

    options.add(naruto)
    options.add(demon)
    options.add(jujutsu)
    options.add(back)

    page.add(options, BorderLayout.CENTER)

    screen(page,800,630)
  }

  // ======================================================
  // DIFFICULTY AND CAMPAIGN
  // ======================================================

  var currentSeries = "Naruto Shippuden"
  var currentDifficulty = "MEDIUM"
  var currentLevel = 1

  def showDifficulty(series: String): Unit = {
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)

    page.add(title(series.toUpperCase(Locale.ROOT) + " - DIFFICULTY"),BorderLayout.NORTH)

    val options = new JPanel(new GridLayout(4,1,10,10))
    options.setOpaque(false)
    options.setBorder(BorderFactory.createEmptyBorder(20,100,20,100))

    val easy = button("EASY - 30 LEVELS")
    val medium = button("MEDIUM - 30 LEVELS")
    val hard = button("HARD - 30 LEVELS")
    val back = button("BACK")

    easy.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = startCampaign(series,"EASY")
    })

    medium.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = startCampaign(series,"MEDIUM")
    })

    hard.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = startCampaign(series,"HARD")
    })

    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (series == "Pokemon") showHome() else showSeriesMenu()
      }
    })

    options.add(easy)
    options.add(medium)
    options.add(hard)
    options.add(back)

    page.add(options,BorderLayout.CENTER)

    screen(page,800,630)
  }

  var playerCard: Fighter = _
  var enemyCard: Fighter = _

  var selected = false
  var battleReady = false
  var battleResolved = false
  var isLanBattle = false
  var currentOpponentName = "ROBOT"

  var playerView: NinjaCardView = _
  var enemyView: NinjaCardView = _
  var rootPanel: JPanel = _
  var actionPanel: JPanel = _
  var statPanel: JPanel = _
  var messageLabel: JLabel = _
  var scoreLabel: JLabel = _
  var statButtons = Vector.empty[JButton]

  def startCampaign(series: String, diff: String): Unit = {
    closePeer()
    isLanBattle = false
    currentSeries = series
    currentDifficulty = diff
    currentLevel = 1
    showBattle()
  }

  def statValue(f: Fighter, stat: String): Int = {
    if (stat == "POWER") f.power
    else if (stat == "SPEED") f.speed
    else if (stat == "DEFENSE") f.defense
    else if (stat == "CHAKRA") f.chakra
    else 0
  }

  def robotStat(f: Fighter, stat: String): Int = {
    val difficultyBonus =
      if (currentDifficulty == "EASY") -7
      else if (currentDifficulty == "HARD") 8
      else 0

    val levelBonus = (currentLevel - 1) / 5

    math.max(1, math.min(100, statValue(f,stat) + difficultyBonus + levelBonus))
  }


  // ======================================================
  // CHARACTER ARTWORK (LOCAL FILES + POKEMON SPRITE FALLBACK)
  // ======================================================

  val characterImageCache = mutable.Map[String, Image]()
  val characterImagesLoading = mutable.Set[String]()
  var advancedBattleArtworkPanel: JPanel = _

  val pokemonSpriteIds: Map[String, Int] = Map(
    "Pikachu" -> 25,
    "Charizard" -> 6,
    "Mewtwo" -> 150,
    "Eevee" -> 133,
    "Lucario" -> 448,
    "Greninja" -> 658,
    "Gengar" -> 94,
    "Dragonite" -> 149,
    "Blastoise" -> 9,
    "Venusaur" -> 3,
    "Snorlax" -> 143,
    "Garchomp" -> 445
  )

  def imageSlug(name: String): String = {
    name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", "")
  }

  def getCharacterArtwork(fighter: Fighter): Image = {
    if (fighter == null) return null

    val key = fighter.name
    var cached: Image = null
    var shouldLoad = false

    characterImageCache.synchronized {
      cached = characterImageCache.getOrElse(key, null)
      if (cached == null && !characterImagesLoading.contains(key)) {
        characterImagesLoading += key
        shouldLoad = true
      }
    }

    if (shouldLoad) {
      val seriesForLoad = currentSeries
      val fForLoad = fighter
      val loader = new Thread(new Runnable {
        override def run(): Unit = {
          var image: Image = null
          val slug = imageSlug(fForLoad.name)

          try {
            val candidates = Vector(
              new File("assets/cards/" + slug + ".png"),
              new File("assets/cards/" + slug + ".jpg"),
              new File("assets/pokemon/" + slug + ".png")
            )

            val local = candidates.find(_.isFile)
            if (local.isDefined) {
              image = ImageIO.read(local.get)
            } else if (seriesForLoad == "Pokemon" && pokemonSpriteIds.contains(fForLoad.name)) {
              val spriteId = pokemonSpriteIds(fForLoad.name)
              val url = new URL("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" + spriteId + ".png")
              val connection = url.openConnection()
              connection.setConnectTimeout(2500)
              connection.setReadTimeout(2500)
              val stream = connection.getInputStream
              try image = ImageIO.read(stream) finally stream.close()
            }
          } catch {
            case _: Exception => image = null
          }

          characterImageCache.synchronized {
            characterImagesLoading -= key
            if (image != null) characterImageCache(key) = image
          }

          SwingUtilities.invokeLater(new Runnable {
            override def run(): Unit = {
              if (playerView != null && playerView.fighter != null && playerView.fighter.name == key) playerView.repaint()
              if (enemyView != null && enemyView.fighter != null && enemyView.fighter.name == key) enemyView.repaint()
              if (advancedBattleArtworkPanel != null) advancedBattleArtworkPanel.repaint()
            }
          })
        }
      })
      loader.setDaemon(true)
      loader.setName("card-art-" + imageSlug(key))
      loader.start()
    }

    cached
  }

  def drawCharacterArtwork(g: Graphics2D, fighter: Fighter, x: Int, y: Int, w: Int, h: Int): Unit = {
    if (fighter == null) return

    val art = getCharacterArtwork(fighter)
    if (art != null && art.getWidth(null) > 0 && art.getHeight(null) > 0) {
      val scale = math.min(w.toDouble / art.getWidth(null), h.toDouble / art.getHeight(null))
      val dw = math.max(1, (art.getWidth(null) * scale).toInt)
      val dh = math.max(1, (art.getHeight(null) * scale).toInt)
      g.drawImage(art, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh, null)
    } else {
      // Original fallback illustration appears while offline or before an image loads.
      g.setColor(new Color(0, 0, 0, 55))
      g.fillOval(x + w / 4, y + h / 8, w / 2, h * 3 / 4)
      g.setColor(fighter.color.brighter())
      g.fillOval(x + w / 3, y + h / 5, w / 3, h / 3)
      g.fillOval(x + w / 5, y + h / 2, w * 3 / 5, h / 3)
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, math.max(18, h / 2)))
      val initial = fighter.name.take(1)
      val tw = g.getFontMetrics.stringWidth(initial)
      g.drawString(initial, x + (w - tw) / 2, y + h * 3 / 5)
    }
  }

  // ======================================================
  // CARD PANEL
  // ======================================================

  class NinjaCardView(
    val fighter: Fighter,
    val isPlayerCard: Boolean
  ) extends JPanel {

    var revealed = false
    var chosen = false
    var flipScale = 1.0

    setPreferredSize(new Dimension(250,310))
    setMinimumSize(new Dimension(185,250))
    setBackground(panelColor)
    setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))

    addMouseListener(new MouseAdapter {
      override def mouseClicked(e: MouseEvent): Unit = {
        if (isPlayerCard && revealed && battleReady && !battleResolved) {
          selected = true
          chosen = true
          messageLabel.setText("YOUR CARD SELECTED! CHOOSE A STAT.")
          updateBattleControls()
          repaint()
        }
      }
    })

    override 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

        g.translate(w / 2.0,0.0)
        g.scale(flipScale,1.0)
        g.translate(-w / 2.0,0.0)

        if (!revealed) {
          g.setColor(new Color(65,35,110))
          g.fillRoundRect(10,10,w-20,h-20,24,24)

          g.setColor(goldColor)
          g.setStroke(new BasicStroke(4))
          g.drawRoundRect(18,18,w-36,h-36,20,20)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,26))

          val txt = "BIXUU CARD"
          val tw = g.getFontMetrics.stringWidth(txt)

          g.drawString(txt,(w-tw)/2,h/2)

          g.setFont(new Font("Arial",Font.BOLD,13))

          val role =
            if (isPlayerCard) "YOUR CARD"
            else if (isLanBattle) currentOpponentName
            else "ROBOT ENEMY"

          val rw = g.getFontMetrics.stringWidth(role)
          g.drawString(role,(w-rw)/2,h-35)

        } else {
          g.setColor(fighter.color)
          g.fillRoundRect(10,10,w-20,h-20,24,24)

          g.setColor(new Color(18,20,34))
          g.fillRoundRect(20,20,w-40,h-40,18,18)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,16))

          val nw = g.getFontMetrics.stringWidth(fighter.name)
          g.drawString(fighter.name,math.max(22,(w-nw)/2),48)

          val family =
            if (currentSeries == "Pokemon") "POKEMON"
            else if (currentSeries == "Naruto Shippuden") "NINJA"
            else if (currentSeries == "Demon Slayer") "DEMON SLAYER"
            else "CURSED ENERGY"

          g.setColor(fighter.color.brighter())
          g.setFont(new Font("Arial",Font.BOLD,17))

          val fw = g.getFontMetrics.stringWidth(family)
          g.drawString(family,(w-fw)/2,83)

          g.setColor(new Color(8, 10, 18))
          g.fillRoundRect(w / 2 - 42, 90, 84, 54, 12, 12)
          drawCharacterArtwork(g, fighter, w / 2 - 38, 91, 76, 52)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,13))

          g.drawString("POWER     " + fighter.power,28,162)
          g.drawString("SPEED       " + fighter.speed,28,184)
          g.drawString("DEFENSE  " + fighter.defense,28,206)
          g.drawString("CHAKRA   " + fighter.chakra,28,228)

          g.setColor(new Color(255,220,120))
          g.setFont(new Font("Arial",Font.BOLD,10))
          g.drawString(fighter.special,28,252)

          g.setFont(new Font("Arial",Font.BOLD,12))

          val status =
            if (isPlayerCard && chosen) "SELECTED!"
            else if (isPlayerCard) "TAP TO SELECT"
            else "ENEMY CARD"

          if (isPlayerCard && chosen) g.setColor(Color.GREEN)
          else g.setColor(goldColor)

          g.drawString(status,28,h-27)

          if (isPlayerCard && chosen) g.setColor(Color.GREEN)
          else g.setColor(goldColor)

          g.setStroke(new BasicStroke(if (isPlayerCard && chosen) 4 else 2))
          g.drawRoundRect(10,10,w-20,h-20,24,24)
        }
      } finally {
        g.dispose()
      }
    }
  }

  // ======================================================
  // FLIP ANIMATION
  // ======================================================

  def flipCard(view: NinjaCardView, done: () => Unit): Unit = {
    var step = 0
    var timer: Timer = null

    timer = new Timer(28,new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        step += 1

        if (step <= 5) {
          view.flipScale = 1.0 - step * 0.18
        } else if (step == 6) {
          view.flipScale = 0.10
          view.revealed = true
        } else {
          view.flipScale = math.min(1.0,(step-5)*0.20)
        }

        view.repaint()

        if (step >= 11) {
          timer.stop()
          view.flipScale = 1.0
          view.repaint()
          done()
        }
      }
    })

    timer.start()
  }

  // ======================================================
  // BATTLE SCREEN
  // ======================================================

  def showBattle(): Unit = {
    selected = false
    battleReady = false
    battleResolved = false

    if (!isLanBattle) {
      val set = cardsFor(currentSeries)
      playerCard = set(random.nextInt(set.size))

      val enemies = set.filterNot(_.name == playerCard.name)
      enemyCard = enemies(random.nextInt(enemies.size))
      currentOpponentName = "ROBOT"
    }

    rootPanel = new JPanel(new BorderLayout(10,10))
    rootPanel.setBackground(backgroundColor)
    rootPanel.setBorder(BorderFactory.createEmptyBorder(12,15,12,15))

    rootPanel.add(
      title(currentSeries.toUpperCase(Locale.ROOT) + " - LEVEL " + currentLevel + "/30"),
      BorderLayout.NORTH
    )

    val arena = new JPanel(new GridLayout(1,2,12,8))
    arena.setOpaque(false)

    val playerBox = plainPanel(new BorderLayout(5,5))
    playerBox.add(label("YOUR CARD",15,Color.WHITE),BorderLayout.NORTH)

    playerView = new NinjaCardView(playerCard,true)
    playerBox.add(playerView,BorderLayout.CENTER)

    val enemyBox = plainPanel(new BorderLayout(5,5))

    val enemyTitle =
      if (isLanBattle) currentOpponentName
      else "ROBOT ENEMY"

    enemyBox.add(label(enemyTitle,15,Color.WHITE),BorderLayout.NORTH)

    enemyView = new NinjaCardView(enemyCard,false)
    enemyBox.add(enemyView,BorderLayout.CENTER)

    arena.add(playerBox)
    arena.add(enemyBox)

    rootPanel.add(arena,BorderLayout.CENTER)

    actionPanel = plainPanel(new BorderLayout(6,6))

    messageLabel = label("GET READY - FLIPPING CARDS!",15,goldColor)
    actionPanel.add(messageLabel,BorderLayout.NORTH)

    statPanel = new JPanel(new GridLayout(2,2,8,8))
    statPanel.setOpaque(false)

    statButtons = stats.map { stat =>
      val b = button("CHOOSE " + stat)
      b.setEnabled(false)

      b.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = fight(stat)
      })

      statPanel.add(b)
      b
    }

    actionPanel.add(statPanel,BorderLayout.CENTER)

    val footer = plainPanel(new BorderLayout(5,5))

    scoreLabel = label(
      "WINS: " + currentAccount.wins +
      " | LOSSES: " + currentAccount.losses +
      " | CARDS: " + currentAccount.cards.size,
      12,
      Color.WHITE
    )

    footer.add(scoreLabel,BorderLayout.CENTER)

    val back = button("BACK TO HOME")

    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (isLanBattle) {
          closePeer()
          isLanBattle = false
        }
        showHome()
      }
    })

    footer.add(back,BorderLayout.SOUTH)
    actionPanel.add(footer,BorderLayout.SOUTH)

    rootPanel.add(actionPanel,BorderLayout.SOUTH)

    screen(rootPanel,920,800)

    flipCard(playerView, () => {
      flipCard(enemyView, () => {
        battleReady = true
        messageLabel.setText("CARDS REVEALED! TAP YOUR CARD!")
        updateBattleControls()
      })
    })
  }

  def updateBattleControls(): Unit = {
    statButtons.foreach(_.setEnabled(battleReady && selected && !battleResolved))
    if (playerView != null) playerView.repaint()
  }

  // ======================================================
  // ROBOT BATTLE AND REWARDS
  // ======================================================

  def fight(stat: String): Unit = {
    if (!battleReady || !selected || battleResolved) return

    if (isLanBattle && peer != null) {
      fightLan(stat)
      return
    }

    battleResolved = true
    updateBattleControls()

    val mine = statValue(playerCard,stat)
    val theirs = robotStat(enemyCard,stat)

    currentAccount.battles += 1

    if (mine > theirs) {
      currentAccount.wins += 1
      currentAccount.cards += (currentSeries + "|" + enemyCard.name)
      saveAccount(currentAccount)

      showResult(
        "YOU WIN!",
        stat + ": " + mine + " vs " + theirs,
        "NEW CARD UNLOCKED: " + enemyCard.name,
        true,
        false
      )

    } else if (mine < theirs) {
      currentAccount.losses += 1
      saveAccount(currentAccount)

      showResult(
        "ROBOT WINS!",
        stat + ": " + mine + " vs " + theirs,
        "No reward this time. Try again!",
        false,
        false
      )

    } else {
      saveAccount(currentAccount)

      showResult(
        "DRAW!",
        stat + ": " + mine + " vs " + theirs,
        "Both cards have equal stats.",
        false,
        false
      )
    }
  }

  def showResult(
    result: String,
    detail: String,
    reward: String,
    won: Boolean,
    multiplayerResult: Boolean
  ): Unit = {
    scoreLabel.setText(
      "WINS: " + currentAccount.wins +
      " | LOSSES: " + currentAccount.losses +
      " | CARDS: " + currentAccount.cards.size
    )

    messageLabel.setText(
      "<html><center>" + result +
      "<br>" + detail +
      "<br>" + reward +
      "</center></html>"
    )

    statButtons.foreach(_.setEnabled(false))

    val resultPanel = new JPanel(new GridLayout(2,1,6,6))
    resultPanel.setOpaque(false)

    val resultColor =
      if (won) new Color(100,255,130)
      else new Color(255,165,100)

    resultPanel.add(label(result,18,resultColor))
    resultPanel.add(label(reward,13,whiteColor))

    val next =
      if (multiplayerResult) button("RETURN TO HOME")
      else if (currentLevel >= 30) button("FINISH CAMPAIGN")
      else button("NEXT LEVEL")

    next.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (multiplayerResult) {
          closePeer()
          isLanBattle = false
          showHome()
        } else if (currentLevel >= 30) {
          showMessage(
            "You completed the 30-level campaign!\n" +
            "Wins: " + currentAccount.wins +
            "\nCards collected: " + currentAccount.cards.size,
            "CAMPAIGN COMPLETE"
          )
          showHome()
        } else {
          currentLevel += 1
          showBattle()
        }
      }
    })

    resultPanel.add(next)

    actionPanel.remove(statPanel)
    actionPanel.add(resultPanel,BorderLayout.CENTER)

    actionPanel.revalidate()
    actionPanel.repaint()
    rootPanel.revalidate()
    rootPanel.repaint()
  }

  // ======================================================
  // PROFILE AND COLLECTION
  // ======================================================

  def collectionText: String = {
    if (currentAccount.cards.isEmpty) {
      "YOUR COLLECTION IS EMPTY.\n\nWin a battle to earn a card."
    } else {
      val grouped = currentAccount.cards.groupBy((s: String) => s).toSeq.sortBy(_._1)

      val lines = grouped.map {
        case (key,copies) =>
          val parts = key.split("\\|",2)

          val series =
            if (parts.length > 1) parts(0)
            else "Unknown"

          val name =
            if (parts.length > 1) parts(1)
            else key

          series + " - " + name + " x" + copies.size
      }

      "MY EARNED CARDS\n\n" +
      lines.mkString("\n") +
      "\n\nTOTAL CARDS: " + currentAccount.cards.size +
      "\nUNIQUE CARDS: " + grouped.size
    }
  }

  def showCollection(): Unit = {
    showTextPage("MY CARD COLLECTION",collectionText)
  }

  def showProfile(): Unit = {
    val a = currentAccount

    val content =
      "PLAYER PROFILE\n\n" +
      "Player ID: " + a.username + "\n\n" +
      "Battles: " + a.battles + "\n" +
      "Wins: " + a.wins + "\n" +
      "Losses: " + a.losses + "\n" +
      "Cards: " + a.cards.size + "\n\n" +
      "Account save folder:\n" + dataDir.getAbsolutePath +
      "\n\nExport an account backup to move your account to another PC."

    showTextPage("MY PROFILE",content)
  }

  def showTextPage(pageTitle: String, body: String): Unit = {
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(14,18,14,18))

    page.add(title(pageTitle),BorderLayout.NORTH)

    val text = new JTextArea(body)
    text.setEditable(false)
    text.setLineWrap(true)
    text.setWrapStyleWord(true)
    text.setFont(new Font("Arial",Font.PLAIN,16))
    text.setForeground(whiteColor)
    text.setBackground(panelColor)
    text.setCaretPosition(0)
    text.setBorder(BorderFactory.createEmptyBorder(14,14,14,14))

    page.add(new JScrollPane(text),BorderLayout.CENTER)

    val back = button("BACK TO HOME")
    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showHome()
    })

    val footer = plainPanel(new FlowLayout(FlowLayout.CENTER))
    footer.add(back)
    page.add(footer,BorderLayout.SOUTH)

    screen(page,800,700)
  }

  // ======================================================
  // MULTIPLAYER ROOMS - UP TO 10 PLAYERS
  // ======================================================

  val LAN_PORT = 50677

  class LanPeer(
    val socket: Socket,
    val input: DataInputStream,
    val output: DataOutputStream,
    val remoteUser: String,
    val series: String
  )

  var peer: LanPeer = _
  var activeServer: ServerSocket = _

  def closePeer(): Unit = {
    try {
      if (peer != null) peer.socket.close()
    } catch {
      case _: Exception => ()
    }

    peer = null

    try {
      if (activeServer != null) activeServer.close()
    } catch {
      case _: Exception => ()
    }

    activeServer = null
    closeAdvancedNetworkNoUI()
  }

  def localIPAddresses(): String = {
    val found = mutable.ArrayBuffer[String]()

    try {
      val interfaces = NetworkInterface.getNetworkInterfaces

      while (interfaces.hasMoreElements) {
        val network = interfaces.nextElement()

        if (network.isUp && !network.isLoopback) {
          val addresses = network.getInetAddresses

          while (addresses.hasMoreElements) {
            val address = addresses.nextElement()

            if (address.isInstanceOf[Inet4Address] && !address.isLoopbackAddress) {
              found += address.getHostAddress
            }
          }
        }
      }
    } catch {
      case _: Exception => ()
    }

    if (found.isEmpty) "IP not detected. Check your Wi-Fi."
    else found.distinct.mkString(", ")
  }

  def showMultiplayerMenu(): Unit = showAdvancedRoomBrowser()

  // IMPORTANT: Array[Object] avoids the Scala Array[String] type error.

  def hostLanMatch(): Unit = {
    val options: Array[Object] =
      seriesNames.map(s => s.asInstanceOf[Object]).toArray

    val chosen = JOptionPane.showInputDialog(
      frame,
      "Choose the card series:",
      "HOST MATCH",
      JOptionPane.QUESTION_MESSAGE,
      null,
      options,
      options(0)
    )

    if (chosen == null) return

    val series = chosen.toString

    var server: ServerSocket = null

    try {
      server = new ServerSocket(LAN_PORT)
      server.setSoTimeout(180000)
      activeServer = server
    } catch {
      case ex: Exception =>
        showMessage(
          "Could not start host: " + ex.getMessage,
          "HOST ERROR",
          JOptionPane.ERROR_MESSAGE
        )
        return
    }

    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)

    page.add(title("WAITING FOR PLAYER"),BorderLayout.NORTH)

    val info =
      "Series: " + series + "\n\n" +
      "Your local IP address:\n" + localIPAddresses() + "\n\n" +
      "Share this IP with your friend.\n" +
      "Your friend should choose JOIN and enter this IP.\n\n" +
      "Allow the game through Windows Firewall for private networks."

    page.add(label(info,15,whiteColor),BorderLayout.CENTER)

    val cancel = button("CANCEL")

    cancel.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        try server.close() catch {
          case _: Exception => ()
        }
        activeServer = null
        showMultiplayerMenu()
      }
    })

    page.add(cancel,BorderLayout.SOUTH)
    screen(page,720,540)

    val serverRef = server

    val thread = new Thread(new Runnable {
      override def run(): Unit = {
        var socket: Socket = null

        try {
          socket = serverRef.accept()
          socket.setTcpNoDelay(true)

          val in = new DataInputStream(
            new BufferedInputStream(socket.getInputStream())
          )

          val out = new DataOutputStream(
            new BufferedOutputStream(socket.getOutputStream())
          )

          val command = in.readUTF()
          val remoteName = in.readUTF()

          if (command != "JOIN") throw new IOException("Invalid join request.")

          val myCard = randomFighter(series)

          out.writeUTF("BIXUU-LAN-V1")
          out.writeUTF(series)
          out.writeUTF(currentAccount.username)
          out.writeUTF(myCard.name)
          out.flush()

          val remoteCardName = in.readUTF()
          val remoteCard = findFighter(series,remoteCardName)

          val connected = new LanPeer(socket,in,out,remoteName,series)

          SwingUtilities.invokeLater(new Runnable {
            override def run(): Unit = {
              activeServer = null
              startLanBattle(connected,series,myCard,remoteCard)
            }
          })

          try serverRef.close() catch {
            case _: Exception => ()
          }

        } catch {
          case ex: Exception =>
            try {
              if (socket != null) socket.close()
            } catch {
              case _: Exception => ()
            }

            SwingUtilities.invokeLater(new Runnable {
              override def run(): Unit = {
                if (activeServer eq serverRef) {
                  activeServer = null
                  showMessage(
                    "Host error or timeout: " + ex.getMessage,
                    "MULTIPLAYER",
                    JOptionPane.ERROR_MESSAGE
                  )
                  showMultiplayerMenu()
                }
              }
            })
        }
      }
    })

    thread.setDaemon(true)
    thread.start()
  }

  def joinLanMatch(): Unit = {
    val ip = JOptionPane.showInputDialog(
      frame,
      "Enter the host's local IP address:",
      "JOIN TRIAL MATCH",
      JOptionPane.QUESTION_MESSAGE
    )

    if (ip == null || ip.trim.isEmpty) return

    val cleanIP = ip.trim

    val page = new JPanel(new BorderLayout())
    page.setBackground(backgroundColor)
    page.add(title("CONNECTING..."),BorderLayout.CENTER)

    screen(page,600,350)

    val thread = new Thread(new Runnable {
      override def run(): Unit = {
        var socket: Socket = null

        try {
          socket = new Socket()
          socket.connect(new InetSocketAddress(cleanIP,LAN_PORT),10000)
          socket.setTcpNoDelay(true)

          val in = new DataInputStream(
            new BufferedInputStream(socket.getInputStream())
          )

          val out = new DataOutputStream(
            new BufferedOutputStream(socket.getOutputStream())
          )

          out.writeUTF("JOIN")
          out.writeUTF(currentAccount.username)
          out.flush()

          val protocol = in.readUTF()

          if (protocol != "BIXUU-LAN-V1") {
            throw new IOException("The host uses a different game version.")
          }

          val series = in.readUTF()
          val remoteName = in.readUTF()
          val remoteCardName = in.readUTF()

          val myCard = randomFighter(series)

          out.writeUTF(myCard.name)
          out.flush()

          val remoteCard = findFighter(series,remoteCardName)

          val connected = new LanPeer(socket,in,out,remoteName,series)

          SwingUtilities.invokeLater(new Runnable {
            override def run(): Unit = {
              startLanBattle(connected,series,myCard,remoteCard)
            }
          })

        } catch {
          case ex: Exception =>
            try {
              if (socket != null) socket.close()
            } catch {
              case _: Exception => ()
            }

            SwingUtilities.invokeLater(new Runnable {
              override def run(): Unit = {
                showMessage(
                  "Could not join: " + ex.getMessage +
                  "\nCheck the IP, Wi-Fi and firewall settings.",
                  "JOIN ERROR",
                  JOptionPane.ERROR_MESSAGE
                )
                showMultiplayerMenu()
              }
            })
        }
      }
    })

    thread.setDaemon(true)
    thread.start()
  }

  def randomFighter(series: String): Fighter = {
    val set = cardsFor(series)
    set(random.nextInt(set.size))
  }

  def startLanBattle(
    connected: LanPeer,
    series: String,
    mine: Fighter,
    opponent: Fighter
  ): Unit = {
    peer = connected

    currentSeries = series
    currentDifficulty = "MEDIUM"
    currentLevel = 1

    isLanBattle = true
    currentOpponentName = connected.remoteUser

    playerCard = mine
    enemyCard = opponent

    showBattle()
  }

  def fightLan(stat: String): Unit = {
    val connected = peer

    if (connected == null) {
      showMessage("Multiplayer connection was lost.","ERROR",JOptionPane.ERROR_MESSAGE)
      closePeer()
      isLanBattle = false
      showHome()
      return
    }

    battleResolved = true
    updateBattleControls()

    messageLabel.setText("WAITING FOR YOUR FRIEND...")

    val thread = new Thread(new Runnable {
      override def run(): Unit = {
        try {
          connected.output.writeUTF(stat)
          connected.output.flush()

          val otherStat = connected.input.readUTF()

          SwingUtilities.invokeLater(new Runnable {
            override def run(): Unit = finishLanBattle(stat,otherStat)
          })

        } catch {
          case ex: Exception =>
            SwingUtilities.invokeLater(new Runnable {
              override def run(): Unit = {
                showMessage(
                  "Connection lost: " + ex.getMessage,
                  "MULTIPLAYER ERROR",
                  JOptionPane.ERROR_MESSAGE
                )
                closePeer()
                isLanBattle = false
                showHome()
              }
            })
        }
      }
    })

    thread.setDaemon(true)
    thread.start()
  }

  def finishLanBattle(myStat: String, otherStat: String): Unit = {
    val mine = statValue(playerCard,myStat)
    val theirs = statValue(enemyCard,otherStat)

    currentAccount.battles += 1

    if (mine > theirs) {
      currentAccount.wins += 1
      currentAccount.cards += (currentSeries + "|" + enemyCard.name)
      saveAccount(currentAccount)

      showResult(
        "YOU WIN THE MULTIPLAYER TRIAL!",
        myStat + ": " + mine + " vs " + otherStat + ": " + theirs,
        "NEW CARD: " + enemyCard.name,
        true,
        true
      )

    } else if (mine < theirs) {
      currentAccount.losses += 1
      saveAccount(currentAccount)

      showResult(
        "YOUR FRIEND WINS!",
        myStat + ": " + mine + " vs " + otherStat + ": " + theirs,
        "Try another trial match!",
        false,
        true
      )

    } else {
      saveAccount(currentAccount)

      showResult(
        "MULTIPLAYER DRAW!",
        myStat + ": " + mine + " vs " + otherStat + ": " + theirs,
        "Both players chose equal values.",
        false,
        true
      )
    }
  }


  // ======================================================
  // ADVANCED LAN ROOMS: ROOM BROWSER + 10-PLAYER BATTLES
  // 60-second join window, optional password, ready state,
  // automatic 30-round tournament, reconnect-free round flow.
  // ======================================================

  val ADVANCED_DISCOVERY_PORT = 50678
  val ADVANCED_MAX_PLAYERS = 10

  def netEncode(value: String): String = {
    Base64.getUrlEncoder.withoutPadding().encodeToString(value.getBytes("UTF-8"))
  }

  def netDecode(value: String): String = {
    new String(Base64.getUrlDecoder.decode(value), "UTF-8")
  }

  case class DiscoveredRoom(
    id: String,
    roomName: String,
    series: String,
    host: String,
    port: Int,
    players: Int,
    maxPlayers: Int,
    passworded: Boolean,
    secondsLeft: Int,
    lastSeen: Long
  ) {
    override def toString: String = {
      val lockText = if (passworded) "LOCKED" else "OPEN"
      roomName + "  |  " + series + "  |  " + players + "/" + maxPlayers +
        "  |  " + lockText + "  |  " + secondsLeft + "s  |  " + host
    }
  }

  case class NetLobbyPlayer(id: Int, name: String, ready: Boolean, score: Int)

  class AdvancedServerPlayer(
    val id: Int,
    val name: String,
    val socket: Socket,
    val output: PrintWriter
  ) {
    @volatile var ready = false
    @volatile var played = false
    @volatile var score = 0
    @volatile var card: Fighter = null

    def send(line: String): Unit = output.synchronized {
      output.println(line)
      output.flush()
    }
  }

  class AdvancedRoomServer(
    val roomName: String,
    val series: String,
    val password: String,
    val hostUserName: String
  ) {
    val serverSocket = new ServerSocket(0)
    val port = serverSocket.getLocalPort
    val roomId = java.lang.Long.toHexString(System.currentTimeMillis()) + "-" + random.nextInt(1000000)
    serverSocket.setSoTimeout(1000)

    @volatile var running = true
    @volatile var joinOpen = true
    @volatile var phase = "LOBBY"
    @volatile var roundNo = 0
    @volatile var roundStat = "POWER"
    @volatile var joinDeadline = System.currentTimeMillis() + 60000L
    @volatile var lastInfo = "Room opened. Players may join for 60 seconds."

    private val players = mutable.LinkedHashMap[Int, AdvancedServerPlayer]()
    private var nextId = 1
    private var hostId = -1
    private var resultDeadline = 0L
    private val rematchVotes = mutable.Set[Int]()
    private val announcer = new DatagramSocket()
    announcer.setBroadcast(true)

    def start(): Unit = {
      val acceptor = new Thread(new Runnable {
        override def run(): Unit = acceptLoop()
      })
      acceptor.setDaemon(true)
      acceptor.setName("card-room-accept-" + port)
      acceptor.start()

      val ticker = new Thread(new Runnable {
        override def run(): Unit = tickLoop()
      })
      ticker.setDaemon(true)
      ticker.setName("card-room-ticker-" + port)
      ticker.start()
    }

    def stop(): Unit = {
      running = false
      joinOpen = false
      try serverSocket.close() catch { case _: Exception => () }
      try announcer.close() catch { case _: Exception => () }
      synchronized {
        players.values.foreach { p => try p.socket.close() catch { case _: Exception => () } }
        players.clear()
      }
    }

    private def acceptLoop(): Unit = {
      while (running) {
        try {
          val socket = serverSocket.accept()
          socket.setTcpNoDelay(true)
          val worker = new Thread(new Runnable {
            override def run(): Unit = handleSocket(socket)
          })
          worker.setDaemon(true)
          worker.setName("card-room-player")
          worker.start()
        } catch {
          case _: java.net.SocketTimeoutException => ()
          case _: Exception => if (running) lastInfo = "Room connection error."
        }
      }
    }

    private def handleSocket(socket: Socket): Unit = {
      var player: AdvancedServerPlayer = null
      var reader: BufferedReader = null

      try {
        socket.setSoTimeout(10000)
        reader = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
        val writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream, "UTF-8"), true)
        val hello = reader.readLine()
        if (hello == null) throw new IOException("Missing join request")

        val bits = hello.split("\\|", -1)
        if (bits.length < 3 || bits(0) != "JOIN") {
          writer.println("ERROR|" + netEncode("Invalid room join request."))
          writer.flush()
          return
        }

        val username = netDecode(bits(1)).trim
        val suppliedPassword = netDecode(bits(2))

        player = registerPlayer(username, suppliedPassword, socket, writer)
        if (player == null) return

        socket.setSoTimeout(0)
        var line = reader.readLine()
        var keepGoing = true
        while (running && keepGoing && line != null) {
          keepGoing = handleCommand(player, line)
          if (keepGoing) line = reader.readLine()
        }
      } catch {
        case _: Exception => ()
      } finally {
        if (player != null) removePlayer(player.id)
        try socket.close() catch { case _: Exception => () }
      }
    }

    private def registerPlayer(
      username: String,
      suppliedPassword: String,
      socket: Socket,
      writer: PrintWriter
    ): AdvancedServerPlayer = synchronized {
      if (!running) {
        writer.println("ERROR|" + netEncode("This room is closed.")); writer.flush(); return null
      }
      if (!joinOpen || System.currentTimeMillis() >= joinDeadline) {
        writer.println("ERROR|" + netEncode("The 60-second join window has closed.")); writer.flush(); return null
      }
      if (players.size >= ADVANCED_MAX_PLAYERS) {
        writer.println("ERROR|" + netEncode("This room already has 10 players.")); writer.flush(); return null
      }
      if (username.isEmpty || !username.matches("[A-Za-z0-9_. -]{3,24}")) {
        writer.println("ERROR|" + netEncode("Player name must be 3-24 letters, numbers, spaces, dot, dash or underscore.")); writer.flush(); return null
      }
      if (password.nonEmpty && suppliedPassword != password) {
        writer.println("ERROR|" + netEncode("Incorrect room password.")); writer.flush(); return null
      }
      if (players.values.exists(_.name.equalsIgnoreCase(username))) {
        writer.println("ERROR|" + netEncode("That player name is already in this room.")); writer.flush(); return null
      }

      val player = new AdvancedServerPlayer(nextId, username, socket, writer)
      nextId += 1
      if (username.equalsIgnoreCase(hostUserName) || hostId < 0) hostId = player.id
      players(player.id) = player

      player.send("WELCOME|" + player.id + "|" + netEncode(series) + "|" + netEncode(roomId) + "|" + netEncode(roomName))
      lastInfo = username + " joined the room."
      broadcastStateLocked()
      player
    }

    private def handleCommand(player: AdvancedServerPlayer, line: String): Boolean = {
      val bits = line.split("\\|", -1)
      if (bits.isEmpty) return true

      bits(0) match {
        case "LEAVE" => false

        case "READY" =>
          synchronized {
            if (phase == "LOBBY" || phase == "READY") {
              player.ready = !player.ready
              lastInfo = player.name + (if (player.ready) " is READY." else " is NOT READY.")
              broadcastStateLocked()
              maybeStartLocked()
            }
          }
          true

        case "START" =>
          synchronized {
            if (player.id == hostId && !joinOpen && phase == "READY") maybeStartLocked()
          }
          true

        case "PLAY" =>
          synchronized {
            if (phase == "PLAYING" && !player.played) {
              player.played = true
              lastInfo = player.name + " played round " + roundNo + "."
              broadcastStateLocked()
              finishRoundIfReadyLocked()
            }
          }
          true

        case "REMATCH" =>
          synchronized {
            if (phase == "GAMEOVER") {
              rematchVotes += player.id
              if (players.nonEmpty && players.values.forall(p => rematchVotes.contains(p.id))) {
                players.values.foreach { p =>
                  p.ready = false
                  p.played = false
                  p.score = 0
                  p.card = null
                }
                rematchVotes.clear()
                roundNo = 0
                joinOpen = true
                joinDeadline = System.currentTimeMillis() + 60000L
                phase = "LOBBY"
                lastInfo = "Rematch lobby opened. New players can join for 60 seconds."
              }
              broadcastStateLocked()
            }
          }
          true

        case _ => true
      }
    }

    private def removePlayer(id: Int): Unit = synchronized {
      val removed = players.remove(id)
      if (removed.isDefined) {
        rematchVotes -= id
        lastInfo = removed.get.name + " left the room."
        if (players.isEmpty) {
          if (!joinOpen && phase != "GAMEOVER") {
            phase = "LOBBY"
            joinOpen = true
            joinDeadline = System.currentTimeMillis() + 60000L
          }
        }
        broadcastStateLocked()
        if (phase == "PLAYING") finishRoundIfReadyLocked()
      }
    }

    private def serializePlayersLocked(): String = {
      players.values.map { p =>
        p.id + "\t" + p.name + "\t" + p.ready + "\t" + p.score
      }.mkString("\n")
    }

    private def broadcastStateLocked(): Unit = {
      val secs = if (joinOpen) math.max(0, ((joinDeadline - System.currentTimeMillis() + 999L) / 1000L).toInt) else 0
      val line = "STATE|" + phase + "|" + secs + "|" + roundNo + "|" + netEncode(serializePlayersLocked())
      players.values.foreach(_.send(line))
    }

    private def sendBeacon(): Unit = {
      if (!running || !joinOpen) return
      val count = synchronized { players.size }
      val seconds = math.max(0, ((joinDeadline - System.currentTimeMillis() + 999L) / 1000L).toInt)
      val line = "ROOM|" + netEncode(roomId) + "|" + netEncode(roomName) + "|" + netEncode(series) + "|" + port + "|" + count + "|" + ADVANCED_MAX_PLAYERS + "|" + password.nonEmpty + "|" + seconds
      try {
        val bytes = line.getBytes("UTF-8")
        val packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("255.255.255.255"), ADVANCED_DISCOVERY_PORT)
        announcer.send(packet)
      } catch {
        case _: Exception => ()
      }
    }

    private def tickLoop(): Unit = {
      while (running) {
        try {
          synchronized {
            val now = System.currentTimeMillis()
            if (joinOpen && now >= joinDeadline) {
              if (players.size < 2) {
                // If only the host joined, re-open another minute rather than strand the room.
                joinDeadline = now + 60000L
                joinOpen = true
                phase = "LOBBY"
                lastInfo = "Need at least 2 players. The room is open for another minute."
                players.values.foreach(p => p.ready = false)
              } else {
                joinOpen = false
                phase = "READY"
                lastInfo = "Join time ended. Everyone must press READY."
              }
              broadcastStateLocked()
              maybeStartLocked()
            }

            if (phase == "RESULTS" && now >= resultDeadline) {
              if (roundNo >= 30) {
                phase = "GAMEOVER"
                lastInfo = "All 30 rounds are complete."
                players.values.foreach(_.send("GAMEOVER|" + netEncode(scoreboardLocked())))
                broadcastStateLocked()
              } else {
                roundNo += 1
                startRoundLocked()
              }
            } else if (phase == "LOBBY" || phase == "READY") {
              broadcastStateLocked()
              maybeStartLocked()
            }
          }
          sendBeacon()
          Thread.sleep(1000L)
        } catch {
          case _: InterruptedException => running = false
          case _: Exception => Thread.sleep(250L)
        }
      }
    }

    private def maybeStartLocked(): Unit = {
      if (!joinOpen && phase == "READY" && players.size >= 2 && players.values.forall(_.ready)) {
        roundNo = 1
        players.values.foreach(p => p.score = 0)
        lastInfo = "The 30-round battle has started!"
        startRoundLocked()
      }
    }

    private def startRoundLocked(): Unit = {
      if (players.size < 2) {
        phase = "READY"
        lastInfo = "Waiting for at least 2 connected players."
        broadcastStateLocked()
        return
      }
      phase = "PLAYING"
      roundStat = stats((roundNo - 1) % stats.size)
      players.values.foreach { p =>
        p.card = randomFighter(series)
        p.played = false
      }
      lastInfo = "Round " + roundNo + "/30: compare " + roundStat + "."
      broadcastStateLocked()
      players.values.foreach { p =>
        p.send("ROUND|" + roundNo + "|30|" + roundStat + "|" + netEncode(p.card.name))
      }
    }

    private def finishRoundIfReadyLocked(): Unit = {
      if (phase != "PLAYING") return
      if (players.size < 2) {
        phase = "GAMEOVER"
        val board = scoreboardLocked()
        players.values.foreach(_.send("GAMEOVER|" + netEncode(board)))
        broadcastStateLocked()
        return
      }
      if (players.values.forall(_.played)) {
        val scored = players.values.toVector.map(p => (p, statValue(p.card, roundStat)))
        val maxValue = scored.map(_._2).max
        val winners = scored.filter(_._2 == maxValue).map(_._1)
        winners.foreach(_.score += 1)
        phase = "RESULTS"
        resultDeadline = System.currentTimeMillis() + 4500L
        val winnerNames = winners.map(_.name).mkString(", ")
        val values = scored.map { case (p, value) => p.name + ": " + value }.mkString(" | ")
        val result = "ROUND " + roundNo + "/30 - " + roundStat + "\n" +
          "Winner(s): " + winnerNames + "\n" + values + "\n\n" + scoreboardLocked()
        lastInfo = "Round " + roundNo + " complete. Next round starts automatically."
        broadcastStateLocked()
        players.values.foreach(_.send("RESULT|" + roundNo + "|" + roundStat + "|" + netEncode(winnerNames) + "|" + netEncode(result)))
      }
    }

    private def scoreboardLocked(): String = {
      players.values.toVector.sortBy(p => (-p.score, p.name.toLowerCase(Locale.ROOT))).map { p =>
        p.name + "  ?  " + p.score + " round wins"
      }.mkString("\n")
    }
  }

  class AdvancedNetClient(
    val socket: Socket,
    val reader: BufferedReader,
    val writer: PrintWriter,
    val hostRole: Boolean,
    val roomName: String,
    val series: String,
    val id: Int
  ) {
    @volatile var running = true
    @volatile var phase = "LOBBY"
    @volatile var secondsLeft = 60
    @volatile var roundNo = 0
    @volatile var players = Vector.empty[NetLobbyPlayer]
    @volatile var currentCard: Fighter = null
    @volatile var currentStat = "POWER"
    @volatile var hasPlayed = false
    @volatile var resultText = "Waiting for the first round."
    @volatile var finalScoreboard = ""
    @volatile var screenMode = ""
    @volatile var lastProcessedResultRound = 0

    def send(command: String): Unit = {
      if (running) writer.synchronized {
        writer.println(command)
        writer.flush()
      }
    }

    def closeNow(): Unit = {
      running = false
      try writer.println("LEAVE") catch { case _: Exception => () }
      try writer.flush() catch { case _: Exception => () }
      try socket.close() catch { case _: Exception => () }
    }

    def startReader(): Unit = {
      val readerThread = new Thread(new Runnable {
        override def run(): Unit = {
          try {
            var line = reader.readLine()
            while (running && line != null) {
              val incoming = line
              SwingUtilities.invokeLater(new Runnable {
                override def run(): Unit = handleAdvancedMessage(AdvancedNetClient.this, incoming)
              })
              line = reader.readLine()
            }
          } catch {
            case _: Exception => ()
          } finally {
            if (running) {
              running = false
              SwingUtilities.invokeLater(new Runnable {
                override def run(): Unit = {
                  if (activeAdvancedClient eq AdvancedNetClient.this) {
                    showMessage("Room connection ended. Check Wi-Fi or host status.", "ROOM DISCONNECTED", JOptionPane.WARNING_MESSAGE)
                    leaveAdvancedRoom()
                  }
                }
              })
            }
          }
        }
      })
      readerThread.setDaemon(true)
      readerThread.setName("card-room-reader-" + id)
      readerThread.start()
    }
  }

  @volatile var activeAdvancedServer: AdvancedRoomServer = _
  @volatile var activeAdvancedClient: AdvancedNetClient = _
  @volatile var roomDiscoveryRunning = false
  @volatile var roomDiscoverySocket: DatagramSocket = _
  var roomDiscoveryThread: Thread = _
  val discoveredRooms = mutable.LinkedHashMap[String, DiscoveredRoom]()
  var roomListModel: DefaultListModel[DiscoveredRoom] = _
  var roomListWidget: JList[DiscoveredRoom] = _
  var roomBrowserStatusLabel: JLabel = _
  var advancedLobbyInfoLabel: JLabel = _
  var advancedLobbyPlayersArea: JTextArea = _
  var advancedReadyButton: JButton = _
  var advancedHostStartButton: JButton = _
  var advancedBattleInfoLabel: JLabel = _
  var advancedBattlePlayButton: JButton = _
  var advancedBattleScoreArea: JTextArea = _
  var advancedGameOverArea: JTextArea = _

  def closeAdvancedNetworkNoUI(): Unit = {
    stopRoomDiscovery()
    val client = activeAdvancedClient
    activeAdvancedClient = null
    if (client != null) client.closeNow()
    val server = activeAdvancedServer
    activeAdvancedServer = null
    if (server != null) server.stop()
  }

  def stopRoomDiscovery(): Unit = {
    roomDiscoveryRunning = false
    val ds = roomDiscoverySocket
    roomDiscoverySocket = null
    if (ds != null) try ds.close() catch { case _: Exception => () }
  }

  def startRoomDiscovery(): Unit = {
    stopRoomDiscovery()
    discoveredRooms.synchronized { discoveredRooms.clear() }

    try {
      val ds = new DatagramSocket(null)
      ds.setReuseAddress(true)
      ds.bind(new InetSocketAddress(ADVANCED_DISCOVERY_PORT))
      ds.setSoTimeout(1000)
      roomDiscoverySocket = ds
      roomDiscoveryRunning = true

      roomDiscoveryThread = new Thread(new Runnable {
        override def run(): Unit = {
          while (roomDiscoveryRunning && (roomDiscoverySocket eq ds)) {
            try {
              val buffer = new Array[Byte](2048)
              val packet = new DatagramPacket(buffer, buffer.length)
              ds.receive(packet)
              val line = new String(packet.getData, packet.getOffset, packet.getLength, "UTF-8")
              val p = line.split("\\|", -1)
              if (p.length >= 9 && p(0) == "ROOM") {
                val item = DiscoveredRoom(
                  netDecode(p(1)), netDecode(p(2)), netDecode(p(3)),
                  packet.getAddress.getHostAddress, p(4).toInt, p(5).toInt,
                  p(6).toInt, p(7).toBoolean, p(8).toInt, System.currentTimeMillis()
                )
                discoveredRooms.synchronized {
                  discoveredRooms(item.id) = item
                }
                SwingUtilities.invokeLater(new Runnable {
                  override def run(): Unit = refreshRoomListUI()
                })
              }
            } catch {
              case _: java.net.SocketTimeoutException =>
                val cutoff = System.currentTimeMillis() - 5000L
                discoveredRooms.synchronized {
                  discoveredRooms.retain { case (_, room) => room.lastSeen >= cutoff }
                }
                SwingUtilities.invokeLater(new Runnable {
                  override def run(): Unit = refreshRoomListUI()
                })
              case _: Exception => if (roomDiscoveryRunning) Thread.sleep(250L)
            }
          }
        }
      })
      roomDiscoveryThread.setDaemon(true)
      roomDiscoveryThread.setName("card-room-discovery")
      roomDiscoveryThread.start()
    } catch {
      case ex: Exception =>
        if (roomBrowserStatusLabel != null) roomBrowserStatusLabel.setText("Auto room scan unavailable. Use JOIN BY IP:PORT.")
    }
  }

  def refreshRoomListUI(): Unit = {
    if (roomListModel == null || roomListWidget == null) return

    val selectedId = Option(roomListWidget.getSelectedValue).map(_.id).getOrElse("")
    val snapshot = discoveredRooms.synchronized {
      discoveredRooms.values.toVector.sortBy(r => (r.roomName.toLowerCase(Locale.ROOT), r.host))
    }

    roomListModel.clear()
    snapshot.foreach(roomListModel.addElement)
    if (selectedId.nonEmpty) {
      val idx = snapshot.indexWhere(_.id == selectedId)
      if (idx >= 0) roomListWidget.setSelectedIndex(idx)
    }

    if (roomBrowserStatusLabel != null) {
      roomBrowserStatusLabel.setText(
        if (snapshot.isEmpty) "Searching your Wi-Fi for rooms? Use JOIN BY IP:PORT if a room does not appear."
        else "Found " + snapshot.size + " room(s). Double-click a room or select it and press JOIN."
      )
    }
  }

  def showAdvancedRoomBrowser(): Unit = {
    stopRoomDiscovery()
    roomListModel = new DefaultListModel[DiscoveredRoom]()
    roomListWidget = new JList[DiscoveredRoom](roomListModel)
    roomListWidget.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
    roomListWidget.setFont(new Font("Arial", Font.PLAIN, 15))
    roomListWidget.setBackground(panelColor)
    roomListWidget.setForeground(whiteColor)
    roomListWidget.setSelectionBackground(new Color(80, 75, 125))

    roomListWidget.addMouseListener(new MouseAdapter {
      override def mouseClicked(e: MouseEvent): Unit = {
        if (e.getClickCount == 2) joinSelectedAdvancedRoom()
      }
    })

    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(16,20,16,20))
    page.add(title("MULTIPLAYER ROOM BROWSER - MAX 10 PLAYERS"), BorderLayout.NORTH)

    val center = new JPanel(new BorderLayout(8,8))
    center.setOpaque(false)
    center.add(new JScrollPane(roomListWidget), BorderLayout.CENTER)

    roomBrowserStatusLabel = label("Searching your Wi-Fi for rooms?", 13, Color.LIGHT_GRAY)
    center.add(roomBrowserStatusLabel, BorderLayout.SOUTH)
    page.add(center, BorderLayout.CENTER)

    val buttons = new JPanel(new GridLayout(2,3,8,8))
    buttons.setOpaque(false)
    val create = button("CREATE ROOM")
    val join = button("JOIN SELECTED ROOM")
    val manual = button("JOIN BY IP:PORT")
    val refresh = button("REFRESH ROOMS")
    val back = button("BACK TO HOME")
    val explain = label("60-second join window ? optional password ? 30 auto rounds", 12, goldColor)

    create.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = createAdvancedRoom() })
    join.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = joinSelectedAdvancedRoom() })
    manual.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = joinAdvancedRoomByAddress() })
    refresh.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { discoveredRooms.synchronized { discoveredRooms.clear() }; refreshRoomListUI(); startRoomDiscovery() } })
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { stopRoomDiscovery(); showHome() } })

    buttons.add(create)
    buttons.add(join)
    buttons.add(manual)
    buttons.add(refresh)
    buttons.add(back)
    buttons.add(explain)
    page.add(buttons, BorderLayout.SOUTH)

    screen(page, 1000, 690)
    startRoomDiscovery()
  }

  def createAdvancedRoom(): Unit = {
    val nameField = new JTextField(currentAccount.username + "'s Room", 20)
    val passwordField = new JPasswordField(20)
    val options: Array[Object] = seriesNames.map(s => s.asInstanceOf[Object]).toArray
    val seriesCombo = new JComboBox[Object](options)
    seriesCombo.setSelectedItem("Pokemon")

    val form = new JPanel(new GridLayout(3,2,8,8))
    form.add(new JLabel("Room name")); form.add(nameField)
    form.add(new JLabel("Card series")); form.add(seriesCombo)
    form.add(new JLabel("Optional password")); form.add(passwordField)

    val result = JOptionPane.showConfirmDialog(frame, form, "CREATE MULTIPLAYER ROOM", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)
    if (result != JOptionPane.OK_OPTION) return

    val roomName = nameField.getText.trim
    val series = seriesCombo.getSelectedItem.toString
    val password = new String(passwordField.getPassword)

    if (!roomName.matches("[A-Za-z0-9_. -]{3,32}")) {
      showMessage("Room name must be 3-32 letters, numbers, spaces, dot, dash or underscore.", "INVALID ROOM", JOptionPane.WARNING_MESSAGE)
      return
    }

    try {
      val server = new AdvancedRoomServer(roomName, series, password, currentAccount.username)
      activeAdvancedServer = server
      server.start()
      stopRoomDiscovery()
      connectToAdvancedRoom("127.0.0.1", server.port, password, true)
    } catch {
      case ex: Exception =>
        showMessage("Could not create room: " + ex.getMessage, "ROOM ERROR", JOptionPane.ERROR_MESSAGE)
    }
  }

  def joinSelectedAdvancedRoom(): Unit = {
    if (roomListWidget == null || roomListWidget.getSelectedValue == null) {
      showMessage("Select a room first. Press REFRESH ROOMS if the list is empty.", "JOIN ROOM", JOptionPane.INFORMATION_MESSAGE)
      return
    }

    val room = roomListWidget.getSelectedValue
    val password = if (room.passworded) {
      val value = JOptionPane.showInputDialog(frame, "Enter the room password:", "LOCKED ROOM", JOptionPane.QUESTION_MESSAGE)
      if (value == null) return else value
    } else ""

    stopRoomDiscovery()
    connectToAdvancedRoom(room.host, room.port, password, false)
  }

  def joinAdvancedRoomByAddress(): Unit = {
    val address = JOptionPane.showInputDialog(frame, "Enter host IP:PORT (example 192.168.1.5:52341):", "JOIN BY IP:PORT", JOptionPane.QUESTION_MESSAGE)
    if (address == null || address.trim.isEmpty) return

    val parts = address.trim.split(":", -1)
    if (parts.length != 2) {
      showMessage("Enter both IP address and port, like 192.168.1.5:52341.", "INVALID ADDRESS", JOptionPane.WARNING_MESSAGE)
      return
    }

    val port = try parts(1).toInt catch { case _: Exception => -1 }
    if (port < 1 || port > 65535) {
      showMessage("Port must be between 1 and 65535.", "INVALID PORT", JOptionPane.WARNING_MESSAGE)
      return
    }
    val password = JOptionPane.showInputDialog(frame, "Room password (leave blank if none):", "ROOM PASSWORD", JOptionPane.QUESTION_MESSAGE)
    if (password == null) return
    stopRoomDiscovery()
    connectToAdvancedRoom(parts(0).trim, port, password, false)
  }

  def connectToAdvancedRoom(host: String, port: Int, password: String, hostRole: Boolean): Unit = {
    val waitPage = new JPanel(new BorderLayout())
    waitPage.setBackground(backgroundColor)
    waitPage.add(title("CONNECTING TO ROOM?"), BorderLayout.CENTER)
    screen(waitPage, 600, 320)

    val thread = new Thread(new Runnable {
      override def run(): Unit = {
        var socket: Socket = null
        try {
          socket = new Socket()
          socket.connect(new InetSocketAddress(host, port), 8000)
          socket.setTcpNoDelay(true)
          socket.setSoTimeout(10000)
          val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
          val writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream, "UTF-8"), true)
          writer.println("JOIN|" + netEncode(currentAccount.username) + "|" + netEncode(password))
          writer.flush()

          val first = reader.readLine()
          if (first == null) throw new IOException("Room did not respond.")
          val parts = first.split("\\|", -1)
          if (parts(0) == "ERROR") throw new IOException(netDecode(if (parts.length > 1) parts(1) else netEncode("Join failed.")))
          if (parts.length < 5 || parts(0) != "WELCOME") throw new IOException("Invalid room response.")

          val id = parts(1).toInt
          val series = netDecode(parts(2))
          val roomName = netDecode(parts(4))
          socket.setSoTimeout(0)
          val client = new AdvancedNetClient(socket, reader, writer, hostRole, roomName, series, id)

          SwingUtilities.invokeLater(new Runnable {
            override def run(): Unit = {
              currentSeries = series
              isLanBattle = false
              activeAdvancedClient = client
              client.startReader()
              showAdvancedLobby(client)
            }
          })
        } catch {
          case ex: Exception =>
            if (socket != null) try socket.close() catch { case _: Exception => () }
            SwingUtilities.invokeLater(new Runnable {
              override def run(): Unit = {
                if (hostRole) {
                  val server = activeAdvancedServer
                  activeAdvancedServer = null
                  if (server != null) server.stop()
                }
                showMessage("Could not join room: " + ex.getMessage + "\nCheck Wi-Fi, password and Windows Firewall.", "ROOM CONNECTION", JOptionPane.ERROR_MESSAGE)
                showAdvancedRoomBrowser()
              }
            })
        }
      }
    })
    thread.setDaemon(true)
    thread.setName("card-room-connect")
    thread.start()
  }

  def parseLobbyPlayers(rawEncoded: String): Vector[NetLobbyPlayer] = {
    val raw = try netDecode(rawEncoded) catch { case _: Exception => "" }
    if (raw.isEmpty) return Vector.empty
    raw.split("\n", -1).toVector.flatMap { line =>
      val bits = line.split("\t", -1)
      if (bits.length >= 4) {
        try Some(NetLobbyPlayer(bits(0).toInt, bits(1), bits(2).toBoolean, bits(3).toInt))
        catch { case _: Exception => None }
      } else None
    }
  }

  def handleAdvancedMessage(client: AdvancedNetClient, line: String): Unit = {
    if (client == null || !client.running) return
    val p = line.split("\\|", -1)
    if (p.isEmpty) return

    try {
      p(0) match {
        case "STATE" if p.length >= 5 =>
          client.phase = p(1)
          client.secondsLeft = p(2).toInt
          client.roundNo = p(3).toInt
          client.players = parseLobbyPlayers(p(4))
          if (client.phase == "LOBBY" || client.phase == "READY") {
            if (client.screenMode != "LOBBY") showAdvancedLobby(client)
            else refreshAdvancedLobby(client)
          } else if (client.phase == "PLAYING" || client.phase == "RESULTS") {
            if (client.screenMode != "BATTLE") showAdvancedBattle(client)
            else refreshAdvancedBattle(client)
          } else if (client.phase == "GAMEOVER") {
            if (client.screenMode != "GAMEOVER") showAdvancedGameOver(client)
          }

        case "ROUND" if p.length >= 5 =>
          client.roundNo = p(1).toInt
          if (client.roundNo == 1 && client.lastProcessedResultRound >= 30) client.lastProcessedResultRound = 0
          client.currentStat = p(3)
          client.currentCard = findFighter(client.series, netDecode(p(4)))
          client.hasPlayed = false
          client.resultText = "Round " + client.roundNo + "/30: compare " + client.currentStat + "."
          client.phase = "PLAYING"
          if (client.screenMode != "BATTLE") showAdvancedBattle(client)
          refreshAdvancedBattle(client)

        case "RESULT" if p.length >= 5 =>
          val resultRound = p(1).toInt
          val winnersText = netDecode(p(3))
          client.resultText = netDecode(p(4))
          client.phase = "RESULTS"
          client.hasPlayed = true

          if (currentAccount != null && resultRound > client.lastProcessedResultRound) {
            client.lastProcessedResultRound = resultRound
            currentAccount.battles += 1
            val winnerNames = winnersText.split(",").map(_.trim).toVector
            if (winnerNames.contains(currentAccount.username)) {
              currentAccount.wins += 1
              if (client.currentCard != null) currentAccount.cards += (client.series + "|" + client.currentCard.name)
            } else currentAccount.losses += 1
            try saveAccount(currentAccount) catch { case _: Exception => () }
          }

          if (client.screenMode != "BATTLE") showAdvancedBattle(client)
          refreshAdvancedBattle(client)

        case "GAMEOVER" if p.length >= 2 =>
          client.finalScoreboard = netDecode(p(1))
          client.phase = "GAMEOVER"
          showAdvancedGameOver(client)

        case "INFO" if p.length >= 2 =>
          client.resultText = netDecode(p(1))
          refreshAdvancedLobby(client)

        case _ => ()
      }
    } catch {
      case ex: Exception =>
        showMessage("Room message error: " + ex.getMessage, "NETWORK", JOptionPane.WARNING_MESSAGE)
    }
  }

  def showAdvancedLobby(client: AdvancedNetClient): Unit = {
    client.screenMode = "LOBBY"
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(16,20,16,20))
    page.add(title("ROOM: " + client.roomName + "  |  " + client.series), BorderLayout.NORTH)

    advancedLobbyInfoLabel = label("Joining room?", 15, goldColor)
    advancedLobbyPlayersArea = new JTextArea()
    advancedLobbyPlayersArea.setEditable(false)
    advancedLobbyPlayersArea.setFont(new Font("Consolas", Font.PLAIN, 16))
    advancedLobbyPlayersArea.setForeground(whiteColor)
    advancedLobbyPlayersArea.setBackground(panelColor)
    advancedLobbyPlayersArea.setBorder(BorderFactory.createEmptyBorder(12,12,12,12))

    val center = new JPanel(new BorderLayout(8,8))
    center.setOpaque(false)
    center.add(advancedLobbyInfoLabel, BorderLayout.NORTH)
    center.add(new JScrollPane(advancedLobbyPlayersArea), BorderLayout.CENTER)
    page.add(center, BorderLayout.CENTER)

    val footer = new JPanel(new GridLayout(1,3,8,8))
    footer.setOpaque(false)
    advancedReadyButton = button("I AM READY")
    advancedHostStartButton = button("HOST: START WHEN READY")
    val leave = button("LEAVE ROOM")

    advancedReadyButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = client.send("READY")
    })
    advancedHostStartButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = client.send("START")
    })
    leave.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = leaveAdvancedRoom()
    })

    footer.add(advancedReadyButton)
    footer.add(advancedHostStartButton)
    footer.add(leave)
    page.add(footer, BorderLayout.SOUTH)

    screen(page, 920, 620)
    refreshAdvancedLobby(client)
  }

  def refreshAdvancedLobby(client: AdvancedNetClient): Unit = {
    if (client == null || client.screenMode != "LOBBY" || advancedLobbyInfoLabel == null || advancedLobbyPlayersArea == null) return

    val seconds = if (client.secondsLeft > 0) client.secondsLeft + " seconds left to join" else "Join window closed"
    val endpoint = if (client.hostRole && activeAdvancedServer != null) "  |  HOST " + localIPAddresses() + ":" + activeAdvancedServer.port else ""
    advancedLobbyInfoLabel.setText(seconds + "  |  " + client.players.size + "/10 players  |  " + client.phase + endpoint)

    val rows = client.players.map { p =>
      val flag = if (p.ready) "READY" else "NOT READY"
      (if (p.id == client.id) "> " else "  ") + p.name + "  ?  " + flag + "  ?  Round wins: " + p.score
    }
    val extra = if (client.resultText.nonEmpty && client.phase == "LOBBY") "\n\n" + client.resultText else ""
    val hostAddressLine = if (client.hostRole && activeAdvancedServer != null) {
      val port = activeAdvancedServer.port
      val endpoints = localIPAddresses().split(",").toVector.map(_.trim).filter(_.nonEmpty)
        .map(_ + ":" + port).mkString(", ")
      "\n\nHost address (manual join fallback): " + endpoints
    } else ""

    advancedLobbyPlayersArea.setText(rows.mkString("\n") + extra + hostAddressLine +
      "\n\nThe room accepts up to 10 players. After the 60-second join window, everyone must be READY. The 30-round battle then runs automatically.")
    advancedLobbyPlayersArea.setCaretPosition(0)

    val ownReady = client.players.find(_.id == client.id).exists(_.ready)
    if (advancedReadyButton != null) {
      advancedReadyButton.setText(if (ownReady) "CANCEL READY" else "I AM READY")
      advancedReadyButton.setEnabled(client.phase == "LOBBY" || client.phase == "READY")
    }
    if (advancedHostStartButton != null) {
      val allReady = client.players.size >= 2 && client.players.forall(_.ready)
      advancedHostStartButton.setEnabled(client.hostRole && client.phase == "READY" && allReady)
    }
  }

  class AdvancedBattleCardPanel(client: AdvancedNetClient) extends JPanel {
    setPreferredSize(new Dimension(470, 300))
    setBackground(panelColor)

    override 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
        g.setColor(new Color(255,255,255,22))
        g.fillRoundRect(8,8,w-16,h-16,24,24)
        g.setColor(goldColor)
        g.setStroke(new BasicStroke(3))
        g.drawRoundRect(8,8,w-16,h-16,24,24)

        val f = client.currentCard
        if (f == null) {
          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,22))
          g.drawString("Waiting for round card?", 28, 55)
        } else {
          g.setColor(f.color)
          g.fillRoundRect(18,18,w-36,h-36,18,18)
          g.setColor(new Color(12,14,26,210))
          g.fillRoundRect(25,25,w-50,h-50,14,14)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,22))
          g.drawString(f.name, 42, 58)
          g.setColor(f.color.brighter())
          g.setFont(new Font("Arial",Font.BOLD,13))
          g.drawString(client.series.toUpperCase(Locale.ROOT) + " BATTLE CARD",42,80)

          drawCharacterArtwork(g, f, w/2 - 68, 86, 136, 108)

          g.setColor(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,15))
          g.drawString("POWER  " + f.power, 34, 218)
          g.drawString("SPEED  " + f.speed, 160, 218)
          g.drawString("DEFENSE  " + f.defense, 285, 218)
          g.drawString("CHAKRA  " + f.chakra, 34, 247)
          g.setColor(new Color(255,220,120))
          g.setFont(new Font("Arial",Font.BOLD,13))
          g.drawString(f.special, 160, 247)
        }
      } finally g.dispose()
    }
  }

  def showAdvancedBattle(client: AdvancedNetClient): Unit = {
    client.screenMode = "BATTLE"
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(14,18,14,18))
    page.add(title("CARD BATTLE - ROUND " + math.max(1, client.roundNo) + "/30"), BorderLayout.NORTH)

    advancedBattleInfoLabel = label("Waiting for players?", 16, goldColor)
    val cardPanel = new AdvancedBattleCardPanel(client)
    advancedBattleArtworkPanel = cardPanel

    val center = new JPanel(new BorderLayout(8,8))
    center.setOpaque(false)
    center.add(advancedBattleInfoLabel, BorderLayout.NORTH)
    center.add(cardPanel, BorderLayout.CENTER)

    advancedBattleScoreArea = new JTextArea()
    advancedBattleScoreArea.setEditable(false)
    advancedBattleScoreArea.setFont(new Font("Consolas", Font.PLAIN, 13))
    advancedBattleScoreArea.setForeground(whiteColor)
    advancedBattleScoreArea.setBackground(panelColor)
    advancedBattleScoreArea.setPreferredSize(new Dimension(260, 150))
    center.add(new JScrollPane(advancedBattleScoreArea), BorderLayout.EAST)
    page.add(center, BorderLayout.CENTER)

    val footer = new JPanel(new GridLayout(1,2,8,8))
    footer.setOpaque(false)
    advancedBattlePlayButton = button("PLAY ROUND")
    val leave = button("LEAVE ROOM")
    advancedBattlePlayButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (!client.hasPlayed && client.phase == "PLAYING") {
          client.hasPlayed = true
          client.send("PLAY")
          refreshAdvancedBattle(client)
        }
      }
    })
    leave.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = leaveAdvancedRoom() })
    footer.add(advancedBattlePlayButton)
    footer.add(leave)
    page.add(footer, BorderLayout.SOUTH)

    screen(page, 1060, 650)
    refreshAdvancedBattle(client)
  }

  def refreshAdvancedBattle(client: AdvancedNetClient): Unit = {
    if (client == null || client.screenMode != "BATTLE" || advancedBattleInfoLabel == null) return
    val status = if (client.phase == "RESULTS") "RESULTS ? next round starts automatically" else "ROUND STAT: " + client.currentStat + " ? press PLAY ROUND once your card is ready"
    advancedBattleInfoLabel.setText("ROUND " + math.max(1, client.roundNo) + "/30  |  " + status)
    if (advancedBattlePlayButton != null) {
      advancedBattlePlayButton.setEnabled(client.phase == "PLAYING" && !client.hasPlayed && client.currentCard != null)
      advancedBattlePlayButton.setText(if (client.hasPlayed) "PLAY SENT - WAITING" else "PLAY ROUND (" + client.currentStat + ")")
    }
    if (advancedBattleScoreArea != null) {
      advancedBattleScoreArea.setText("PLAYERS / SCORE\n\n" + client.players.map { p =>
        p.name + (if (p.id == client.id) " (YOU)" else "") + "\n  Wins: " + p.score + "\n  " + (if (p.ready) "READY" else "")
      }.mkString("\n") + "\n\n" + client.resultText)
      advancedBattleScoreArea.setCaretPosition(0)
    }
    if (advancedBattleArtworkPanel != null) advancedBattleArtworkPanel.repaint()
  }

  def showAdvancedGameOver(client: AdvancedNetClient): Unit = {
    client.screenMode = "GAMEOVER"
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(18,22,18,22))
    page.add(title("30-ROUND TOURNAMENT COMPLETE!"), BorderLayout.NORTH)

    advancedGameOverArea = new JTextArea(client.finalScoreboard)
    advancedGameOverArea.setEditable(false)
    advancedGameOverArea.setFont(new Font("Consolas",Font.BOLD,18))
    advancedGameOverArea.setForeground(whiteColor)
    advancedGameOverArea.setBackground(panelColor)
    page.add(new JScrollPane(advancedGameOverArea), BorderLayout.CENTER)

    val footer = new JPanel(new GridLayout(1,2,8,8))
    footer.setOpaque(false)
    val rematch = button("PLAY ANOTHER 30-ROUND TOURNAMENT")
    val leave = button("LEAVE ROOM")
    rematch.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
      client.resultText = "Waiting for all connected players to choose REMATCH."
      client.send("REMATCH")
      rematch.setEnabled(false)
      rematch.setText("REMATCH VOTE SENT")
    }})
    leave.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = leaveAdvancedRoom() })
    footer.add(rematch); footer.add(leave)
    page.add(footer, BorderLayout.SOUTH)
    screen(page, 850, 610)
  }

  def leaveAdvancedRoom(): Unit = {
    stopRoomDiscovery()
    val client = activeAdvancedClient
    activeAdvancedClient = null
    if (client != null) client.closeNow()
    val server = activeAdvancedServer
    activeAdvancedServer = null
    if (server != null) server.stop()
    advancedBattleArtworkPanel = null
    showAdvancedRoomBrowser()
  }

  // ======================================================
  // BASIC UNO GAME
  // ======================================================

  case class UnoCard(colorName: String, value: Int) {
    def display: String = colorName + " " + value
  }

  val unoColors = Vector("RED","BLUE","GREEN","YELLOW")

  var unoDeck = mutable.ArrayBuffer[UnoCard]()
  var unoDiscard = mutable.ArrayBuffer[UnoCard]()
  var unoPlayerHand = mutable.ArrayBuffer[UnoCard]()
  var unoRobotHand = mutable.ArrayBuffer[UnoCard]()
  var unoFinished = false
  var unoStatus = ""

  def unoColor(name: String): Color = {
    if (name == "RED") new Color(210,55,55)
    else if (name == "BLUE") new Color(45,95,210)
    else if (name == "GREEN") new Color(40,145,85)
    else new Color(235,205,55)
  }

  def shuffleUnoDeck(): Unit = {
    val cards = mutable.ArrayBuffer[UnoCard]()

    unoColors.foreach { c =>
      cards += UnoCard(c,0)

      for (n <- 1 to 9) {
        cards += UnoCard(c,n)
        cards += UnoCard(c,n)
      }
    }

    unoDeck.clear()
    unoDeck ++= random.shuffle(cards.toVector)
  }

  def startUno(): Unit = {
    closePeer()
    isLanBattle = false
    unoFinished = false

    shuffleUnoDeck()
    unoDiscard.clear()
    unoPlayerHand.clear()
    unoRobotHand.clear()

    for (i <- 0 until 7) {
      unoPlayerHand += unoDeck.remove(0)
      unoRobotHand += unoDeck.remove(0)
    }

    unoDiscard += unoDeck.remove(0)
    unoStatus = "Match the colour or number."
    showUnoGame()
  }

  def unoPlayable(card: UnoCard): Boolean = {
    val top = unoDiscard.last
    card.colorName == top.colorName || card.value == top.value
  }

  def drawUnoFromDeck(): UnoCard = {
    if (unoDeck.isEmpty && unoDiscard.size > 1) {
      val top = unoDiscard.last
      val recycled = random.shuffle(unoDiscard.dropRight(1).toVector)

      unoDeck.clear()
      unoDeck ++= recycled

      unoDiscard.clear()
      unoDiscard += top
    }

    if (unoDeck.nonEmpty) unoDeck.remove(0)
    else UnoCard("RED",0)
  }

  def showUnoGame(): Unit = {
    val page = new JPanel(new BorderLayout(8,8))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(12,15,12,15))

    page.add(title("BIXUU UNO CARD GAME"),BorderLayout.NORTH)

    val center = plainPanel(new BorderLayout(8,8))
    val top = unoDiscard.last

    center.add(
      label(
        "ROBOT CARDS: " + unoRobotHand.size +
        " | TOP CARD: " + top.display,
        17,
        goldColor
      ),
      BorderLayout.NORTH
    )

    val handPanel = new JPanel(new FlowLayout(FlowLayout.LEFT,6,6))
    handPanel.setBackground(panelColor)

    for (i <- unoPlayerHand.indices) {
      val card = unoPlayerHand(i)
      val index = i

      val b = new JButton(card.display)
      b.setPreferredSize(new Dimension(100,58))
      b.setFont(new Font("Arial",Font.BOLD,12))
      b.setBackground(unoColor(card.colorName))
      b.setForeground(if (card.colorName == "YELLOW") Color.BLACK else Color.WHITE)
      b.setEnabled(unoPlayable(card))

      b.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = playUnoCard(index)
      })

      handPanel.add(b)
    }

    val scroll = new JScrollPane(handPanel)
    scroll.setPreferredSize(new Dimension(760,160))
    center.add(scroll,BorderLayout.CENTER)

    center.add(label(unoStatus,13,whiteColor),BorderLayout.SOUTH)
    page.add(center,BorderLayout.CENTER)

    val footer = new JPanel(new GridLayout(1,2,10,10))
    footer.setOpaque(false)

    val draw = button("DRAW CARD")
    val back = button("BACK TO HOME")

    draw.setEnabled(!unoPlayerHand.exists(unoPlayable))

    draw.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = drawUnoCard()
    })

    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showHome()
    })

    footer.add(draw)
    footer.add(back)
    page.add(footer,BorderLayout.SOUTH)

    screen(page,900,560)
  }

  def playUnoCard(index: Int): Unit = {
    if (unoFinished || index < 0 || index >= unoPlayerHand.size) return

    val card = unoPlayerHand(index)

    if (!unoPlayable(card)) {
      showMessage("Choose a card with the same colour or number.","UNO")
      return
    }

    unoPlayerHand.remove(index)
    unoDiscard += card

    if (unoPlayerHand.isEmpty) {
      finishUnoGame(true)
      return
    }

    unoStatus = "You played " + card.display + ". Robot's turn."
    robotUnoTurn()
  }

  def drawUnoCard(): Unit = {
    if (unoFinished) return

    if (unoPlayerHand.exists(unoPlayable)) {
      unoStatus = "You already have a playable card."
      showUnoGame()
    } else {
      val card = drawUnoFromDeck()
      unoPlayerHand += card

      if (unoPlayable(card)) {
        unoStatus = "You drew " + card.display + ". You can play it."
        showUnoGame()
      } else {
        unoStatus = "You drew " + card.display + ". Robot's turn."
        robotUnoTurn()
      }
    }
  }

  def robotUnoTurn(): Unit = {
    if (unoFinished) return

    var playable = unoRobotHand.indices.filter { i =>
      unoPlayable(unoRobotHand(i))
    }

    if (playable.isEmpty) {
      unoRobotHand += drawUnoFromDeck()

      playable = unoRobotHand.indices.filter { i =>
        unoPlayable(unoRobotHand(i))
      }
    }

    if (playable.nonEmpty) {
      val index = playable(random.nextInt(playable.size))
      val card = unoRobotHand.remove(index)
      unoDiscard += card

      unoStatus = "Robot played " + card.display + "."

      if (unoRobotHand.isEmpty) {
        finishUnoGame(false)
        return
      }
    } else {
      unoStatus = "Robot cannot play and passes."
    }

    showUnoGame()
  }

  def finishUnoGame(playerWon: Boolean): Unit = {
    if (unoFinished) return
    unoFinished = true

    currentAccount.battles += 1

    if (playerWon) currentAccount.wins += 1
    else currentAccount.losses += 1

    saveAccount(currentAccount)

    showMessage(
      if (playerWon) "UNO! YOU WIN!" else "ROBOT WINS THIS UNO GAME!",
      "UNO RESULT"
    )

    showHome()
  }

  // ======================================================
  // START APPLICATION
  // ======================================================

  def run(): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        frame = new JFrame("The Play Battle of Cards Creator Big 2")

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
        frame.setMinimumSize(new Dimension(700,550))

        dataDir.mkdirs()
        showLogin()
      }
    })
  }
}

BixuuCardsBattle.run()