Uno (Card Game)/Wren

From Rosetta Code
Uno (Card Game)/Wren is part of Uno_(Card_Game). You may find other members of Uno_(Card_Game) at Category:Uno_(Card_Game).

Code

Library: DOME
Library: Go-fonts
Library: Wren-iterate
Library: Wren-ellipse
Library: Wren-event
Library: Wren-dynamic
import "dome" for Window, Platform, Process
import "graphics" for Canvas, Color, Font
import "audio" for AudioEngine
import "input" for Mouse
import "random" for Random
import "./iterate" for Indexed
import "./ellipse" for Button
import "./event" for Event
import "./dynamic" for Tuple

var Rand = Random.new()
var Card = Tuple.create("Card", ["colorNo", "face"])
var Players = ["PLAYER", "BOT A", "BOT B", "BOT C"]
var Click = Event.new("click")
var Colors = [Color.red, Color.yellow, Color.green, Color.blue, Color.indigo]
var ColorNames = ["Red", "Yellow", "Green", "Blue", "Indigo"]
var Pack = []
var Hands = List.filled(4, null)
for (p in 0..3) Hands[p] = []
var Scores = List.filled(4, 0)
var DrawPack = []
var DiscPack = []
var Dealer = 0
var PlayerNo = 0
var Penalty = "None"
var Reversed = false
var NextColor = 0
var MustDeclare = false
var NeedCard = false
var PlayDrawn = false
var UnoPressed = false
var MayChallenge = false
var HelpShowing = false
var HandOver = false
var GameOver = false

var DeclareButtons = List.filled(4, null)
for (i in 0..3) {
    DeclareButtons[i] = Button.square(25 + i * 40, 70, 30)
}

var PlayerButtons = List.filled(28, null)
for (i in 0...28) {
    var r = (i / 7).floor + 1
    var c = i % 7
    PlayerButtons[i] = Button.square(270 + c * 60, 80 + 60 * r, 50)
}

var DrawButton = Button.square(270, 490, 50)

var BotButtons = List.filled(3, null)
BotButtons[0] = Button.square(745, 490, 50)
BotButtons[1] = Button.square(390, 830, 50)
BotButtons[2] = Button.square( 85, 490, 50)

var UnoButton = Button.square(815, 70, 30)

var PlayDrawnButtons = List.filled(2, 0)
for (i in 0..1) {
    PlayDrawnButtons[i] = Button.square(25 + i * 40, 760, 30)
}

var HelpButton = Button.square(815, 760, 30)

var Symbols = "0123456789STR"

var HelpText = """
This simulation is based on the official rules of the UNO card game, uses the standard pack
of 108 cards and is played entirely with the mouse.

The following symbols, which appear on the face, are used to describe the cards:

   0 to 9  Colored card of that number
   S       Colored 'skip' card
   R       Colored 'reverse' card
   T       Colored 'draw two' card
   W       Wild card
   F       Wild 'draw four' card

You play against 3 bots: A, B and C. Your hand is visible but the bots' Hands are not.

When you click their icon, the bots play automatically, in a deterministic fashion and don't make
mistakes except that, if you play a wild draw four card, the next bot (which cannot 'see' your cards)
will randomly challenge you 50% of the time.

The cards in your or the bot's hand will be automatically adjusted depending on whether
the challenge is won or lost.

Just click a card in your hand to play it OR click the draw pack to add the top card to your hand.
If the drawn card is playable, click 'Y' to play it or 'N' to leave it in your hand.

If you play a wild card then you will need to declare the next color by clicking the appropriate
button. When a wild card is displayed on opening, the next color will be deduced from the card
you play.

If you only have one card left after making a play, you will need to click the Uno button before
clicking the next bot to avoid being penalized.

There are some unusual scenarios which will result in a void hand. For example, if you were to 
accumulate more than 28 cards in your hand, the hand would be declared void as the display can
only handle a maximum of 28 cards.

Click the mouse's left button to return to the current hand.
"""

class Main {
    construct new() {
        Window.resize(900, 900)
        Canvas.resize(900, 900)
        Window.title = "Uno simulation"
        Font.load("Go-Regular20", "Go-Regular.ttf", 20)
        Canvas.font = "Go-Regular20"

        // download from https://soundbible.com/509-Mouse-Double-Click.html
        AudioEngine.load("clicked", "mouse_click.wav")

        Click.register { |o, argMap|
            onUnoButtonClick(argMap)
        }

        Click.register { |o, argMap|
            onHelpButtonClick(argMap)
        }

        Click.register { |o, argMap|
            onPlayerButtonClick(argMap)
        }

        Click.register { |o, argMap|
            onDrawButtonClick(argMap)
        }

        Click.register { |o, argMap|
            onBotButtonClick(argMap)
        }
    }

