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 ht
   
localmake "mines []   ; list of [row col] lists
   
localmake "statuses (array :rows+-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 [-:rows] [setitem :i :statuses (array :columns+-1)]
   
putmines :nmines   ; Choose mined squares randomly.
   
setsc 0
   
setup_buttons
   
make "nhidden :rows :columns
   borderrow 
-: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 == [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 "NEW GAME "G
   
setbutton (list 80-:maxxmax :maxymax+10) [40 20] [throw "quit] ~
           
"false "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 " []
           
; 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 "Y "Y
   
rebutton (list 230-:maxxmax :maxymax+10) [20 20] ~
          
[make "inference "false  drawinferbuttons] ~
          
not :inference "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 setxy :maxxmax-30 :maxymax+14
                  
invoke word "draw int :prevmines/100 7]
   
if :prevmines 9  [pu seth setxy :maxxmax-19 :maxymax+14
                  
invoke word "draw int (remainder :prevmines 100)/10 7]
   
if :prevmines > -[pu seth setxy :maxxmax-:maxymax+14
                  
invoke word "draw remainder :prevmines 10 7]
   
penpaint
   
if :nmines 99 [pu seth setxy :maxxmax-30 :maxymax+14
                
invoke word "draw int :nmines/100 7]
   
if :nmines 9  [pu seth setxy :maxxmax-19 :maxymax+14
                
invoke word "draw int (remainder :nmines 100)/10 7]
   
if :nmines > -[pu seth setxy :maxxmax-:maxymax+14
                
invoke word "draw remainder :nmines 10 7]
   
make "prevmines :nmines
   
pu seth 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 [stop]
   
borderrow :row :col-1
end

to bordersides :row
; Mark squares -1 and :columns on all rows as border
   
if :row [stop]
   
setstatus :row -"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 [-:ymax :ymax :squaresize] 
   
[   pu setxy -:xmax :y pd setxy :xmax :y
   
]
   
for [-:xmax :xmax :squaresize] 
   
[   pu setxy :x -:ymax pd setxy :x :ymax
   
]
   
pu
   
turtlerows :rows-1
end

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

to turtlerow :col
   
if :col [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 1setpc 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+ycor+1
      
pd repeat [fd :squaresize-rt 90]
      
pu setxy xcor+ycor+1
      
pd repeat [fd :squaresize-rt 90]
      
setfc 13         ; purple for undetected mine
      
fillRect [0 0list squaresize-squaresize-8
      
pu 
;      setxy xcor+3 ycor+3 fill
   
][
      
setpc 11         ; grey in aqua border for empty
      
pu setxy xcor+ycor+1
      
pd repeat [fd :squaresize-rt 90]
      
setfc 15
      
fillRect [0 0list squaresize-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==[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) > [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 == [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+ycor+1
      
pd repeat [fd :squaresize-rt 90]
      
pu 
;      setxy xcor+1 ycor+1
      
setfc 15            ; grey fill
      
fillRect [0 0list squaresize-squaresize-2
;      fill
      
setpensize [2 2seth pu
      setxy xcor
+squaresize/3.4 ycor+squaresize/8
      
if :near>word "draw :near   ; Run procedure to draw digit
      
pu setpensize [1 1seth 0
      
make "nhidden :nhidden 1   ; Keep track of number of squares still hidden.
      
if :nhidden == [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
-:col
   hitfriendsrow :row :col
   hitfriendsrow :row
+: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-:col) + (nrow :row :col) + (nrow :row+: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 == [stop]      ; Game is over, no action allowed.
   
localmake "flagged status :row :col
   
ifelse :flagged == "hidden [   ; Square was hidden, so flag it.
      
if :nmines == [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 0list squaresize-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 0list squaresize-squaresize-2
      
setpc 9         ; Square was flagged, unflag (hide) it.
;      fill
      
make "nmines :nmines 1
      
make "nhidden :nhidden 1
   
]
   
if :nhidden == [win]         ; Flagged last mine, so win.
end

to drawflag
   
setpc 13         ; purple for flag
   
setpensize [2 2]
   
pd fd repeat [fd rt 90]
   
pu 
;   setxy xcor+3 ycor+3 fill
   
fillRect [0 0list squaresize-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+ycor+fill
   setpc 
6                  ; Yellow square on purple
   
setpensize [3 3]
   
pd repeat [fd :squaresize-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 bk 3
   
lt 90 fd 12
   
lt 90+45 fd 4
end

to draw2 [:color 13]
   
setpc :color      ; purple
   
pu setxy xcor-ycor+2
   
pd rt 90 fd bk 6
   
lt 45 fd 8
   
rt 45 pu bk 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 pd arc -130 3
end

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

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

to draw6 [:color 12]
   
setpc :color      ; salmon
   
pu fd rt 90 fd 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 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 rt 90   ; like 6 but upside down
   
pu fd rt 90 fd pd
   
repeat 270 [fd 0.07 rt 1]
   
repeat 45 [fd 0.3 rt 2]
end

to draw0 [:color 7]
   
setpc :color
   
pu fd 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