;;; Connect-the-dots game

to dotgame :size
; Connect-the-dots game.  Input is the number of dots on each side.
if :LogoPlatform = "Windows [maximize.window "true]
ht cs
setpc 7
setpensize [6 6]
localmake "offset (:size-1)*20
pu setpos list -:offset -:offset
board :size
localmake "lines ~
   se (crossmap [list (list ?1 ?2) (list ?1 1+?2)]
                (iseq 0 :size-1) (iseq 0 :size-2)) ~
      (crossmap [list (list ?1 ?2) (list 1+?1 ?2)]
                (iseq 0 :size-2) (iseq 0 :size-1))
localmake "computer 0
localmake "person 0
localmake "numboxes (:size-1)*(:size-1)
localmake "boxlists (array 5 0)
localmake "oldmove []
for [i 1 4] [setitem :i :boxlists []]
setitem 0 :boxlists ~
        (crossmap [list ?1 ?2] (iseq 0 :size-2) (iseq 0 :size-2))
localmake "boxes (array :size-1 0)
for [i 0 :size-2] [setitem :i :boxes (array :size-1 0)]

CATCH "WIN [FOREVER [PERSONMOVE COMMOVE]]	; play the game!

if not emptyp :oldmove [	; make the last move white
  setpc 7
  pu
  setpos map [40*? - :offset] first :oldmove
  pd
  setpos map [40*? - :offset] last :oldmove
]
if computer > :person ~
   [print (se [you lost] :computer "to :person)]
if :computer < :person ~
   [print (se [you won] :person "to :computer)]
if :computer = :person [print (se [tie game])]
setpensize [1 1]
end

; --------------- Initial board display -------------------------

to board :num
repeat :num [dots :num]
end

to dots :num
pd
repeat :num [fd 0 pu rt 90 fd 40 lt 90 pd]
pu lt 90 fd 40 * :num rt 90 fd 40
end

; -------------- Human player's move ---------------------

to personmove
; Read a mouse click, turn it into a move if legal.
localmake "move gmove
if not legal? :move [print [Not a legal move!  Try again.]
                     personmove stop]
drawline :move 6
localmake "direction reverse (map "difference (last :move) (first :move))
localmake "found "false
fillboxes 6 "person
if :found [personmove]
end

to gmove
while [not buttonp] []
while [buttonp] []
output findline (map [? + :offset] mousepos)
end

to findline :pos
; Find the nearest vertical or horizontal line to the mouse click.
localmake "xrem remainder (first :pos)+10 40
localmake "yrem remainder (last :pos)+10 40
localmake "xpos (first :pos)+10-:xrem
localmake "ypos (last :pos)+10-:yrem
if :xrem > :yrem ~
   [output list (list :xpos/40 :ypos/40) (list :xpos/40+1 :ypos/40)]
output list (list :xpos/40 :ypos/40) (list :xpos/40 :ypos/40+1)
end

to legal? :move
; Output true if this is an undrawn line segment connecting two dots.
output memberp :move :lines
end

; ----------------- Computer's move ----------------------

to commove
; The computer chooses a move, does the housekeeping for it.
; Strategy: complete boxes if possible, otherwise pick a move that doesn't
; let the opponent complete a box.
ifelse not emptyp (item 3 :boxlists) [
  localmake "move lastline first (item 3 :boxlists)
] [
  localmake "goodlines filter "lineokay? :lines
  ifelse not emptyp :goodlines [
    localmake "move pick :goodlines
  ] [
    localmake "cohorts []
    makecohorts :lines
    localmake "move lastline first smallest :cohorts
  ]
]
drawline :move 4
localmake "direction reverse (map "difference (last :move) (first :move))
localmake "found "false
fillboxes 4 "computer
if :found [commove]
end

to lineokay? :move
; Output true if this move won't let the opponent complete a box.
localmake "direction reverse (map "difference (last :move) (first :move))
output and (boxokay? first :move) ~
           (boxokay? (map "difference (first :move) :direction))
end

to boxokay? :box
; Output true if this box has fewer than 2 edges already drawn.
if or ((first :box) < 0) ((last :box) < 0) [output "true]
if or ((first :box) > (:size-2)) ((last :box) > (:size-2)) [output "true]
localmake "count item (last :box) item (first :box) :boxes
if emptyp :count [make "count 0]
output :count<2
end

to lastline :box
; Box has three lines drawn; find the missing one for us to draw.
if memberp (list :box (map "sum :box [0 1])) :lines [
  output (list :box (map "sum :box [0 1]))]
if memberp (list :box (map "sum :box [1 0])) :lines [
  output (list :box (map "sum :box [1 0]))]
if memberp (list (map "sum :box [0 1]) (map "sum :box [1 1])) :lines [
  output (list (map "sum :box [0 1]) (map "sum :box [1 1]))]
if memberp (list (map "sum :box [1 0]) (map "sum :box [1 1])) :lines [
  output (list (map "sum :box [1 0]) (map "sum :box [1 1]))]
output []	; box was full already (from makecohort)
end

to makecohorts :lines
; Partition the available boxes into chains, to look for the smallest.
; Note, the partition is not necessarily optimal -- this algorithm needs work.
; It's important that LINES be a local variable here, so that we can "draw"
; lines hypothetically that we're not really going to draw on the board.
while [not emptyp :lines] [
  localmake "cohort []
  makecohort first :lines
  push "cohorts :cohort
]
end

to makecohort :line
; Group all the boxes in a chain that starts with this line.
; Mark the line as drawn (locally to caller), then look in both directions
; for completable boxes.
make "lines remove :line :lines
localmake "direction reverse (map "difference (last :line) (first :line))
makecohort1 (map "difference (first :line) :direction)
makecohort1 first :line
end

to makecohort1 :box
; Examine one of the boxes adjoining the line just hypothetically drawn.
; It has 0, 1, or 2 undrawn sides.  (If 3 or 4, wouldn't have gotten here.)
; 0 sides -> count the box if not already, but no further lines in the chain.
; 1 side -> count the box, continue the chain with its last side.
; 2 sides -> the box isn't ready to complete, so it's not in this chain.
if or ((first :box) < 0) ((last :box) < 0) [stop]
if or ((first :box) > (:size-2)) ((last :box) > (:size-2)) [stop]
localmake "togo filter [memberp (list (map "sum :box first ?)
				      (map "sum :box last ?))
				:lines] ~
		       [[[0 0] [0 1]] [[0 0] [1 0]]
		        [[1 0] [1 1]] [[0 1] [1 1]]]
if (count :togo)=2 [stop]
if not memberp :box :cohort [push "cohort :box]
if emptyp :togo [stop]
localmake "line (list (map "sum :box first first :togo)
                      (map "sum :box last first :togo))
makecohort :line
end

to smallest :cohorts [:sofar []] [:minsize :numboxes+1]
if emptyp :cohorts [output :sofar]
if (count first :cohorts) < :minsize ~
   [output (smallest bf :cohorts first :cohorts count first :cohorts)]
output (smallest bf :cohorts :sofar :minsize)
end

; ----------- Common procedures for person and computer moves --------

to drawline :move :color
; Actually draw the selected move on the screen.
if not emptyp :oldmove [
  setpc 7
  pu
  setpos map [40*? - :offset] first :oldmove
  pd
  setpos map [40*? - :offset] last :oldmove
]
setpc :color
pu
setpos map [40*? - :offset] first :move
pd
setpos map [40*? - :offset] last :move
make "oldmove :move
end

to fillboxes :color :owner
; Implicit inputs (inherited from caller):
;   :move is the move someone just made.
;   :direction is [1 0] for vertical move, [0 1] for horizontal.
; Note that the line is drawn, check the two boxes (maybe) on either side,
; color them and count them for the appropriate player, see if game over.
make "lines remove :move :lines
if boxbefore? :move [fillbox (map "difference (first :move) :direction)]
if boxafter? :move [fillbox first :move]
testwin
end

to boxafter? :move
; Output true if the box above or to the right of the move is now complete.
output (increment first :move)=4
end

to boxbefore? :move
; Output true if the box below or to the left of the move is now complete.
localmake "p3 (map "difference (first :move) :direction)
output (increment :p3)=4
end

to increment :box
; If this isn't a box at all (might be if the move was on a border),
; just output [].  Otherwise, increment the number in the :boxes array,
; and move this box from one of the :boxlists to the next higher one.
; Output the new count of number of lines drawn in this box.
if or ((first :box) < 0) ((last :box) < 0) [output []]
if or ((first :box) > (:size-2)) ((last :box) > (:size-2)) [output []]
localmake "count item (last :box) item (first :box) :boxes
if emptyp :count [make "count 0]
setitem (last :box) item (first :box) :boxes :count+1
setitem :count :boxlists (remove :box item :count :boxlists)
setitem :count+1 :boxlists (fput :box item :count+1 :boxlists)
output :count+1
end

to fillbox :box
; Color in a completed box, increase the box count of its owner, and
; flag that a box was completed.
pu
setpos (map [40*? - :offset] :box)
filled :color [repeat 4 [fd 40 rt 90]]
make :owner (thing :owner)+1
make "found "true
end

; ------------------- Endgame processing --------------------

to testwin
if :computer+:person = :numboxes [throw "win]
end
