; [Mastermind game]

cslsload "buttons
cslsload "streams

to master [:numsquares 4] [:dup.ok "false] [:mysecret "true]
; Mastermind game program.
; Program is controlled by mouse clicks or keystrokes.
if :LogoPlatform = "Windows [maximize.window "true]
window
if :LogoPlatform = "wxWidgets [localmake "fontsize labelsize]
localmake "colors "ROYGBV
localmake "colornums [[R 4] [O 14] [Y 6] [G 2] [B 1] [V 13]]
localmake "exact "true
local [numguesses numcolors column guess gotnum winloop permuting]
local [perms oldcount newcount guess.exact guess.inexact guess.word]
catch "quit [forever [
  catch "master [
    make "numguesses 0
    make "numcolors 0
    make "column 0
    make "winloop "false
    initdraw		 ; Clear screen, draw color palette 
    ifelse :mysecret [
	ifelse :dup.ok	 ; Choose secret permutation
	   [make "secret (choose.dup :numsquares :colors)]
	   [make "secret (choose.nodup :numsquares :colors)]
        newguess	 ; Display first guess frame
        action.loop	 ; Read keyboard characters or mouse clicks 
    ] [
	catch "win [	 ; User's secret, program has to guess.
	  ifelse :dup.ok [
	    make "permuting "false	; Lots of cases with dups okay, so
	    make "perms (list copies :numsquares "x)
	    make "newcount 0		; find colors systematically first.
	    catch "perm [
	      for [i 1 6] [	; Learn how many red, then orange, etc.
		make "oldcount :newcount
		doguess subst :i "x head :perms
		make "newcount :guess.exact + :guess.inexact
	    	make "perms flatten stream.map
		   `[insert ,[:newcount-:oldcount] ,:i ?] :perms
		make "perms stream.filter
                          `[okay? ? ,:guess.exact ,:guess.inexact ,:guess.word]
			  :perms
		check.consistency :perms
		if equalp :newcount :numsquares [throw "perm]
	      ]
	      check.consistency []	; Tried all colors, user lied.
	    ]
	    make "permuting "true
	  ] [
	    make "perms perms "123456 :numsquares	; not :dup.ok
	    make "permuting equalp :numsquares 6
	  ]
	  forever [			; common portion
	    doguess head :perms
	    if equalp :numsquares :guess.exact + :guess.inexact ~
	      [make "permuting "true]
	    make "perms stream.filter
                          `[okay? ? ,:guess.exact ,:guess.inexact ,:guess.word]
			  :perms
	    check.consistency :perms
	  ]
	  ; Can't get here; either doguess finds a winner or
	  ; check.consistency complains.
	] ; We get here on throw "win from doguess.
	move [15 12]
	setpc 7 label "WIN!
	ct print (sentence [I win in] :numguesses "turns.)
	make "winloop "true
	action.loop
    ]
]]]
cs ct setpc 7 st
end

;;; ================== LOGIC FOR MY GUESSES (USER SECRET) =================

to doguess :guessword
; Present computer's guess to user and ask about matches.
newguess				; Draw frame for guess.
make "guess.word :guessword		; Remember my colors.
foreach :guessword [apply "putguess item ? :colornums]	; Show colors.
askexact				; Ask user for exact matches.
make "gotnum "false
catch "ready [action.loop]
pu setpos [150 205] setpc 0 filled 0 [repeat 2 [fd 35 rt 90 fd 110 rt 90]]
setpc 7
ifelse :guess.exact < :numsquares [	; Not all colors are exact.
  ifelse :permuting [			;   If we know all the colors,
    make "exact "false			;   compute how many are inexact
    getnum :numsquares-:guess.exact	;   without asking.
  ] [
    askinexact				;   Otherwise, ask for inexact.
    make "gotnum "false
    catch "ready [action.loop]
    pu setpos [150 205] setpc 0 filled 0 [repeat 2 [fd 35 rt 90 fd 110 rt 90]]
    setpc 7
    if :guess.exact + :guess.inexact > :numsquares ~
       [check.consistency []]	; Quick error message if too many matches.
  ]
] [
  throw "win				; All colors are exact, we win.
]
end

to subst :new :old :word
; For dups-okay guessing:  Substitute the next trial color for
; all unknown squares in a partial permutation.
output map [ifelse equalp ? :old [:new] [?]] :word
end

to copies :num :letter
output cascade :num [word ? :letter] "
end

to insert :num :new :word
; For dups-okay guessing:  We've learned that there are :NUM instances
; of color :NEW in the secret combination, so stick that many of them into
; a still-possible partial permutation, in every possible size=:NUM
; subset of the unknown slots.
; The result is a *stream* of possible (partial) permutations.
if :num=0 [output (list :word)]		; No slots needed, just one result.
if emptyp :word [output []]		; Not enough slots, no results!
if equalp first :word "x 		; Else combine results of choosing or ~
   [op flatten				; not choosing to replace into this X.
	 stream insert :num-1 :new word :new butfirst :word
	      `[(list stream.map [word "x ?] insert ,:num ",:new bf ",:word )]]
output stream.map `[word ",[first :word] ?] insert :num :new butfirst :word
end

to check.consistency :str
; If the stream of still-possible permutations is empty, then
; the user has lied to us.
if emptyp :str [ct print [Error -- inconsistent answers!]
		repeat 2 [setbg 4 wait 1 setbg 0 wait 1]
		type [Click or type anything to restart.] wait 0
		waitforclick
		throw "master]
end

to perms :word :num
; Output the stream of permutations of :NUM letters chosen from :WORD.
if :num=0 [output (list "|| )]
if emptyp :word [output []]	; Can't happen (would mean :num>count :word).
output flatten stream.map ~
   `[[letter] stream.map `[word ,:letter ?]
                         perms remonce :letter ,:word ,[:num-1]] ~
   :word
end

to okay? :perm :guess.exact :guess.inexact :guess.word
output and (equalp :guess.exact exact :perm :guess.word) ~
           (equalp :guess.inexact inexact :perm :guess.word)
end

to askexact
; ct type "|How many EXACT matches? |
; pu setpos [185 210] setpc 6 label "EXACT?
localmake "caption.scrunch 1.5
setbutton [152 210] [100 25] [] "true 0 "EXACT? []
ern "caption.scrunch
make "exact "true
end

to askinexact
; ct type "|How many INEXACT matches? |
; pu setpos [185 210] setpc 6 label "INEXACT?
localmake "caption.scrunch 1.5
setbutton [152 210] [100 25] [] "true 0 "INEXACT? []
ern "caption.scrunch
make "exact "false
end

;;; ================== LOGIC FOR USER GUESSES (MY SECRET) =================

to choose.dup :number :colors
if :number = 0 [output "]
output word (pick :colors) (choose.nodup :number-1 :colors)
end 
 
to choose.nodup :number :colors
if :number = 0 [output "]
make "color pick :colors
output word :color (choose.nodup :number-1 remonce :color :colors)
end

;;;;; ================ Used by both kinds of logic ======================
 
to exact :secret :guess
if empty? :secret [output 0]
output ehelp + (exact butfirst :secret butfirst :guess)
end

to ehelp
ifelse equal? (first :secret) (first :guess) [output 1] [output 0]
end 

to inexact :secret :guess
output (anymatch :secret :guess) - (exact :secret :guess)
end

to anymatch :secret :guess
if empty? :secret [output 0]
if member? first :secret :guess ~
   [output 1 + anymatch (butfirst :secret) (remonce first :secret :guess)]
output anymatch butfirst :secret :guess
end 

to remonce :this :those
if empty? :those [output "]
if equal? :this first :those [output butfirst :those]
output word (first :those) (remonce :this butfirst :those)
end

;;;;; =================== USER INTERFACE (DRAWING) =======================

to initdraw
fs init.buttons
localmake "bigwidth ifelse :LogoPlatform = "wxWidgets [5*first :fontsize] [40]
localmake "bigbutton list :bigwidth 25
localmake "tallheight ifelse :LogoPlatform="wxWidgets [2+2*last :fontsize] [30]
localmake "tallbutton list :bigwidth :tallheight
ifelse :mysecret ~
  [colorchart 6 "ROYGBV [4 14 6 2 1 13] 165
   setbutton [-245 -15] :bigbutton [clear] "false 0 "erase "DEL] ~
  [numchart 0 165]
setbutton pos0 [-245 -45] :bigbutton [if not :winloop [guess]] "true 0 "OK "RET
setbutton nxt :tallheight :tallbutton [throw "master] "false 0 [new game] "N
setbutton nxt 25 :bigbutton [throw "quit] "false 0 "quit "Q
ignore nxt 10
setbutton nxt :tallheight :tallbutton [make "mysecret "true  throw "master] ~
   :mysecret 0 [I guess] "I
setbutton nxt :tallheight :tallbutton [make "mysecret "false  throw "master] ~
   (not :mysecret) 0 [Logo guess] "L
caption [-245 206] [65 29] [Number |of colors:|]
numsquares -170 2 6
caption [-10 206] [65 29] [Duplicates allowed:]
ifelse :LogoPlatform = "wxWidgets [
localmake "buttonx ((first :fontsize)*10)-5
setbutton list :buttonx 210 [25 25] [make "dup.ok "true throw "master] ~
   :dup.ok 0 "yes []
setbutton list :buttonx+30 210 [25 25] [make "dup.ok "false throw "master] ~
   (not :dup.ok) 0 "no []
] [
setbutton [70 210] [25 25] [make "dup.ok "true throw "master] ~
   :dup.ok 0 "yes []
setbutton [100 210] [25 25] [make "dup.ok "false throw "master] ~
   (not :dup.ok) 0 "no []
]
end

to numsquares :xcor :num :last
if :num > :last [stop]
setbutton (list :xcor 210) [25 25] `[make "numsquares ,:num throw "master] ~
   (:num = :numsquares) 0 :num []
numsquares :xcor+30 :num+1 :last
end

to colorchart :num :names :colors :ycor
if :num = 0 [stop]
setbutton (list -245 :ycor) [25 25] ~
  `[putguess ",[first :names] ,[first :colors]] "false ~
  (first :colors) [] (first :names)
colorchart :num-1 bf :names bf :colors :ycor-30
end

to numchart :num :ycor
if :num > :numsquares [stop]
setbutton (list -245 :ycor) [25 25] ~
  `[if not :winloop [getnum ,:num]] "false 0 :num :num
numchart :num+1 :ycor-30
end

to pos0 :pos
make "controlpos :pos
output :pos
end

to nxt :height
make "controlpos list (first :controlpos) ((last :controlpos) - (:height + 5))
output :controlpos
end

to move :start
; Move the turtle to the given coordinates
; relative to the lower left corner of the first empty square
; in the current frame.
; Depends on :COLUMN (0 or 1 for >14 guesses), :NUMGUESSES, and :NUMCOLORS
; Note, since :NUMGUESSES starts at 1,
; first frame is at [-180 170] not [-180 200].
pu
setpos (list (-145 + (first :start) + 210*:column + 25*(:numcolors-1))
	     (200 + (last :start) - 30*(:numguesses - 14*:column)))
pd
end 

; -----------------------------------------------

to newguess
; Called from MASTER for first guess frame,
; then from GUESS for later guess frames (my secret),
; or from DOGUESS (user's secret).
make "numguesses :numguesses+1
if :numguesses > 14 [make "column 1]
make "numcolors 1
move [0 0]
drawframe
make "guess "
end

to drawframe
setpc 7 seth 0
repeat :numsquares [square 25 rt 90 fd 25 lt 90]
end

to square :side
repeat 4 [fd :side rt 90]
end

;;;;; =================== USER INTERFACE (READING) =======================

to waitforclick
action.off
; Wait for any key or mouse click, then return, ignoring which/where.
if buttonp [while [buttonp] []  stop]
if keyp [ignore rc  stop]
waitforclick
end

; ----------- Procedures to carry out user commands ---------------

to getnum :num [:cursor cursor]
; Called for digit key or mouse click on digit button.
make ifelse :exact ["guess.exact] ["guess.inexact] :num
move list ifelse :exact [15] [35] 12
setpc 0 filled 0 [repeat 4 [fd 20 rt 90]]
setpc 7 label :num
; type :num setcursor :cursor
make "gotnum "true
end

to putguess :colorletter :colornumber
; Called from mouse click in color palette;
;   first input is a letter for :GUESS (e.g. R for red),
;   second input is a Logo color number for SETPC (e.g. 4 for red).
if :numcolors < 1 [stop]
if :numcolors > :numsquares [stop]
if not :dup.ok [if member? :colorletter :guess [stop]]
make "guess word :guess :colorletter
move [0 0]
filled :colornumber [repeat 4 [fd 25 rt 90]]
make "numcolors :numcolors+1
end

to clear
; Called by clicking ERASE button
if :numcolors < 2 [stop]
make "guess butlast :guess
make "numcolors :numcolors-1
move [0 0]
filled 0 [repeat 4 [fd 25 rt 90]]
end

to guess
; Called by clicking GUESS button.
if not :mysecret [if :gotnum [ct wait 0 throw "ready] stop]
if not (:numcolors > :numsquares) [stop]
ifelse equal? :guess :secret [
    move [15 12]
    setpc 7 label "WIN!
    print (sentence [You win in] :numguesses "turns.)
] [
    move [15 12]
    setpc 7 label exact :secret :guess
    move [35 12]
    setpc 7 label inexact :secret :guess
    newguess
]
end
