Uno (Card Game)/Julia

From Rosetta Code
Revision as of 21:22, 2 October 2021 by Wherrera (talk | contribs) (Created page with "=== Gtk based graphical version. === <lang julia>using Random, Colors, Gtk, Cairo #=========== Channel section ===================# """ channel, communicates player's mouse...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Gtk based graphical version.

<lang julia>using Random, Colors, Gtk, Cairo

  1. =========== Channel section ===================#

""" channel, communicates player's mouse choice of card or color to game logic """ channel = Channel{Any}(100)

""" flush the channel from mouse choice to game logic """ flushchannel() = while !isempty(channel) take!(channel); end

  1. ============ Game play section ==================#

""" The Uno card type. The first field is color, second field is number or command. """ const UnoCard = Pair{String, String} color(c::UnoCard) = first(c) type(c::UnoCard) = last(c)

""" Each Uno player has a name, may be a bot, and has a hand of UnoCards. """ mutable struct UnoCardGamePlayer

   name::String
   isabot::Bool
   hand::Vector{UnoCard}

end

"""

   mutable struct UnoCardGameState

Encapsulates a board state of the gane, including players, cards, color being played, order of play, current player, and whether the card showing has had its command used """ mutable struct UnoCardGameState

   drawpile::Vector{UnoCard}
   discardpile::Vector{UnoCard}
   players::Vector{UnoCardGamePlayer}
   pnow::Int
   colornow::String
   clockwise::Bool
   commandsvalid::Bool

end

""" classifications of colors and types for card faces """ const colors = ["Red", "Yellow", "Green", "Blue"] # suit colors const types = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "Skip", "Draw Two", "Reverse"] const numtypes = types[begin:end-3] const wildtypes = ["Wild", "Draw Four"] const cmdtypes = vcat(types[end-2:end], wildtypes) const alltypes = vcat(types, wildtypes) const unopreferred = ["Skip", "Draw Two", "Reverse", "Draw Four"] const ttypes = sort!(vcat(types, types)) const typeordering = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "Wild", "Skip",

   "Reverse", "Draw Two", "Draw Four"]

popfirst!(ttypes) # only 1 "0" card per color

""" The Uno card game deck, unshuffled. """ const originaldeck = [vec([UnoCard(c, v) for v in ttypes, c in colors]);

     fill(UnoCard("Wild", "Wild"), 4); fill(UnoCard("Wild", "Draw Four"), 4)]

""" Set the next player to play to game.pnow (clockwise or counterclockwise) """ function nextplayer!(game, idx)

  game.pnow = mod1(game.clockwise ? idx + 1 : idx - 1, length(game.players))

end nextplayer!(game) = nextplayer!(game, game.pnow)

"""

   nextsaiduno(game)

Returns true if the next player to play has said Uno, which means they have only one card left. If so, it is best for the current player if they play to make them draw or lose a turn. """ function nextsaiduno(game)

   idx = game.pnow
   nextp = mod1(game.pnow + (game.clockwise ? 1 : -1), length(game.players))
   return length(game.players[nextp].hand) == 1

end

"""

   UnoCardGameState(playernames = ["Player", "Bot1", "Bot2", "Bot3"])

Construct and initialize Uno game. Includes dealing hands and picking who is to start. """ function UnoCardGameState(playernames = ["Player", "Bot1", "Bot2", "Bot3"])

   deck = shuffle(originaldeck)
   discardpile, drawpile = UnoCard[], UnoCard[]
   while true  # cannot start with a Draw Four on discard pile top
       discardpile, drawpile = [deck[29]], deck[30:end]
       last(last(discardpile)) != "Draw Four" && break
       deck[29:end] .= shuffle(deck[29:end])
   end
   hands = [deck[i:i+6] for i in 1:7:27]
   game = UnoCardGameState(drawpile, discardpile, [UnoCardGamePlayer(playernames[i],
       startswith(playernames[i], "Bot") ? true : false, hands[i])
       for i in 1:length(playernames)], 1, "Wild", true, true)
   dealer = rand(1:length(playernames))
   logline("Player $(playernames[dealer]) is dealer.")
   # handle an initial Reverse card
   if type(last(discardpile)) == "Reverse"
           game.clockwise = false
           logline("First card Reverse, so starting counterclockwise with dealer.")
           game.commandsvalid = false
           game.pnow = dealer
   else
       nextplayer!(game, dealer)
   end
   logline("Player $(playernames[game.pnow]) starts play.")
   if color(last(discardpile)) == "Wild"
       choosecolor!(game)
       game.commandsvalid = false
   else
       game.colornow = color(last(discardpile))
   end
   return game