    init() {
        startUp()
    }

    startUp() {
        createPack()
        shuffle(Pack)
        // deal 7 cards to each player
        for (i in 0..6) {
            for (p in 0..3) Hands[p].add(Pack[i * 4 + p])
        }
        DrawPack = Pack[28..-1]
        while (DrawPack[0].face == "F") {
            shuffle(DrawPack)
        }
        var discTop = DrawPack.removeAt(0)
        DiscPack = [discTop]
        Dealer = Rand.int(4)
        PlayerNo = Dealer
        var face = discTop.face
        NextColor = discTop.colorNo
        if (face == "S") {
            nextPlayer(2)
        } else if (face == "R") {
            Reversed = true
            redraw()
        } else if (face == "T") {
            nextPlayer(1, false)
            for (i in 1..2) {
                var newCard = DrawPack.removeAt(0)
                Hands[PlayerNo].add(newCard)
            }
            nextPlayer(1)
        } else if (face == "W") {
            nextPlayer(1, false)
            if (Hands[0].all { |c| c.colorNo == 4 }) {
                handVoid("Player has no colored card.")
                return
            }
            if (PlayerNo == 0) {
                NeedCard = true
                redraw()
            } else {
                for (card in Hands[PlayerNo]) {
                    if (card.colorNo < 4) {
                        if (card.face == "S") {
                            skip(card)
                            return
                        } else if (card.face == "R") {
                            reverse(card)
                            return
                        } else if (card.face == "T") {
                           draw2(card)
                           return
                        } else {
                           playSame(card)
                           return
                        }
                    }
                }
            }
        } else {
            nextPlayer(1)
        }
    }

    createPack() {
        for (i in 0..3) Pack.add(Card.new(4, "W"))
        for (i in 0..3) Pack.add(Card.new(4, "F"))
        for (i in 0..3) {
            Pack.add(Card.new(i, "0"))
            for (j in 1..2) {
                for (f in "123456789SRT") Pack.add(Card.new(i, f))
            }
        }
    }

    shuffle(a) { Rand.shuffle(a) }

    reshuffle() {
        var discTop = DiscPack.removeAt(0)
        shuffle(DiscPack)
        DrawPack = DiscPack
        DiscPack = [discTop]
    }

    nextPlayer(places, draw) {
        if (!Reversed) {
            PlayerNo = (PlayerNo + places) % 4
        } else {
            PlayerNo = PlayerNo - places
            if (PlayerNo < 0) PlayerNo = PlayerNo + 4
        }
        Penalty = "None"
        if (draw) redraw()
    }

    nextPlayer(places) {
        nextPlayer(places, true)
    }

