Code Sketch


Bixuu02 card battel and multiplayer game
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

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(245, 245, 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("BIXUU'S ANIME FIGHTING CARDS"), 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("BIXUU'S ANIME FIGHTING CARDS"), 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 - SAME WIFI")
    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))
  }

  // ======================================================
  // 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(Color.WHITE)
          g.setFont(new Font("Arial",Font.BOLD,14))

          g.drawString("POWER     " + fighter.power,28,120)
          g.drawString("SPEED       " + fighter.speed,28,148)
          g.drawString("DEFENSE  " + fighter.defense,28,176)
          g.drawString("CHAKRA   " + fighter.chakra,28,204)

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

          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 - SAME WIFI
  // ======================================================

  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
  }

  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 = {
    val page = new JPanel(new BorderLayout(10,10))
    page.setBackground(backgroundColor)
    page.setBorder(BorderFactory.createEmptyBorder(18,22,18,22))

    page.add(title("MULTIPLAYER - SAME WIFI"),BorderLayout.NORTH)

    val instructions =
      "HOST: creates a trial match and shares the local IP address.\n\n" +
      "JOIN: enters the host's IP address.\n\n" +
      "Both computers must run this game, log in, and be on the same Wi-Fi.\n\n" +
      "Trial match difficulty is MEDIUM.\n" +
      "Port: " + LAN_PORT + "\n\n" +
      "This is local-network multiplayer, not internet matchmaking."

    val info = new JTextArea(instructions)
    info.setEditable(false)
    info.setFont(new Font("Arial",Font.PLAIN,16))
    info.setForeground(whiteColor)
    info.setBackground(panelColor)
    info.setLineWrap(true)
    info.setWrapStyleWord(true)

    page.add(info,BorderLayout.CENTER)

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

    val host = button("HOST TRIAL MATCH")
    val join = button("JOIN TRIAL MATCH")
    val back = button("BACK TO HOME")

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

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

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

    buttons.add(host)
    buttons.add(join)
    buttons.add(back)

    page.add(buttons,BorderLayout.SOUTH)

    screen(page,800,650)
  }

  // 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
      )
    }
  }

  // ======================================================
  // 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("Bixuu's Anime Fighting Cards")

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

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

BixuuCardsBattle.run()