end

cardvalue(c::UnoCard) = something(findfirst(x -> x == type(c), typeordering), 0) countcolor(cards, clr) = count(c -> color(c) == clr, cards) colorcounts(cards) = sort!([(countcolor(cards, clr), clr) for clr in colors])

""" Preferred color is the one that is most counted in the hand. """ preferredcolor(hand) = last(last(colorcounts(hand)))

"""

   playableindices(game)

Return a vector of indices of cards in hand that are legal to discard """ function playableindices(game)

   hand = game.players[game.pnow].hand
   mcolor, mtype = game.colornow, type(game.discardpile[end])
   return [i for (i, c) in enumerate(hand) if
       color(c) == mcolor || type(c) == mtype || color(c) == "Wild"]

end

""" Current player to draw n cards from the draw pile. """ function drawcardsfromdeck!(game, n=1)

   logline("Player $(game.players[game.pnow].name) draws $n card$(n == 1 ? "" : "s").")
   for _ in 1:n
       push!(game.players[game.pnow].hand, pop!(game.drawpile))
   end

end

"""

   discard!(game, idx = -1)

Current player to discard card at index idx in hand (last card in hand as default). Handle wild cartd discard by having current player choose the new game.colornow. """ function discard!(game, idx = -1)

   hand = game.players[game.pnow].hand
   if idx != -1
       hand[idx], hand[end] = hand[end], hand[idx]
   end
   push!(game.discardpile, pop!(hand))
   lastdiscard = last(game.discardpile)
   logline("Player $(game.players[game.pnow].name) discarded $lastdiscard")
   if color(lastdiscard) == "Wild"  # wild card discard, so choose a color to be colornow
       choosecolor!(game)
       logline("New color chosen: $(game.colornow)")
   else
       game.colornow = color(lastdiscard)
   end
   game.commandsvalid = true

end

"""

   turn!(game)

Execute a single turn of the game. Command cards are followed only the first turn after played. """ function turn!(game)

   name, hand = game.players[game.pnow].name, game.players[game.pnow].hand
   lastdiscard, indices = game.discardpile[end], playableindices(game)
   mcolor, mtype = game.colornow, type(lastdiscard)
   isempty(hand) && error("Empty hand held by $name")
   if mtype in cmdtypes && game.commandsvalid && mtype != "Wild"
       game.commandsvalid = false
       if mtype == "Draw Four"
           logline("$name must draw four.")
           drawcardsfromdeck!(game, 4)
       elseif mtype == "Draw Two"
           logline("$name must draw two.")
           drawcardsfromdeck!(game, 2)
       elseif mtype == "Skip"    # skip, no uno check
           logline("$name skips a turn.")
       elseif mtype == "Reverse"
           game.clockwise = !game.clockwise
           logline("Reverse: now going $(game.clockwise ? "clockwise." : "counter-clockwise.")")
           nextplayer!(game)
       end
       nextplayer!(game)
       return
   else  # num card, or command card is already used
       if isempty(indices)
           drawcardsfromdeck!(game)  # draw, then discard if drawn card is a match
           indices = playableindices(game)
           !isempty(indices) && discard!(game, first(indices))
       elseif !startswith(name, "Bot")  # not bot, so player moves
           logline("Click on a card to play.")
           flushchannel()
           while true
               item = take!(channel)
               if item isa Int && item in indices
                   discard!(game, item)
                   break
               end
               logline("That card is not playable.")
           end
       elseif nextsaiduno(game)  # bot might need to stop next player win
           sort!(hand, lt = (x, y) -> cardvalue(x) < cardvalue(y))
           indices = playableindices(game)
           discard!(game, last(indices))
       else # bot play any playable in hand
           discard!(game, rand(indices))
       end
   end
   length(hand) == 1 && logline("$name says UNO!")
   nextplayer!(game)

