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


-- demo/rosetta/mastermind.exw
constant SET_LIMIT = 1_000_000  -- above this, it uses random sampling.
 
constant help_text = """
The game of mastermind, with a Knuth solver.
 
Specify the number of colours (1..20), the code length (1..10), the
number of guesses allowed (1-20), and whether colours can be repeated
(auto-ticked & greyed-out inactive when length>colours).
 
Note that at the highest settings there are 10,240,000,000,000 possible
answers: the (order n squared) analysis of that is simply not practical, 
as indeed is simply building the initial list of all possible answers, 
and therefore a fixed limit of 1,000,000 has been applied, which also 
just about manages to keep the program responsive. Obviously, should the
actual answer not be among those samples, it cannot possibly find it,
and it will tell you in plain english when that occurs. You can always 
trim the search space back to something more reasonable at any time, and 
still play the game when that limit is breached, with weaker hints. 
 
Conversely the lowest settings do not make for an interesting game, but
proved quite useful when ironing out some bugs, so were left in.
 
The Use button (disabled until something useful found) allows you to 
take the best found (so far), displayed at the top of the colours frame. 
Obviously "1/1 (100%)" means that it has deduced the correct answer.
Below that the colours frame shows all available colours, which can be 
individually clicked in any order.
 
Press Delete or click on the last peg (in the left-hand game frame)
to remove it, before the last one is placed, however once full your
turn is immediately scored and cannot be undone.
 
New Game, Help, and Exit buttons are assumed to be self-explanatory.
Changing the option settings implicitly triggers a new game, except 
for the number of permitted guesses, pre-game-over. Reducing the
number of guesses can also be used as a means of conceding.
 
When a correct guess is entered or all guesses have been used the hint 
and colours are replaced with "GAME OVER - YOU WIN/LOSE" under the 
actual answer.
"""
include pGUI.e
 
Ihandle dlg, colours, codelen, maxgoes, repeats, progress, 
        usehint, game_canvas, colour_canvas
integer ncolours, ncodelen, nmaxgoes
bool brepeats
 
sequence secret = {},
         hint = {},
         guesses = {{}},
         scores = {}
--
-- note: while the game is ongoing, length(guesses) should always be
--       length(scores)+1; equal lengths is equivalent to game over.
--
 
function get_score(sequence guess, goal)
integer blacks = 0, -- (right colour & place)
        whites = 0  -- ("" but wrong place)
    for i=1 to length(guess) do
        if guess[i]=goal[i] then
            blacks += 1
            guess[i] = ' '
            goal[i] = ' '
        end if
    end for
    for i=1 to length(guess) do
        if guess[i]!=' ' then
            integer k = find(guess[i],goal)
            if k then
                whites += 1
                goal[k] = ' '
            end if
        end if
    end for
    return {blacks, whites}
end function
 
function random_set()
-- create the secret code, and/or (when rqd) a SET_LIMIT random sample
    sequence cset = tagset(ncolours),
             res = repeat(0,ncodelen)
    for i=1 to ncodelen do
        integer c = rand(length(cset))
        res[i] = cset[c]
        if not brepeats then
            cset[c..c] = {}
        end if
    end for
    return res
end function
 
sequence initial_set
atom is_len,    -- logically length(initial_set), except when > SET_LIMIT.
     excluded   -- initialset[1..excluded-1], are not, [excluded..$] are.
 
procedure create_initial_set()
    is_len = iff(brepeats?power(ncolours,ncodelen):k_perm(ncolours,ncodelen))
    if is_len<=SET_LIMIT then
        --
        -- generate the full set
        --
        initial_set = repeat(0,is_len)
        excluded = is_len+1 -- (ie none)
        sequence next = iff(brepeats?repeat(1,ncodelen):tagset(ncodelen))
        for i=1 to is_len do
            initial_set[i] = next
            for ndx=length(next) to 1 by -1 do
                integer n = next[ndx]
                while n<=ncolours do
                    n += 1
                    if brepeats 
--                  or not find(n,next[1..ndx-1]) then 
                    or not find(n,next) then --(see below)
                        exit
                    end if
                end while
                next[ndx] = n
                if n<=ncolours then
                    if not brepeats then
                        --
                        -- Fill in the rest lowest-first, eg
                        -- in the 4 colours and 4 holes case:
                        --   (start)       (above)       (this)
                        --  {1,2,3,4} --> {1,2,4,_} --> {1,2,4,3}
                        --  {1,2,4,3} --> {1,3,_,_} --> {1,3,2,4}
                        --  ...   (20 other cases omitted)
                        --  {4,3,1,2} --> {4,3,2,_} --> {4,3,2,1}
                        --
                        -- (probably sub-optimal, but insignificant 
                        --  vs. the o(n^2) analysis which follows.)
                        --
                        for j=ndx+1 to length(next) do
                            for k=1 to ncolours do