    redraw() {
        Canvas.cls()
        Canvas.print("Declare color:", 10, 20, Color.white)
        for (i in 0..3) {
            var cb = DeclareButtons[i]
            cb.drawfill(Colors[i])
        }

        Canvas.print("Uno:", 800, 20, Color.white)
        UnoButton.drawfill(Color.orange)
        Canvas.print("U", UnoButton.cx-5, UnoButton.cy-10, Color.black)

        var pc = Hands[0].count
        if (pc > 28) {
            handVoid("Player has more than 28 cards.")
            return
        }
        if (UnoPressed && pc != 1) UnoPressed = false
        Canvas.print("Name: PLAYER", 365, 20, Color.white)
        var uno = (UnoPressed) ? " (UNO)" : ""
        Canvas.print("Cards: %(pc) %(uno)", 365, 50, Color.white)
        Canvas.print("Score: %(Scores[0])", 365, 80, Color.white)

        for (i in 0...Hands[0].count) {
            var b = PlayerButtons[i]
            var c = Hands[0][i]
            b.drawfill(Colors[c.colorNo])
            Canvas.print(c.face, b.cx-5, b.cy-10, Color.black)
        }

        for (iv in Indexed.new(BotButtons)) {
            var ix = iv.index
            var bb = iv.value
            var letter = "ABC"[ix]
            Canvas.print("Name: BOT %(letter)", bb.cx-25, bb.cy-120, Color.white)
            uno = (Hands[ix+1].count == 1) ? " (UNO)" : ""
            Canvas.print("Cards: %(Hands[ix+1].count)%(uno)", bb.cx-25, bb.cy-90, Color.white)
            Canvas.print("Score: %(Scores[ix+1])", bb.cx-25, bb.cy-60, Color.white)
            bb.drawfill(Color.peach)
            Canvas.print(letter, bb.cx-5, bb.cy-10, Color.black)
        }

        Canvas.print("Name: DRAW", 245, 370, Color.white)
        Canvas.print("Cards: %(DrawPack.count)", 245, 400, Color.white)
        Canvas.print("Direction: %(Reversed ? "Anti-clock" : "Clock")", 245, 430, Color.white)
        DrawButton.drawfill(Color.pink)
        Canvas.print("D", DrawButton.cx-5, DrawButton.cy-10, Color.black)

        var discTop = DiscPack[0]
        Canvas.print("Name: DISCARD", 485, 370, Color.white)
        Canvas.print("Cards: %(DiscPack.count)", 485, 400, Color.white)
        var cc
        var ct
        if (NeedCard) {
            cc = Color.peach
            ct = "Need card"
        } else if (MustDeclare) {
            cc = Color.pink
            ct = "Declare"
        } else if (PlayDrawn) {
            cc = Color.orange
            ct = "Play drawn?"
        } else {
            cc = Color.white
            ct = ColorNames[NextColor]
        }
        Canvas.print("Color: %(ct)", 485, 430, cc)
        var dib = Button.square(510, 490, 50)
        dib.drawfill(Colors[discTop.colorNo])
        Canvas.print(discTop.face, dib.cx-5, dib.cy-10, Color.black)

        Canvas.print("Play drawn card:", 10, 710, Color.white)
        for (iv in Indexed.new(["Y", "N"])) {
            var ix = iv.index
            var ncb = PlayDrawnButtons[ix]
            ncb.drawfill(Color.orange)
            Canvas.print(iv.value, ncb.cx-5, ncb.cy-10, Color.black)
        }

        Canvas.print("Help:", 800, 710, Color.white)
        HelpButton.drawfill(Color.orange)
        Canvas.print("H", HelpButton.cx-5, HelpButton.cy-10, Color.black)

        Canvas.print("Dealer: %(Players[Dealer])", 60, 600, Color.white)
        Canvas.print("Player: %(Players[PlayerNo])", 365, 600, Color.yellow)
        var cp = (Penalty == "None") ? Color.white :
                 (Penalty == "+2")   ? Color.blue  :
                 (Penalty == "+4"  ) ? Color.red  : Color.green
        Canvas.print("Penalty: %(Penalty)", 720, 600, cp)
    }

    checkDeclareButton(x, y) {
        if (!MustDeclare) return
        for (i in 0..3) {
            if (DeclareButtons[i].contains(x, y)) {
                AudioEngine.play("clicked")
                MustDeclare = false
                NextColor = i
                var face = DiscPack[0].face
                if (face == "F") {
                    MayChallenge = true
                    nextPlayer(1, false)
                    for (i in 1..4) {
                        var newCard = DrawPack.removeAt(0)
                        Hands[PlayerNo].add(newCard)
                        if (DrawPack.isEmpty) reshuffle()
                    }
                }
                nextPlayer(1)
            }
        }
    }

    checkPlayDrawnButton(x, y) {
        if (!PlayDrawn) return
        for (i in 0..1) {
            if (PlayDrawnButtons[i].contains(x, y)) {
                AudioEngine.play("clicked")
                PlayDrawn = false
                if (i == 0) {
                    var card = Hands[0][-1]
                    playerPlay(card)
                } else {
                    nextPlayer(1)
                }
                return
            }
        }
    }

    onUnoButtonClick(argMap) {
        if (PlayerNo == 0 || UnoPressed || Hands[0].count != 1) return
        var x = argMap["x"]
        var y = argMap["y"]
        if (UnoButton.contains(x,y)) {
            AudioEngine.play("clicked")
            UnoPressed = true
            redraw()
        }
    }

    onHelpButtonClick(argMap) {
        if (HelpShowing) return
        var x = argMap["x"]
        var y = argMap["y"]
        if (HelpButton.contains(x,y)) {
            AudioEngine.play("clicked")
            HelpShowing = true
            help()
        }
    }

