summaryrefslogtreecommitdiff
path: root/ext/tk/sample/demos-en/knightstour.rb
blob: 1ad82fcc9d7094fee03a348bae7a250674fda1ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# frozen_string_literal: false
# Based on the widget demo of Tcl/Tk8.5.2
# The following is the original copyright text.
#----------------------------------------------------------------------------
# Copyright (C) 2008 Pat Thoyts <patthoyts@users.sourceforge.net>
#
#	Calculate a Knight's tour of a chessboard.
#
#	This uses Warnsdorff's rule to calculate the next square each
#	time. This specifies that the next square should be the one that
#	has the least number of available moves.
#
#	Using this rule it is possible to get to a position where
#	there are no squares available to move into. In this implementation
#	this occurs when the starting square is d6.
#
#	To solve this fault an enhancement to the rule is that if we
#	have a choice of squares with an equal score, we should choose
#	the one nearest the edge of the board.
#
#	If the call to the Edgemost function is commented out you can see
#	this occur.
#
#	You can drag the knight to a specific square to start if you wish.
#	If you let it repeat then it will choose random start positions
#	for each new tour.
#----------------------------------------------------------------------------
require 'tk'

class Knights_Tour
  # Return a list of accessible squares from a given square
  def valid_moves(square)
    moves = []
    [
      [-1,-2], [-2,-1], [-2,1], [-1,2], [1,2], [2,1], [2,-1], [1,-2]
    ].each{|col_delta, row_delta|
      col = (square % 8) + col_delta
      row = (square.div(8)) + row_delta
      moves << (row * 8 + col) if row > -1 && row < 8 && col > -1 && col < 8
    }
    moves
  end

  # Return the number of available moves for this square
  def check_square(square)
    valid_moves(square).find_all{|pos| ! @visited.include?(pos)}.length
  end

  # Select the next square to move to. Returns -1 if there are no available
  # squares remaining that we can move to.
  def next_square(square)
    minimum = 9
    nxt = -1
    valid_moves(square).each{|pos|
      unless @visited.include?(pos)
        cnt = check_square(pos)
        if cnt < minimum
          minimum = cnt
          nxt = pos
        elsif cnt == minimum
          nxt = edgemost(nxt, pos)
        end
      end
    }
    nxt
  end

  # Select the square nearest the edge of the board
  def edgemost(nxt, pos)
    col_A = 3 - ((3.5 - nxt % 8).abs.to_i)
    col_B = 3 - ((3.5 - pos % 8).abs.to_i)
    row_A = 3 - ((3.5 - nxt.div(8)).abs.to_i)
    row_B = 3 - ((3.5 - pos.div(8)).abs.to_i)
    (col_A * row_A < col_B * row_B)? nxt : pos
  end

  # Display a square number as a standard chess square notation.
  def _N(square)
    '%c%d' % [(97 + square % 8), (square.div(8) + 1)]
  end

  # Perform a Knight's move and schedule the next move.
  def move_piece(last, square)
    @log.insert(:end, "#{@visited.length}. #{_N last} -> #{_N square}\n", '')
    @log.see(:end)
    @board.itemconfigure(1+last, :state=>:normal, :outline=>'black')
    @board.itemconfigure(1+square, :state=>:normal, :outline=>'red')
    @knight.coords(@board.coords(1+square)[0..1])
    @visited << square
    if (nxt = next_square(square)) != -1
      @after_id = Tk.after(@delay.numeric){move_piece(square, nxt) rescue nil}
    else
      @start_btn.state :normal
      if @visited.length == 64
        if @initial == square
          @log.insert :end, 'Closed tour!'
        else
          @log.insert :end, "Success\n", {}
          Tk.after(@delay.numeric * 2){tour(rand(64))} if @continuous.bool
        end
      else
        @log.insert :end, "FAILED!\n", {}
      end
    end
  end

  # Begin a new tour of the board given a random start position
  def tour(square = nil)
    @visited.clear
    @log.clear
    @start_btn.state :disabled
    1.upto(64){|n|
      @board.itemconfigure(n, :state=>:disabled, :outline=>'black')
    }
    unless square
      square = @board.find_closest(*(@knight.coords << 0 << 65))[0].to_i - 1
    end
    @initial = square
    Tk.after_idle{ move_piece(@initial, @initial) rescue nil }
  end

  def _stop
    Tk.after_cancel(@after_id) rescue nil
  end

  def _exit
    _stop
    $knightstour.destroy
  end

  def set_delay(new)
    @delay.numeric = new.to_i
  end

  def drag_start(w, x, y)
    w.dtag('selected')
    w.addtag('selected', :withtag, 'current')
    @dragging = [x, y]
  end

  def drag_motion(w, x, y)
    return unless @dragging
    w.move('selected', x - @dragging[0], y - @dragging[1])
    @dragging = [x, y]
  end

  def drag_end(w, x, y)
    square = w.find_closest(x, y, 0, 65)
    w.coords('selected', w.coords(square)[0..1])
    w.dtag('selected')
    @dragging = nil
  end

  def make_SeeDismiss
    ## See Code / Dismiss
    frame = Ttk::Frame.new($knightstour)
    sep = Ttk::Separator.new(frame)
    Tk.grid(sep, :columnspan=>4, :row=>0, :sticky=>'ew', :pady=>2)
    TkGrid('x',
           Ttk::Button.new(frame, :text=>'See Code',
                           :image=>$image['view'], :compound=>:left,
                           :command=>proc{showCode 'knightstour'}),
           Ttk::Button.new(frame, :text=>'Dismiss',
                           :image=>$image['delete'], :compound=>:left,
                           :command=>proc{
                             $knightstour.destroy
                             $knightstour = nil
                           }),
           :padx=>4, :pady=>4)
    frame.grid_columnconfigure(0, :weight=>1)
    frame
  end

  def create_gui(parent = nil)
    $knightstour.destroy rescue nil
    $knightstour = Tk::Toplevel.new(parent, :title=>"Knight's tour")
    $knightstour.withdraw
    base_f = Ttk::Frame.new($knightstour)
    @board = Tk::Canvas.new(base_f, :width=>240, :height=>240)
    @log = Tk::Text.new(base_f, :width=>12, :height=>1,
                        :font=>'Arial 8', :background=>'white')
    scr = @log.yscrollbar(Ttk::Scrollbar.new(base_f))

    @visited = []
    @delay = TkVariable.new(600)
    @continuous = TkVariable.new(false)

    tool_f = Ttk::Frame.new($knightstour)
    label = Ttk::Label.new(tool_f, :text=>'Speed')
    scale = Ttk::Scale.new(tool_f, :from=>8, :to=>2000, :variable=>@delay,
                           :command=>proc{|n| set_delay(n)})
    check = Ttk::Checkbutton.new(tool_f, :text=>'Repeat',
                                 :variable=>@continuous)
    @start_btn = Ttk::Button.new(tool_f, :text=>'Start',
                                 :command=>proc{tour()})
    @exit_btn = Ttk::Button.new(tool_f, :text=>'Exit',
                                :command=>proc{_exit()})

    7.downto(0){|row|
      0.upto(7){|col|
        if ((col & 1) ^ (row & 1)).zero?
          fill  = 'bisque'
          dfill = 'bisque3'
        else
          fill  = 'tan3'
          dfill = 'tan4'
        end
        coords = [col * 30 + 4, row * 30 + 4, col * 30 + 30, row * 30 + 30]
        @board.create(TkcRectangle, coords,
                      :fill=>fill, :disabledfill=>dfill,
                      :width=>2, :state=>:disabled)
      }
    }

    @knight_font = TkFont.new(:size=>-24)
    @knight = TkcText.new(@board, 0, 0, :font=>@knight_font,
                          :text=>Tk::UTF8_String.new('\u265e'),
                          :anchor=>'nw', # :tags=>'knight',
                          :fill=>'black', :activefill=>'#600000')
    @knight.coords(@board.coords(rand(64)+1)[0..1])
    @knight.bind('ButtonPress-1', '%W %x %y'){|w,x,y| drag_start(w,x,y)}
    @knight.bind('Motion', '%W %x %y'){|w,x,y| drag_motion(w,x,y)}
    @knight.bind('ButtonRelease-1', '%W %x %y'){|w,x,y| drag_end(w,x,y)}

    Tk.grid(@board, @log, scr, :sticky=>'news')
    base_f.grid_rowconfigure(0, :weight=>1)
    base_f.grid_columnconfigure(0, :weight=>1)

    Tk.grid(base_f, '-', '-', '-', '-', '-', :sticky=>'news')
    widgets = [label, scale, check, @start_btn]
    sg = nil
    unless $RubyTk_WidgetDemo
      widgets << @exit_btn
      if Tk.windowingsystem != 'aqua'
        #widgets.unshift(Ttk::SizeGrip.new(tool_f))
        Ttk::SizeGrip.new(tool_f).pack(:side=>:right, :anchor=>'se')
      end
    end
    Tk.pack(widgets, :side=>:right)
    if Tk.windowingsystem == 'aqua'
      TkPack.configure(widgets, :padx=>[4, 4], :pady=>[12, 12])
      TkPack.configure(widgets[0], :padx=>[4, 24])
      TkPack.configure(widgets[-1], :padx=>[16, 4])
    end

    Tk.grid(tool_f, '-', '-', '-', '-', '-', :sticky=>'ew')

    if $RubyTk_WidgetDemo
      Tk.grid(make_SeeDismiss(), '-', '-', '-', '-', '-', :sticky=>'ew')
    end

    $knightstour.grid_rowconfigure(0, :weight=>1)
    $knightstour.grid_columnconfigure(0, :weight=>1)

    $knightstour.bind('Control-F2'){TkConsole.show}
    $knightstour.bind('Return'){@start_btn.invoke}
    $knightstour.bind('Escape'){@exit_btn.invoke}
    $knightstour.bind('Destroy'){ _stop }
    $knightstour.protocol('WM_DELETE_WINDOW'){ _exit }

    $knightstour.deiconify
    $knightstour.tkwait_destroy
  end

  def initialize(parent = nil)
    create_gui(parent)
  end
end

Tk.root.withdraw unless $RubyTk_WidgetDemo
Thread.new{Tk.mainloop} if __FILE__ == $0
Knights_Tour.new