--                              if not find(k,next[1..j-1]) then
                                if not find(k,next) then --(see below)
                                    next[j] = k
                                    exit
                                end if
                            end for
                        end for
                    end if
                    exit
                end if
                --
                -- technical note: if not brepeats, we are going to
                -- replace all next[ndx..$] later/above anyway, but
                -- replacing with 0 means we can avoid those slices.
                -- The next three all work: 1 is perfect for the
                -- brepeats=true case, but brepeats=false needs the 
                -- above slices, while the 2nd & 3rd are equivalent
                -- the latter is obviously somewhat faster, at the
                -- cost of a wtf?!, without a comment such as this.
                --
--              next[ndx] = 1
--              next[ndx] = iff(brepeats?1:0)
                next[ndx] = brepeats -- (equivalent)
            end for
        end for
    else
        --
        -- generate SET_LIMIT random codes
        -- note that if (as is quite likely) the actual answer is
        -- not present in initial_set, then obviously it cannot
        -- possibly find it!
        --
        initial_set = repeat(0,SET_LIMIT)
        excluded = SET_LIMIT+1  -- (ie none)
        for i=1 to SET_LIMIT do
            initial_set[i] = random_set()
        end for
    end if
end procedure
 
atom done, is_done, best
 
function idle_action()
    atom to_do = excluded-1,
         t1 = time()+1
    string samp = iff(is_len=length(initial_set)?"":sprintf(" samples of %,d",{is_len}))
    for i=1 to 100000 do    -- reasonable slice of work
        done += 1
        is_done += (done<excluded)
        sequence guest = initial_set[done],
                 scores = {}, counts = {}
        if not find(guest,guesses) then
            for j=1 to excluded-1 do
                sequence s = get_score(guest,initial_set[j])
                integer k = find(s,scores)
                if k=0 then
                    scores = append(scores,s)
                    counts = append(counts,1)
                else
                    counts[k] += 1
                end if
            end for
            if length(counts)=0 then
                IupSetStrAttribute(progress,"TITLE","[answer not in %,d%s]",{SET_LIMIT,samp})
                return IUP_IGNORE   -- (stop idle)
            end if
            integer k = largest(counts,return_index:=true),
                    ck = counts[k]
            if ck<best then
                best = ck
                hint = guest
                IupSetInt(usehint,"ACTIVE",true)
                IupUpdate(colour_canvas)
            end if
        end if
 
        if done=length(initial_set) then
            IupSetStrAttribute(progress,"TITLE","%,d/%,d%s (100%%)",{is_done,to_do,samp})
            return IUP_IGNORE   -- (stop idle)
        end if
        if time()>t1 then exit end if
    end for
    IupSetStrAttribute(progress,"TITLE","%,d/%,d%s (%d%%)",{is_done,to_do,samp,100*(is_done/to_do)})
    return IUP_DEFAULT
end function
constant idle_action_cb = Icallback("idle_action")
 
procedure start_idle()
    done = 0
    is_done = 0
    best = length(initial_set)+1
    IupSetGlobalFunction("IDLE_ACTION",idle_action_cb)
end procedure
 
procedure new_game()
    ncolours = IupGetInt(colours,"VALUE")
    ncodelen = IupGetInt(codelen,"VALUE")
    nmaxgoes = IupGetInt(maxgoes,"VALUE")
    brepeats = IupGetInt(repeats,"VALUE")
    secret = random_set()
    guesses = {{}}
    scores = {}
    hint = {}
    create_initial_set()
    start_idle()
end procedure
 