    onPlayerButtonClick(argMap) {
        if (PlayerNo != 0) return
        var x = argMap["x"]
        var y = argMap["y"]
        var discTop = DiscPack[0]
        for (iv in Indexed.new(PlayerButtons)) {
            var i = iv.index
            var btn = iv.value
            if (btn.contains(x, y)) {
                var card = Hands[0][i]
                if (NeedCard) {
                    if (card.colorNo == 4) return
                    NeedCard = false
                } else if (card.colorNo != 4 && card.colorNo != NextColor && card.face != discTop.face) {
                    return
                }
                AudioEngine.play("clicked")
                playerPlay(card)
                return
            }
        }
    }

    onDrawButtonClick(argMap) {
        if (PlayerNo != 0) return
        var x = argMap["x"]
        var y = argMap["y"]
        if (DrawButton.contains(x,y)) {
            if (NeedCard) return
            AudioEngine.play("clicked")
            var card = DrawPack.removeAt(0)
            Hands[0].add(card)
            if (DrawPack.isEmpty) reshuffle()
            if (card.colorNo != 4 && card.colorNo != NextColor && card.face != DiscPack[0].face) {
                nextPlayer(1)
                return
            }
            PlayDrawn = true
            redraw()
        }
    }

    onBotButtonClick(argMap) {
        if (PlayerNo == 0) return
        var x = argMap["x"]
        var y = argMap["y"]
        var btn = BotButtons[PlayerNo-1]
        if (btn.contains(x, y)) {
            AudioEngine.play("clicked")
            if (MayChallenge) {
                if (Rand.int(2) == 0) {
                    if (Hands[0].any { |card| card.colorNo == NextColor }) {
                        for (i in 1..4) {
                            Hands[0].add(Hands[PlayerNo-1].removeAt(-1))
                        }
                        Penalty = "+4"
                    } else {
                        if (Hands[PlayerNo].count == 0) {
                            handFinished(PlayerNo)
                            return
                        }
                        for (i in 1..2) {
                            var newCard = DrawPack.removeAt(0)
                            Hands[PlayerNo-1].add(newCard)
                            if (DrawPack.isEmpty) reshuffle()
                        }
                        Penalty = "-2"
                    }
                }
                MayChallenge = false
                redraw()
            } else if (Hands[0].count == 1 && !UnoPressed) {
                for (i in 1..2) {
                    var newCard = DrawPack.removeAt(0)
                    Hands[0].add(newCard)
                    if (DrawPack.isEmpty) reshuffle()
                }
                Penalty = "+2"
                redraw()
            } else {
                var saveNo = PlayerNo
                Penalty = "None"
                botPlay()
                if (Hands[saveNo].count == 0) handFinished(saveNo)
            }
        }
    }

    playerPlay(card) {
       if (card.face == "S") {
            skip(card)
        } else if (card.face == "R") {
            reverse(card)
        } else if (card.face == "T") {
            draw2(card)
        } else if (card.face == "W") {
            wild(card)
        } else if (card.face == "F") {
            draw4(card)
            MayChallenge = true
        } else {
            playSame(card)
        }
    }

    botPlay() {
        var face = DiscPack[0].face
        var hand = Hands[PlayerNo]
        var cardToPlay = null
        for (card in hand) {
            if (card.colorNo == NextColor || card.face == face) {
                cardToPlay = card
                break
            }
        }
        if (!cardToPlay) {
            for (card in hand) { 
                if (card.face == "W") {
                    cardToPlay = card
                    break
                }
            }
        }
        if (!cardToPlay) {
            for (card in hand) {
                if (card.face == "F") {
                    cardToPlay = card
                    break
                }
            }
        }
        if (!cardToPlay) {
            cardToPlay = DrawPack.removeAt(0)
            Hands[PlayerNo].add(cardToPlay)
            if (DrawPack.isEmpty) reshuffle()
            if (cardToPlay.colorNo != 4 && cardToPlay.colorNo != NextColor && cardToPlay.face != face) {
                nextPlayer(1)
                return
            }
        }

        if (cardToPlay.face == "S") {
            skip(cardToPlay)
        } else if (cardToPlay.face == "R") {
            reverse(cardToPlay)
        } else if (cardToPlay.face == "T") {
            draw2(cardToPlay)
        } else if (cardToPlay.face == "W") {
            wild(cardToPlay)
        } else if (cardToPlay.face == "F") {
            draw4(cardToPlay)
        } else {
            playSame(cardToPlay)
        }
    }

