;  Minesweeper game

;  Mouse clicks call HIT procedure

;  Main data structure: array of arrays, e.g.,
;      (ITEM (ITEM STATUS 3) 5) for row 3 column 5.
;  Values are: HIDDEN (tan square),
;              FLAGGED (flagged as a mine),
;              SHOWN (open, non-mine, shows number of mined neighbors),
;              BORDER (just outside of actual playing field).
;  Notice that a FLAGGED square might not really be a mine.
;  (Player can make mistakes!)
;  Actual mines are listed in MINES (list of [row col] lists).

cslsload "buttons

to mines
if :LogoPlatform = "Windows [maximize.window "true]
localmake "inference "false
localmake "newnmines 100
localmake "newsize 15
localmake "halfsize 10
localmake "squaresize 2*:halfsize
localmake "maxxmax :halfsize*15*2
localmake "maxymax :halfsize*15
localmake "sizechoice 3
localmake "hardchoice 2
localmake "sizes [5 10 15]
localmake "hardness [38.1 44.45 59.26]
localmake "windows ifelse :LogoPlatform="Windows [16] [0]
if :LogoPlatform="wxWidgets [make "windows 16]
norefresh
catch "quit [forever [catch "newgame [setup :newnmines :newsize :newsize*2]]]
refresh
cs ct setpc 7 st
end

; ------------------------ Initial setup ---------------------------------

to setup :nmines :rows :columns
cs ct wait 0 ht fs
localmake "mines []	; list of [row col] lists
localmake "statuses (array :rows+2 -1)	; status of each square
localmake "xmax :halfsize*:columns
localmake "ymax :halfsize*:rows
localmake "opening "true
localmake "playing "true	; false only at end of game
for [i -1 :rows] [setitem :i :statuses (array :columns+2 -1)]
putmines :nmines	; Choose mined squares randomly.
setbg 0
setup.buttons
make "nhidden :rows * :columns
borderrow -1 :columns
borderrow :rows :columns
    ; mark nonexistent squares just outside field as BORDER
    ; to simplify how-many-of-my-neighbors computations
bordersides :rows-1
hint		; open some safe squares so we don't have to guess blindly
localmake "prevmines -1
pu setxy :maxxmax-100-2*:windows :maxymax+14 setpc 7
label [Mines left:]
showminesleft
action.loop
end

to putmines :num
; Choose random square, and make it a mine unless it already was one.
if :num = 0 [stop]
localmake "row random :rows
localmake "col random :columns
if member? (list :row :col) :mines [putmines :num  stop]
make "mines fput (list :row :col) :mines
putmines :num-1
end