end

"""

   choosecolor!(game)

Choose a new game.colornow, automatically if a bot, via player choice if not a bot. """ function choosecolor!(game)

   logline("Player $(game.players[game.pnow].name) choosing color")
   hand = game.players[game.pnow].hand
   isempty(hand) && return rand(colors)
   if game.players[game.pnow].isabot
       game.colornow = preferredcolor(hand)
   else
       flushchannel()
       while true
           item = take!(channel)
           if item isa String && item in colors
               game.colornow = item
               break
           end
       end
   end
   logline("Current color is now $(game.colornow).")

end


  1. ============ GUI interface section =======================#

const logwindow = GtkScrolledWindow() const logtxt = GtkTextBuffer() logtxt.text[String] = "Started a game of Uno." const tview = GtkTextView(logtxt) push!(logwindow, tview)

""" Lines are logged by extending logtxt at its start. """ function logline(txt)

   set_gtk_property!(logtxt, :text, txt * "\n" * get_gtk_property(logtxt, :text, String))

end

const cairocolor = Dict("Red" => colorant"red", "Yellow" => colorant"gold",

   "Green" => colorant"green", "Blue" => colorant"blue", "Wild" => colorant"black")

""" Draw a UnoCard as a rectangle with rounded corners. """ function cairocard(ctx, card, x0, y0, width, height, bcolor=colorant"white")

   fcolor = cairocolor[color(card)]
   set_source(ctx, fcolor)
   radius = (width + height) / 4
   set_line_width(ctx, radius / 5)
   x1 = x0 + width
   y1 = y0 + height
   if width / 2 < radius
       if height / 2 < radius
           move_to(ctx, x0, (y0 + y1) / 2)
           curve_to(ctx, x0 ,y0, x0, y0, (x0 + x1) / 2, y0)
           curve_to(ctx, x1, y0, x1, y0, x1, (y0 + y1) / 2)
           curve_to(ctx, x1, y1, x1, y1, (x1 + x0) / 2, y1)
           curve_to(ctx, x0, y1, x0, y1, x0, (y0 + y1) / 2)
       else
           move_to(ctx, x0, y0 + radius)
           curve_to(ctx, x0 ,y0, x0, y0, (x0 + x1) / 2, y0)
           curve_to(ctx, x1, y0, x1, y0, x1, y0 + radius)
           line_to(ctx, x1 , y1 - radius)
           curve_to(ctx, x1, y1, x1, y1, (x1 + x0) / 2, y1)
           curve_to(ctx, x0, y1, x0, y1, x0, y1 - radius)
       end
   else
       if rect_height / 2 < radius
           move_to(ctx, x0, (y0 + y1)  /2)
           curve_to(ctx, x0 , y0, x0 , y0, x0 + radius, y0)
           line_to(ctx, x1 - radius, y0)
           curve_to(ctx, x1, y0, x1, y0, x1, (y0 + y1) / 2)
           curve_to(ctx, x1, y1, x1, y1, x1 - radius, y1)
           line_to(ctx, x0 + radius, y1)
           curve_to(ctx, x0, y1, x0, y1, x0, (y0 + y1) / 2)
       else
           move_to(ctx, x0, y0 + radius)
           curve_to(ctx, x0 , y0, x0 , y0, x0 + radius, y0)
           line_to(ctx, x1 - radius, y0)
           curve_to(ctx, x1, y0, x1, y0, x1, y0 + radius)
           line_to(ctx, x1 , y1 - radius)
           curve_to(ctx, x1, y1, x1, y1, x1 - radius, y1)
           line_to(ctx, x0 + radius, y1)
           curve_to(ctx, x0, y1, x0, y1, x0, y1- radius)
       end
   end
   close_path(ctx)
   set_source(ctx, bcolor)
   fill_preserve(ctx)
   set_source(ctx, fcolor)
   stroke(ctx)
   move_to(ctx, x0 + width / 3, y0 + height / 3)
   txt = uppercase(type(card))
   if first(txt) in ['R', 'S', 'W']
       txt = string(first(txt))
   elseif first(txt) == 'D'
       txt = "D" * (txt[end] == 'O' ? "2" : "4")
   end
   show_text(ctx, txt)
   stroke(ctx)

