Create a simple version of the board game:   Mastermind.

It must be possible to:

   choose the number of colors will be used in the game (2 - 20)
   choose the color code length (4 - 10)
   choose the maximum number of guesses the player has (7 - 20)
   choose whether or not colors may be repeated in the code


The (computer program) game should display all the player guesses and the results of that guess.

Display (just an idea):



 Feature 
 Graphic Version 
 Text Version


 Player guess

 Colored circles

 Alphabet letters


Correct color & position

Black circle

X


Correct color

White circle

O


None

Gray circle

 -




A text version example:        1.   ADEF   -   XXO- 

Translates to:

the first guess;

the four colors (ADEF);

result:

 two correct colors and spot, 
 one correct color/wrong spot, one color isn't in the code.
Happy coding!



Related tasks

   Bulls and cows
   Bulls and cows/Player
   Guess the number
   Guess the number/With Feedback


using Gtk, Colors, Cairo, Graphics
 
struct Guess
    code::Vector{Color}
    guess::Vector{Color}
    hint::Vector{Color}
end
 
function Guess(code, guess)
    len = length(code)
    hints = fill(colorant"gray", len) # gray default
    for (i, g) in enumerate(guess), (j, c) in enumerate(code)
        if g == c
            if i == j
                hints[i] = colorant"black"
            elseif hints[i] != colorant"black"
                hints[i] = colorant"white"
            end
        end
    end
    g = Guess([c for c in code], [c for c in guess], [c for c in hints])
end
 
tovec(guess) = [x for x in [guess.code; guess.guess; guess.hint]]
 