    skip(card) {
        if (Hands[PlayerNo].count == 1) {
            handFinished(PlayerNo)
            return
        }
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        NextColor = card.colorNo
        nextPlayer(2)
    }

    reverse(card) {
        if (Hands[PlayerNo].count == 1) {
            handFinished(PlayerNo)
            return
        }
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        Reversed = !Reversed
        NextColor = card.colorNo
        nextPlayer(1)
    }

    draw2(card) {
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        nextPlayer(1, false)
        for (i in 1..2) {
            var newCard = DrawPack.removeAt(0)
            Hands[PlayerNo].add(newCard)
            if (DrawPack.isEmpty) reshuffle()
        }
        if (Hands[PlayerNo].count == 0) {
            handFinished(PlayerNo)
            return
        }
        NextColor = card.colorNo
        nextPlayer(1)
    }

    wild(card) {
        if (Hands[PlayerNo].count == 1) {
            handFinished(PlayerNo)
            return
        }
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        if (PlayerNo > 0) {
            NextColor = Rand.int(4)
            nextPlayer(1)
        } else {
            MustDeclare = true
            redraw()
        }
    }

    draw4(card) {
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        if (PlayerNo > 0) {
            NextColor = Rand.int(4)
            nextPlayer(1, false)
            for (i in 1..4) {
                var newCard = DrawPack.removeAt(0)
                Hands[PlayerNo].add(newCard)
                if (DrawPack.isEmpty) reshuffle()
            }
            if (Hands[PlayerNo].count == 0) {
                handFinished(PlayerNo)
                return
            }
            nextPlayer(1)
        } else {
            MustDeclare = true
            redraw()
        }
    }

    playSame(card) {
        if (Hands[PlayerNo].count == 1) {
            handFinished(PlayerNo)
            return
        }
        Hands[PlayerNo].remove(card)
        DiscPack.insert(0, card)
        NextColor = card.colorNo
        nextPlayer(1)
    }

    update() {
        if (Mouse["left"].justPressed) {
            if (HelpShowing) {
                AudioEngine.play("clicked")
                HelpShowing = false
                redraw()
            } else if (GameOver) {
                AudioEngine.play("clicked")
                Process.exit()
            } else if (HandOver) {
                AudioEngine.play("clicked")
                Pack.clear()
                for (p in 0..3) Hands[p] = []
                Penalty = "None"
                Reversed = false
                MustDeclare = false
                NeedCard = false
                PlayDrawn = false
                UnoPressed = false
                MayChallenge = false
                HandOver = false
                startUp()
            } else if (MustDeclare) {
                checkDeclareButton(Mouse.x, Mouse.y)
            } else if (PlayDrawn) {
                checkPlayDrawnButton(Mouse.x, Mouse.y)
            } else {
                Click.notify({"x": Mouse.x, "y": Mouse.y})
            }
        }
    }

    draw(alpha) {
    }

    help() {
        Canvas.cls()
        Canvas.print(HelpText, 10, 10, Color.white)
    }

    handVoid(msg) {
        Canvas.cls()
        Canvas.print(msg, 50, 50, Color.white)
        Canvas.print("Click the mouse's left button to start a new hand.", 50, 100, Color.white)
        HandOver = true
    }

    handFinished(winner) {
        var total = 0
        for (player in 0..3) {
            if (player != winner) {
                for (card in Hands[player]) {
                    if (card.face == "S" || card.face == "R" || card.face == "T") {
                        total = total + 20
                    } else if (card.face == "W" || card.face == "F") {
                        total = total + 50
                    } else {
                        total = total + Num.fromString(card.face)
                    }
                }
            }
        }
        Scores[winner] = Scores[winner] + total
        Canvas.cls()
        if (Scores[winner] >= 500) {
            Canvas.print("%(Players[winner]) has won the game with %(Scores[winner]) points!", 50, 50, Color.white)
            Canvas.print("Click the mouse's left button to exit.", 50, 100, Color.white)
            GameOver = true
        } else {
            Canvas.print("%(Players[winner]) has won the hand with %(total) points!", 50, 50 , Color.white)
            Canvas.print("The total points scored by this player are now %(Scores[winner])", 50, 100, Color.white)
            Canvas.print("Click the mouse's left button to start the next hand.", 50, 150, Color.white)
            HandOver = true
        }
    }
}

var Game = Main.new()