aUCBLogo Demos and Tests / minesweeper
; Minesweeper game by Brian Harvey
; 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).
load "buttons.lg
to minesweeper
allFullScreen
(splitScreen 0.9)
; disableLineSmooth
localmake "inference "false
localmake "newnmines 100
localmake "newsize 20
localmake "halfsize 10
localmake "squaresize 2*:halfsize
localmake "maxxmax :halfsize*20
localmake "maxymax :halfsize*20
localmake "sizechoice 3
localmake "hardchoice 2
localmake "sizes [10 15 20]
localmake "hardness [18 21 28]
localmake "windows ifelse LogoVersion.3 =="Windows [16] [0]
catch "quit
[ forever
[ catch "newgame
[ setup :newnmines :newsize :newsize
]
]
]
clearScreen
clearText
setPenColor 0
setScreenColor 7
showTurtle
notFullScreen
splitScreen
end
; ------------------------ Initial setup ---------------------------------
to setup :nmines :rows :columns
cs ct wait 0 ht
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.
setsc 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-50-2*:windows :maxymax+14 setpc 7
seth 90 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-30 :maxymax+10) [90 20] [throw "newgame] ~
"false 0 "NEW GAME "G
setbutton (list 80-:maxxmax :maxymax+10) [40 20] [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 repeat 4 [fd :squaresize-4 rt 90]
setfc 13 ; purple for undetected mine
fillRect [0 0] list squaresize-8 squaresize-8
pu
; setxy xcor+3 ycor+3 fill
][
setpc 11 ; grey in aqua border for empty
pu setxy xcor+1 ycor+1
pd repeat 4 [fd :squaresize-2 rt 90]
setfc 15
fillRect [0 0] list squaresize-4 squaresize-4
pu
; setxy xcor+3 ycor+3 fill
]
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 (((first bf :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 repeat 4 [fd :squaresize-2 rt 90]
pu
; setxy xcor+1 ycor+1
setfc 15 ; grey fill
fillRect [0 0] list squaresize-2 squaresize-2
; fill
setpensize [2 2] seth 0 pu
setxy xcor+squaresize/3.4 ycor+squaresize/8
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
setfc 2 ; Green background
pu
; setxy xcor+6 ycor+3 fill
fillRect [0 0] list squaresize-2 squaresize-2
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
setfc 2 ; First make it all green
pu
; setxy xcor+6 ycor+3 fill
fillRect [0 0] list squaresize-2 squaresize-2
setpc 9 ; Square was flagged, unflag (hide) it.
; fill
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 repeat 4 [fd 5 rt 90]
pu
; setxy xcor+3 ycor+3 fill
fillRect [0 0] list squaresize-2 squaresize-2
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
[ action_once
setpc 2
fill
wait 2
action_once
setpc 0
fill
wait 2
]
end
to lose
make "playing "false
setpc 13 ; Show which mine was hit
pu setxy xcor+3 ycor+3 fill
setpc 6 ; Yellow square on purple
setpensize [3 3]
pd repeat 4 [fd :squaresize-6 rt 90]
setpensize [1 1]
make "nhidden 0
print [You lose!!!!]
pu setxy :xmax+3 0 ; flash screen red
pd
repeat 5
[ action_once
setfc 4
fill
wait 2
action_once
setfc 0
fill
wait 2
]
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