to setup.buttons
init.buttons
setbutton (list -:maxxmax :maxymax+10) [60 20] [throw "newgame] ~
          "false 0 "|NEW GAME| "G
setbutton (list 80-:maxxmax :maxymax+10) [40 20] [action.off throw "quit] ~
          "false 0 "QUIT "Q
caption (list 160-:maxxmax-:windows :maxymax+10) [60 20] "infer:
drawinferbuttons
caption (list -:maxxmax -(:maxymax+30)) [60 20] "size:
caption (list 160-:maxxmax-:windows -(:maxymax+30)) [80 20] "difficulty:
drawsizebuttons
setbutton (list -:xmax -:ymax) (list 2*:xmax 2*:ymax) ~
          [hit :mousepos  showminesleft] "false 9 "|| []
          ; Entire board is one big button.
showboard "false   ; input is TRUE to uncover entire board at end of game 
pu
end

to drawinferbuttons
rebutton (list 200-:maxxmax :maxymax+10) [20 20] ~
         [make "inference "true  drawinferbuttons] ~
         :inference 0 "Y "Y
rebutton (list 230-:maxxmax :maxymax+10) [20 20] ~
         [make "inference "false  drawinferbuttons] ~
         not :inference 0 "N "N
end

to drawsizebuttons
make "newsize item :sizechoice :sizes
make "newnmines int (item :hardchoice :hardness)*:newsize*:newsize/100
rebutton (list 40-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "sizechoice 1  drawsizebuttons] ~
         :sizechoice=1 0 "S "S
rebutton (list 70-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "sizechoice 2  drawsizebuttons] ~
         :sizechoice=2 0 "M "M
rebutton (list 100-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "sizechoice 3  drawsizebuttons] ~
         :sizechoice=3 0 "L "L
rebutton (list 230-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "hardchoice 1  drawsizebuttons] ~
         :hardchoice=1 0 "1 "1
rebutton (list 260-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "hardchoice 2  drawsizebuttons] ~
         :hardchoice=2 0 "2 "2
rebutton (list 290-:maxxmax -(:maxymax+30)) [20 20] ~
         [make "hardchoice 3  drawsizebuttons] ~
         :hardchoice=3 0 "3 "3
end

to showminesleft
if :prevmines=:nmines [stop]
setpensize [2 2]
penerase
if :prevmines > 99 [pu seth 0 setxy :maxxmax-30 :maxymax+14
                    invoke word "draw int :prevmines/100 7]
if :prevmines > 9  [pu seth 0 setxy :maxxmax-19 :maxymax+14
                    invoke word "draw int (remainder :prevmines 100)/10 7]
if :prevmines > -1 [pu seth 0 setxy :maxxmax-8 :maxymax+14
                    invoke word "draw remainder :prevmines 10 7]
penpaint
if :nmines > 99 [pu seth 0 setxy :maxxmax-30 :maxymax+14
                 invoke word "draw int :nmines/100 7]
if :nmines > 9  [pu seth 0 setxy :maxxmax-19 :maxymax+14
                 invoke word "draw int (remainder :nmines 100)/10 7]
if :nmines > -1 [pu seth 0 setxy :maxxmax-8 :maxymax+14
                 invoke word "draw remainder :nmines 10 7]
make "prevmines :nmines
pu seth 0 setpensize [1 1]
end

; --------------------------- Mark border squares -------------------------

to borderrow :row :col
; Mark all squares on this row (including -1 and :columns) as border
setstatus :row :col "border
if :col < 0 [stop]
borderrow :row :col-1
end

to bordersides :row
; Mark squares -1 and :columns on all rows as border
if :row < 0 [stop]
setstatus :row -1 "border
setstatus :row :columns "border
bordersides :row-1
end

; ---------------------- Initial and final display of entire board --------

to showboard :over
; Input is FALSE during setup, TRUE when a mine is uncovered and game ends.
setpc 7
for [y -:ymax :ymax :squaresize] [pu setxy -:xmax :y pd setxy :xmax :y]
for [x -:xmax :xmax :squaresize] [pu setxy :x -:ymax pd setxy :x :ymax]
pu
turtlerows :rows-1
end

to turtlerows :row
if :row < 0 [stop]
turtlerow :columns-1
turtlerows :row-1
end

to turtlerow :col
if :col < 0 [stop]
ifelse :over [		; game over, only hidden squares need be displayed
  if "hidden = status :row :col [onesquare]
  if and ("flagged = status :row :col)
         (not member? (list :row :col) :mines) [
			; but indicate mistakenly flagged ones
     setx (:col*:squaresize)-:xmax
     sety (:row*:squaresize)-:ymax
     setpensize [3 3]
     setpc 4		; draw red X over mine symbol
     pd setxy xcor+:squaresize ycor+:squaresize
     pu setx xcor-:squaresize
     pd setxy xcor+:squaresize ycor-:squaresize
     pu setpensize [1 1] setpc 7
  ]
] [			; game starting, mark each square as hidden
  setstatus :row :col "hidden
 ]
turtlerow :col - 1
end

to onesquare
action.once
setx (:col*:squaresize)-:xmax
sety (:row*:squaresize)-:ymax
ifelse member? (list :row :col) :mines [
  setpc 2			; thick green border
  pu setxy xcor+1 ycor+1
  pd repeat 4 [fd :squaresize-2 rt 90]
  pu setxy xcor+1 ycor+1
  pd filled 13 [repeat 4 [fd :squaresize-4 rt 90]]
] [
  setpc 11			; grey in aqua border for empty
  pu setxy xcor+1 ycor+1
  pd filled 15 [repeat 4 [fd :squaresize-2 rt 90]]
]
end

; ---------------- Start game by uncovering a few safe squares --------------

to hint [:tries 30] [:inference "true]
if :tries=0 [stop]		; limit number of attempts
localmake "ohidden :nhidden
localmake "ry random :rows
localmake "rx random :columns
if and equalp status :ry :rx "hidden not member? (list :ry :rx) :mines [
  catch "error [hitsquare :ry :rx]
  if (:ohidden - :nhidden) > 5 [stop]
	; stop when at least 5 neighbors were opened
]
(hint :tries-1)
end

; -------- Main player activity, mouse clicks on minefield squares -----

to hit :pos
; Convert mouse (pixel) coordinate to column and row numbers
; (square is :squaresize x :squaresize pixels)
if not :playing [stop]
localmake "opening equalp :button 1	; true to open, false to flag
catch "error [hitsquare (int (((last :pos) + :ymax) / :squaresize))
			(int (((first :pos) + :xmax) / :squaresize))]
end

to hitsquare :row :col
; Called on player mouse click and automatic opening of neighbors
; when infering.
if :nhidden = 0 [stop]		; No hidden squares, game is over.
if (or (:row<0) (:row>=:rows) (:col<0) (:col>=:columns)) [stop]
penup
setx (:col*:squaresize)-:xmax	; Move turtle over chosen square
sety (:row*:squaresize)-:ymax
localmake "status status :row :col
localmake "near neighbors :row :col "minecheck
if not equal? :status "shown [	; Clicking on hidden or flagged square.
  if not :opening [showflag stop]	; FLAG mode.  (Below is OPEN mode.)
  if :status = "flagged [showflag stop]	; Can't open flagged square.
  setstatus :row :col "shown		; This square is now shown.
  if member? (list :row :col) :mines [lose stop]	; Oops!
  setpc 11				; aqua border
  pu setxy xcor+1 ycor+1
  pd filled 15 [repeat 4 [fd :squaresize-2 rt 90]]
  setpensize [2 2] seth 0 pu
  setxy xcor+6 ycor+3
  if :near>0 word "draw :near	; Run procedure to draw digit
  pu setpensize [1 1] seth 0
  make "nhidden :nhidden - 1	; Keep track of number of squares still hidden.
  if :nhidden = 0 [win stop]	; If all squares shown or flagged, we win!
  if and (not equal? :near 0) (not :inference) [stop]
	; Finished if no automatic inference
]
	; Automatic inference section:
localmake "hnear neighbors :row :col "hiddencheck
localmake "fnear neighbors :row :col "flaggedcheck
ifelse :fnear = :near [		; If number of neighboring mines equals
  localmake "opening "true	;   number of flagged neighbors,
  hitfriends :row :col		;   all hidden neighbors must be safe
			        ;   (unless player has flagged wrongly!)
] [
  if (:hnear + :fnear) = :near [ ; If number of neighboring mines equals
    localmake "opening "false 	 ;  number of non-shown (hidden or flagged)
    hitfriends :row :col	 ;  neighbors, all hidden neighbors must be
			         ;  mines.
  ]
]
end

; --------------- Automatic inference to speed up game ------------------

; Either OPEN or FLAG all eight immediate neighbors unless already open or
; flagged. Note mutual recursion: HITSQUARE calls HITFRIENDS to do inference;
; HITFRIENDS calls HITSQUARE for each inferred opening/flagging.

to hitfriends :row :col
hitfriendsrow :row-1 :col
hitfriendsrow :row :col
hitfriendsrow :row+1 :col
end

to hitfriendsrow :row :col
hitfriend :row :col-1
hitfriend :row :col
hitfriend :row :col+1
end

to hitfriend :row :col
if "hidden = status :row :col [hitsquare :row :col]
end

; Here's where we count the neighbors that have some property
; (being truly mined, being flagged as mined, or being hidden).
; Third input is a procedure that takes ROW and COL inputs,
;   returning 1 if property is satisfied, 0 if not.

to neighbors :row :col :check
output (nrow :row-1 :col) + (nrow :row :col) + (nrow :row+1 :col)
end

to nrow :row :col
output (invoke :check :row :col-1) + (invoke :check :row :col) + ~
       (invoke :check :row :col+1)
end

; Here are the three property-checking procedures.

to minecheck :row :col
output ifelse member? (list :row :col) :mines [1] [0]
end

to hiddencheck :row :col
output ifelse "hidden = status :row :col [1] [0]
end

to flaggedcheck :row :col
output ifelse "flagged = status :row :col [1] [0]
end

; --------------------- Flag mode (user says where mines are) --------------

to showflag
if :nhidden = 0 [stop]		; Game is over, no action allowed.
localmake "flagged status :row :col
ifelse :flagged = "hidden [	; Square was hidden, so flag it.
    if :nmines = 0 [stop]	; But don't allow more flags than actual mines.
    setstatus :row :col "flagged
    setpc 7
    filled 2 [repeat 4 [fd :squaresize rt 90]]
    pu setxy xcor+6 ycor+3
    drawflag			; with purple flag
    make "nmines :nmines-1
    make "nhidden :nhidden-1
] [
    if not equal? :flagged "flagged [stop] ; Square was shown, can't flag it.
    setstatus :row :col "hidden
    setpc 7
    filled 9 [repeat 4 [fd :squaresize rt 90]]
    make "nmines :nmines + 1
    make "nhidden :nhidden + 1
]
if :nhidden = 0 [win]			; Flagged last mine, so win.
end

to drawflag
setpc 13			; purple for flag
setpensize [2 2]
pd fd 5 filled 13 [repeat 4 [fd 5 rt 90]]
end

; ------------ Notify user when game is won or lost -------------------

to win
make "playing "false
make "nhidden 0
; print [You win!!!!]
pu setxy :xmax+3 0	; flash screen green
repeat 5 [setpc 2 fill wait 0 action.once setpc 0 fill wait 0]
end

to lose
make "playing "false
setpc 6						; Yellow square on purple
setpensize [3 3]
pu setxy xcor+3 ycor+3 pd
filled 13 [repeat 4 [fd :squaresize-6 rt 90]]	; Show which mine was hit
setpensize [1 1]
make "nhidden 0
; print [You lose!!!!]
pu setxy :xmax+3 0	; flash screen red
repeat 5 [setpc 4 fill wait 0 action.once setpc 0 fill wait 0]
showboard "true
end

; --------------- data abstraction for statuses array -------------

to status :row :col
output item :col (item :row :statuses)
end

to setstatus :row :col :value
setitem :col (item :row :statuses) :value
end


; -------------------- draw digits ----------------------

to draw1 [:color 4]
setpc :color		; red
pd rt 90 fd 6 bk 3
lt 90 fd 12
lt 90+45 fd 4
end

to draw2 [:color 13]
setpc :color		; purple
pu setxy xcor-1 ycor+2
pd rt 90 fd 6 bk 6
lt 45 fd 8
rt 45 pu bk 3 pd arc -180 3
end

to draw3 [:color 0]
setpc :color		; black
pu fd 12 rt 90
pd fd 6
rt 90+45 fd 7
pu lt 45 fd 3 pd arc -130 3
end

to draw4 [:color 8]
setpc :color		; brown
pu fd 6 
pd fd 6 bk 6
rt 90 fd 6 bk 3
lt 90 fd 6 bk 12
end

to draw5 [:color 10]
setpc :color		; forest
pu fd 12
pd rt 90 fd 6 bk 6
rt 90 fd 5
pu fd 3 pd arc -180 3
end

to draw6 [:color 12]
setpc :color		; salmon
pu fd 7 rt 90 fd 1 pd
repeat 270 [fd 0.07 rt 1]
repeat 45 [fd 0.3 rt 2]
end

to draw7 [:color 1]
setpc :color		; blue
pu fd 11 rt 90
pd fd 6
rt 90+30 fd 9
end

to draw8 [:color 5]
setpc :color		; magenta
pu fd 3 rt 90 fd 2
pd arc 359 3
pu lt 90 fd 6
pd arc 359 3
end

to draw9 [:color 7]
setpc :color
pu fd 12 rt 90 fd 6 rt 90	; like 6 but upside down
pu fd 7 rt 90 fd 1 pd
repeat 270 [fd 0.07 rt 1]
repeat 45 [fd 0.3 rt 2]
end

to draw0 [:color 7]
setpc :color
pu fd 6 pd
repeat 90 [fd (2-repcount/90)*6/150 rt 1]
repeat 90 [fd (1+repcount/90)*6/150 rt 1]
repeat 90 [fd (2-repcount/90)*6/150 rt 1]
repeat 90 [fd (1+repcount/90)*6/150 rt 1]
end