end

""" Face down Uno cards are displayed as blank black rectangles with rounded corners. """ function cairodrawfacedowncard(ctx, x0, y0, width, height, bcolor=colorant"darkgray")

   cairocard(ctx, UnoCard("Wild", " "), x0, y0, width, height, bcolor)

end

"""

   UnoCardGameApp(w = 800, hcan = 600, hlog = 100)

Uno card game Gtk app. Draws game on a canvas, logs play on box below canvas. """ function UnoCardGameApp(w = 800, hcan = 600, hlog = 100)

   win = GtkWindow("Uno Card Game", w, hcan + hlog) |> (GtkFrame() |> (vbox = GtkBox(:v)))
   set_gtk_property!(vbox, :expand, true)
   can = GtkCanvas(w, hcan)
   push!(vbox, can)
   push!(vbox, logwindow)  # from log section
   set_gtk_property!(logwindow, :expand, true)
   fontpointsize = w / 50
   cardpositions = Dict{Int, Vector{Int}}()
   colorpositions = Dict("Red" => [280, 435, 320, 475], "Yellow" => [340, 435, 380, 475],
       "Green" => [400, 435, 440, 475], "Blue" => [460, 435, 500, 475])
   game = UnoCardGameState()
   """ Draw the game board on the canvas including player's hand """
   @guarded Gtk.draw(can) do widget
       ctx = Gtk.getgc(can)
       height, width = Gtk.height(ctx), Gtk.width(ctx)
       select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD)
       set_font_size(ctx, fontpointsize)
       boardcolor = colorant"lightyellow"
       set_source(ctx, boardcolor)
       rectangle(ctx, 0, 0, width, height)
       fill(ctx)
       color = colorant"navy"
       set_source(ctx, color)
       move_to(ctx, 360, 400)
       show_text(ctx, game.players[1].name)
       stroke(ctx)
       move_to(ctx, 60, 300)
       show_text(ctx, game.players[2].name)
       stroke(ctx)
       move_to(ctx, 370, 60)
       show_text(ctx, game.players[3].name)
       stroke(ctx)
       move_to(ctx, 680, 300)
       show_text(ctx, game.players[4].name)
       stroke(ctx)
       cairocard(ctx, last(game.discardpile), 350, 240, 40, 80)
       cairodrawfacedowncard(ctx, 410, 240, 40, 80)
       for (i, p) in enumerate(colorpositions)
            set_source(ctx, cairocolor[first(p)])
            x0, y0, x1, y1 = last(p)
            rectangle(ctx, x0, y0, 40, 40)
            fill(ctx)
       end
       hand = first(game.players).hand
       isempty(hand) && return
       nrow = (length(hand) + 15) ÷ 16
       for row in nrow
           cards = hand[(row - 1) * 16 + 1 : min(length(hand), row * 16 - 1)]
           startx, starty = 40 + (16 - length(cards)) * 20, 500 + 30 * (row - 1)
           for (i, card) in enumerate(cards)
               idx, x0 = (row - 1) * 16 + i, startx + 50 * (i - 1)
               cardpositions[idx] = [x0, starty, x0 + 40, starty + 80]
               cairocard(ctx, card, x0, starty, 40, 80)
           end
       end
   end
   """ Gtk mouse callback: translates vaild mouse clicks to a channel item """
   signal_connect(can, "button-press-event") do b, evt
       for p in colorpositions
           x0, y0, x1, y1 = last(p)
           if x0 < evt.x < x1 && y0 < evt.y < y1
               push!(channel, first(p))
               return
           end
       end
       for p in cardpositions
           x0, y0, x1, y1 = last(p)
           if x0 < evt.x < x1 && y0 < evt.y < y1
               push!(channel, first(p))
               return
           end
       end
   end
   draw(can)
   Gtk.showall(win)
   while !any(i -> isempty(game.players[i].hand), 1:4)
       turn!(game)
       sleep(3)
       draw(can)
       show(can)
   end
   winner = findfirst(i -> isempty(game.players[i].hand), 1:4)
   logline("Player number $winner wins!")
   info_dialog(winner == nothing ? "No winner found." :
       "The WINNER is $(game.players[winner].name)!", win)

end

UnoCardGameApp() </lang>