; 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