constant colour_table = {#e6194b,   -- Red
                         #3cb44b,   -- Green
                         #ffe119,   -- Yellow
                         #4363d8,   -- Blue
                         #f58231,   -- Orange
                         #911eb4,   -- Purple
                         #42d4f4,   -- Cyan
                         #f032e6,   -- Magenta
                         #bfef45,   -- Lime
                         #fabebe,   -- Pink
                         #469990,   -- Teal
                         #e6beff,   -- Lavender
                         #9A6324,   -- Brown
                         #fffac8,   -- Beige
                         #800000,   -- Maroon
                         #aaffc3,   -- Mint
                         #808000,   -- Olive
                         #ffd8b1,   -- Apricot
                         #000075,   -- Navy
                         #a9a9a9}   -- Grey
 
-- saved in redraw_cb(), for click testing in button_cb():
sequence last_guess = {},
         colour_centres = {}
integer guess_r2 = 0,
        colour_r2 = 0
 
function redraw_cb(Ihandle ih, integer /*posx*/, integer /*posy*/)
    Ihandle frame = IupGetParent(ih)
    string title = IupGetAttribute(ih,"TITLE")
    if not find(title,{"Game","Colours"}) then ?9/0 end if
    integer {cw,ch} = IupGetIntInt(ih, "DRAWSIZE")
 
    cdCanvas cddbuffer = IupGetAttributePtr(ih,"DBUFFER")
    IupGLMakeCurrent(ih)
    cdCanvasActivate(cddbuffer)
    cdCanvasClear(cddbuffer)
 
    if title="Game" then
        integer mx = min(floor(cw/(ncodelen*1.5+0.5)),floor(ch/(nmaxgoes+1))),
                diameter = floor(mx/2),
                px = floor((cw-(ncodelen*1.5+0.5)*mx)/2), -- (set margin)
                cy = ch, cx, c, r
        last_guess = {}
        for g=1 to length(guesses) do
            cy -= mx
            cx = px+floor(mx/2)
            for i=1 to 2*ncodelen+1 do
                if i!=ncodelen+1 then
                    if i<=ncodelen then
                        if i<=length(guesses[g]) then
                            c = colour_table[guesses[g][i]]
                            if g=length(guesses) then
                                last_guess = {{cx,ch-cy}}
                            end if
                        else
                            c = CD_GREY
                        end if
                        r = diameter
                    else
                        c = CD_GREY
                        if g<=length(scores) then
                            integer k = i-ncodelen-1,
                                    {b,w} = scores[g]
                            c = iff(k<=b ? CD_BLACK : iff(k<=b+w ? CD_WHITE : CD_GREY))
                        end if
                        r = floor(diameter*0.5)
                    end if
                    cdCanvasSetForeground(cddbuffer,c)
                    cdCanvasSector(cddbuffer, cx, cy, r, r, 0, 360)
                    cdCanvasSetForeground(cddbuffer,CD_DARK_GREY)
                    cdCanvasCircle(cddbuffer, cx, cy, r)
                end if
                cx += iff(i<ncodelen?mx:floor(mx/2))
            end for
        end for
        guess_r2 = floor(diameter*diameter/4)
    elsif title="Colours" then
        integer mx = min(floor(cw/ncodelen),floor(ch/2)),
                r = floor(mx/2),
                px = floor((cw-ncodelen*mx)/2), -- (set margin)
                cy = ch-r, cx, c
        cx = px+floor(mx/2)
        bool active = length(hint)>0
        if length(scores)=nmaxgoes then
            hint = secret
            active = true
        end if
        for i=1 to ncodelen do
            c = iff(active?colour_table[hint[i]]:CD_GREY)
            cdCanvasSetForeground(cddbuffer,c)
            cdCanvasSector(cddbuffer, cx, cy, r, r, 0, 360)
            cdCanvasSetForeground(cddbuffer,CD_DARK_GREY)
            cdCanvasCircle(cddbuffer, cx, cy, r)
            cx += mx
        end for
        if length(scores)=nmaxgoes
        or guesses[$]=secret then
            ch -= floor(mx/2)
            {} = cdCanvasTextAlignment(cddbuffer, CD_CENTER)
            string wl = iff(guesses[$]=secret?"WIN":"LOSE"),
                   msg = sprintf("GAME OVER - YOU %s",{wl})
            cdCanvasText(cddbuffer, cw/2, ch/2, msg) 
        else
            integer ch0 = ch
            ch -= mx
            --
            -- calculate the best nw*nh way to fit all the colours in:
            -- (if nw ends up = ncodelen there is no clear separation
            --  between the hint and the colour table; the start with
            --  ncodelen+1 solves that and looks pretty good to me.)
            --
            integer nw = ncodelen+1,    -- (as above)
                    nh = 1
            while nw*nh<ncolours do
                if (cw/(nw+1))>(ch/(nh+1)) then
                    nw += 1
                else
                    nh += 1
                end if
            end while
            --
            -- now draw all the colours
            --
            mx = min(floor(cw/nw),floor(ch/nh))
            r = floor(mx/2)
            px = floor((cw-nw*mx)/2)
            cx = px+floor(mx/2)
            cy = ch-r
            integer this_row = 0
            colour_centres = repeat(0,ncolours)
            colour_r2 = floor(r*r/4)
            for i=1 to ncolours do
                colour_centres[i] = {cx,ch0-cy}
                c = colour_table[i]
                cdCanvasSetForeground(cddbuffer,c)
                cdCanvasSector(cddbuffer, cx, cy, r, r, 0, 360)
                cdCanvasSetForeground(cddbuffer,CD_DARK_GREY)
                cdCanvasCircle(cddbuffer, cx, cy, r)
                cx += mx
                this_row += 1
                if this_row>=nw then
                    this_row = 0
                    cx = px + floor(mx/2)
                    cy -= mx
                end if
            end for
        end if
    end if
    cdCanvasFlush(cddbuffer)
    return IUP_DEFAULT
end function
 
function map_cb(Ihandle ih)
    IupGLMakeCurrent(ih)
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvas cddbuffer = cdCreateCanvas(CD_GL, "10x10 %g", {res})
    IupSetAttributePtr(ih,"DBUFFER",cddbuffer)
    cdCanvasSetBackground(cddbuffer, CD_PARCHMENT)
    return IUP_DEFAULT
end function
 
function canvas_resize_cb(Ihandle canvas)
    cdCanvas cddbuffer = IupGetAttributePtr(canvas,"DBUFFER")
    integer {canvas_width, canvas_height} = IupGetIntInt(canvas, "DRAWSIZE")
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvasSetAttribute(cddbuffer, "SIZE", "%dx%d %g", {canvas_width, canvas_height, res})
    return IUP_DEFAULT
end function
 
procedure redraw_all()
    IupUpdate({game_canvas,colour_canvas})
end procedure
 
procedure undo_move()
-- Called from button_cb and from K_DEL, but latter may be invalid.
    if length(guesses[$])!=0 then
        guesses[$] = guesses[$][1..$-1]
        redraw_all()
    end if
end procedure
 
procedure add_move(integer i)
    if i!=0 then
        guesses[$] &= i
    end if
    if length(guesses[$])=ncodelen then
        sequence guest = guesses[$],
                 score = get_score(guest,secret)
        scores = append(scores,score)   
        if score!={ncodelen,0}  -- (not all black==game over)
        and length(guesses)<nmaxgoes then
            for i=excluded-1 to 1 by -1 do
                sequence isi = initial_set[i]
                if get_score(guest,isi)!=score then
                    excluded -= 1
                    if excluded!=i then
                        initial_set[i] = initial_set[excluded]
                        initial_set[excluded] = isi -- (swap)
                    end if
                end if
            end for
            guesses = append(guesses,{})
            hint = {}
            IupSetAttribute(progress,"TITLE","-")
            IupSetInt(usehint,"ACTIVE",false)
            start_idle()
        end if
    end if
    redraw_all()
end procedure
 
function usehint_cb(Ihandle /*usehint*/)
    guesses[$] = hint
    add_move(0)
    return IUP_DEFAULT
end function
 
function button_cb(Ihandle canvas, integer button, pressed, x, y, atom /*pStatus*/)
    Ihandle frame = IupGetParent(canvas)
    string title = IupGetAttribute(frame,"TITLE")
    if not find(title,{"Game","Colours"}) then ?9/0 end if
    if button=IUP_BUTTON1 and not pressed then      -- (left button released)
        {sequence centres, integer r2} = iff(title="Game"?{last_guess,guess_r2}
                                                         :{colour_centres,colour_r2})
        for i=1 to length(centres) do
            integer {cx,cy} = sq_sub(centres[i],{x,y})
            if (cx*cx+cy*cy)<=r2 then
                if title="Game" then
                    undo_move()
                else
                    add_move(i)
                end if
                exit
            end if
        end for
    end if
    return IUP_CONTINUE
end function
 
function new_game_cb(Ihandle /*ih*/)
    new_game()
    redraw_all()
    return IUP_DEFAULT
end function
 
function exit_cb(Ihandle /*ih*/)
    return IUP_CLOSE
end function
constant cb_exit = Icallback("exit_cb")
 
function help_cb(Ihandln /*ih*/)
    IupMessage("Mastermind",help_text)
    return IUP_DEFAULT
end function
 
function key_cb(Ihandle /*dlg*/, atom c)
    if c=K_ESC then return IUP_CLOSE end if
    if c=K_F1 then return help_cb(NULL) end if
    if find(c,{K_DEL,K_BS}) then undo_move() end if
    return IUP_CONTINUE
end function
 
function valuechanged_cb(Ihandle ih)
    ncolours = IupGetInt(colours,"VALUE")
    ncodelen = IupGetInt(codelen,"VALUE")
    nmaxgoes = IupGetInt(maxgoes,"VALUE")
    IupSetInt(repeats,"ACTIVE",ncodelen<=ncolours)
    if ncodelen>ncolours then
        IupSetInt(repeats,"VALUE",true)
    end if
    brepeats = IupGetInt(repeats,"VALUE")
    if ih!=maxgoes
    or length(scores)=length(guesses) then  -- (game over)
        new_game()
    elsif nmaxgoes<=length(scores) then
        -- (signal/force game over state)
        guesses = guesses[1..length(scores)]
    end if
    redraw_all()
    return IUP_DEFAULT
end function
constant cb_valuechanged = Icallback("valuechanged_cb")
 
procedure main()
    IupOpen()
 
    colours = IupText("SPIN=Yes, SPINMIN=1, SPINMAX=20, VALUE=6, RASTERSIZE=34x")
    codelen = IupText("SPIN=Yes, SPINMIN=1, SPINMAX=10, VALUE=4, RASTERSIZE=34x")
    maxgoes = IupText("SPIN=Yes, SPINMIN=1, SPINMAX=20, VALUE=7, RASTERSIZE=34x")
    repeats = IupToggle("Repeatable?","VALUE=YES, RIGHTBUTTON=YES, PADDING=5x4")
    progress = IupLabel("-","EXPAND=HORIZONTAL, PADDING=5x4")
    usehint = IupButton("Use",Icallback("usehint_cb"),"PADDING=5x4, ACTIVE=NO")
    game_canvas = IupGLCanvas("RASTERSIZE=200x")
    colour_canvas = IupGLCanvas("RASTERSIZE=x200")
    Ihandle newgame = IupButton("New Game",Icallback("new_game_cb"),"PADDING=5x4"),
            help = IupButton("Help (F1)",Icallback("help_cb"),"PADDING=5x4"),
            quit = IupButton("E&xit",Icallback("exit_cb"),"PADDING=5x4"),
            vbox = IupVbox({IupHbox({IupLabel("Colours (1-20)","PADDING=5x4"),colours}),
                            IupHbox({IupLabel("Code Length (1-10)","PADDING=5x4"),codelen}),
                            IupHbox({IupLabel("Guesses (1-20)","PADDING=5x4"),maxgoes}),
                            IupHbox({repeats},"MARGIN=10x5"),
                            IupHbox({progress}),
                            IupHbox({usehint,newgame,help,quit})},"MARGIN=5x5"),
            game_frame = IupFrame(IupHbox({game_canvas},"MARGIN=3x3"),"TITLE=Game"),
            option_frame = IupFrame(vbox,"TITLE=Options"),
            colour_frame = IupFrame(colour_canvas,"TITLE=Colours"),
            full = IupHbox({game_frame,IupVbox({option_frame,colour_frame})})
    IupSetCallbacks({colours,codelen,maxgoes,repeats}, {"VALUECHANGED_CB", cb_valuechanged})
    IupSetCallbacks({game_canvas,colour_canvas}, {"ACTION", Icallback("redraw_cb"),
                                                  "MAP_CB", Icallback("map_cb"),
                                                  "RESIZE_CB", Icallback("canvas_resize_cb"),
                                                  "BUTTON_CB", Icallback("button_cb")})
    dlg = IupDialog(IupHbox({full},"MARGIN=3x3"),"TITLE=Mastermind")
    IupSetCallback(dlg, "K_ANY", Icallback("key_cb"))
    IupSetAttributeHandle(dlg,"DEFAULTENTER", usehint)
 
    new_game()
 
    IupShowXY(dlg,IUP_CENTER,IUP_CENTER)
    IupSetAttribute(dlg, "RASTERSIZE", NULL)
    IupSetStrAttribute(dlg, "MINSIZE", IupGetAttribute(dlg,"RASTERSIZE"))
    IupMainLoop()
    IupClose()
end procedure
main()