function mastermindapp()
    allu(s) = length(unique(s)) == length(s)
    ccode(c, n, rok) = while true a = rand(c, n); if rok || allu(a) return a end end
 
    numcolors, codelength, maxguesses, allowrep, gameover = 10, 4, 10, false, false
    colors = distinguishable_colors(numcolors)
    code = ccode(colors, numcolors, allowrep)
    guesshistory = Vector{Guess}()
 
    win = GtkWindow("Mastermind Game", 900, 750) |> (GtkFrame() |> (box = GtkBox(:v)))  
    settingsbox = GtkBox(:h)
    playtoolbar = GtkToolbar()
 
    setcolors = GtkScale(false, 4:20)
    set_gtk_property!(setcolors, :hexpand, true)
    adj = GtkAdjustment(setcolors)
    set_gtk_property!(adj, :value, 10)
    clabel = GtkLabel("Number of Colors")
 
    setcodelength = GtkScale(false, 4:10)
    set_gtk_property!(setcodelength, :hexpand, true)
    adj = GtkAdjustment(setcodelength)
    set_gtk_property!(adj, :value, 4)
    slabel = GtkLabel("Code Length")
 
    setnumguesses = GtkScale(false, 4:40)
    set_gtk_property!(setnumguesses, :hexpand, true)
    adj = GtkAdjustment(setnumguesses)
    set_gtk_property!(adj, :value, 10)
    nlabel = GtkLabel("Max Guesses")
 
    allowrepeatcolor = GtkScale(false, 0:1)
    set_gtk_property!(allowrepeatcolor, :hexpand, true)
    rlabel = GtkLabel("Allow Repeated Colors (0 = No)")
 
    newgame = GtkToolButton("New Game")
    set_gtk_property!(newgame, :label, "New Game")
    set_gtk_property!(newgame, :is_important, true)
 
    tryguess = GtkToolButton("Submit Current Guess")
    set_gtk_property!(tryguess, :label, "Submit Current Guess")
    set_gtk_property!(tryguess, :is_important, true)
 
    eraselast = GtkToolButton("Erase Last (Unsubmitted) Pick")
    set_gtk_property!(eraselast, :label, "Erase Last (Unsubmitted) Pick")
    set_gtk_property!(eraselast, :is_important, true)
 
    map(w->push!(settingsbox, w),[clabel, setcolors, slabel, setcodelength,
        nlabel, setnumguesses, rlabel, allowrepeatcolor])
    map(w->push!(playtoolbar, w),[newgame, tryguess, eraselast])
 
    scrwin = GtkScrolledWindow()
    can = GtkCanvas()
    set_gtk_property!(can, :expand, true)
    map(w -> push!(box, w),[settingsbox, playtoolbar, scrwin])
    push!(scrwin, can)
 
    currentguess = RGB[]
    guessesused = 0
    colorpositions = Point[]
 
    function newgame!(w)
        empty!(guesshistory)
 
        numcolors = Int(GAccessor.value(setcolors))
        codelength = Int(GAccessor.value(setcodelength))
        maxguesses = Int(GAccessor.value(setnumguesses))
        allowrep = Int(GAccessor.value(allowrepeatcolor))
 
        colors = distinguishable_colors(numcolors)
        code = ccode(colors, codelength, allowrep == 1)
        empty!(currentguess)
        currentneeded = codelength
        guessesused = 0
        gameover = false
        draw(can)
    end
    signal_connect(newgame!, newgame, :clicked)
 
    function saywon!()
        warn_dialog("You have WON the game!", win)
        gameover = true
    end
 
    function outofguesses!()
        warn_dialog("You have Run out of moves! Game over.", win)
        gameover = true    
    end
 
    can.mouse.button1press = @guarded (widget, event) -> begin
        if !gameover && (i = findfirst(p -> 
            sqrt((p.x - event.x)^2 + (p.y - event.y)^2) < 20,
                colorpositions)) != nothing
            if length(currentguess) < codelength
                if allowrep == 0 && !allu(currentguess)
                    warn_dialog("Please erase the duplicate color.", win)
                else
                    push!(currentguess, colors[i])
                    draw(can)
                end
            else
                warn_dialog("You need to submit this guess if ready.", win)
            end
        end
    end
 
    @guarded draw(can) do widget
        ctx = Gtk.getgc(can)
        select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD)
        fontpointsize = 12
        set_font_size(ctx, fontpointsize)
        workcolor = colorant"black"
        set_source(ctx, workcolor)
        move_to(ctx, 0, fontpointsize)
        show_text(ctx, "Color options: " * "-"^70)
        stroke(ctx)
        empty!(colorpositions)
        for i in 1:numcolors
            set_source(ctx, colors[i])
            circle(ctx, i * 40, 40, 20)
            push!(colorpositions, Point(i * 40, 40))
            fill(ctx)
        end
        set_gtk_property!(can, :expand, false) # kludge for good text overwriting
        move_to(ctx, 0, 80)
        set_source(ctx, workcolor)
        show_text(ctx, string(maxguesses - guessesused, pad = 2) * " moves remaining.")
        stroke(ctx)
        set_gtk_property!(can, :expand, true)
        for i in 1:codelength
            set_source(ctx, i > length(currentguess) ? colorant"lightgray" : currentguess[i])
            circle(ctx, i * 40, 110, 20)
            fill(ctx)
        end
        if length(guesshistory) > 0
            move_to(ctx, 0, 155)
            set_source(ctx, workcolor)
            show_text(ctx, "Past Guesses: " * "-"^70)
            for (i, g) in enumerate(guesshistory), (j, c) in enumerate(tovec(g)[codelength+1:end])
                x = j * 40 + (j > codelength ? 20 : 0)
                y = 150 + 40 * i
                set_source(ctx, c)
                circle(ctx, x, y, 20)
                fill(ctx)
            end
        end
        Gtk.showall(win)
    end
 
    function submitguess!(w)
        if length(currentguess) == length(code)
            g = Guess(code, currentguess)
            push!(guesshistory, g)
            empty!(currentguess)
            guessesused += 1
            draw(can)
            if all(i -> g.code[i] == g.guess[i], 1:length(code))
                saywon!()
            elseif guessesused > maxguesses
                outofguesses!()
            end
        end
    end
    signal_connect(submitguess!, tryguess, :clicked)
 
    function undolast!(w)
        if length(currentguess) > 0
            pop!(currentguess)
            draw(can)
        end
    end
    signal_connect(undolast!, eraselast, :clicked)
 
    newgame!(win)
    Gtk.showall(win)
 
    condition = Condition()
    endit(w) = notify(condition)
    signal_connect(endit, win, :destroy)
    showall(win)
    wait(condition)
end
 
mastermindapp()
 