kira 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/kira +148 -0
- data/lib/kira.rb +2 -0
- data/lib/kira/group.rb +36 -0
- data/lib/kira/puzzle.rb +106 -0
- data/lib/kira/sudoku.rb +114 -0
- data/lib/kira/version.rb +5 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d54e43948bd3ab61423f7b279dbc075713d915e06760394bf235504f31f7a8bd
|
|
4
|
+
data.tar.gz: 2f902bb6a8e558c72784c1b4b7e78c27d2a1c77142ccca2974eaec001d103667
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1b672eac79678e86121d4590c076e9de542ff5c3c7d8040cb6001bb79004744e48c2992ac790a908d33132069c3966cba06a4388386fe4e321dc168865f7e3e5
|
|
7
|
+
data.tar.gz: f3fead8b59cc529f16d06fc5c4caaf4348046dcf012960a9b04bec8d8224485c2ebb6e54648347988dd18368c05fb2d51356c2a249460d70cfab78decf49bf6b
|
data/bin/kira
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
require 'kira'
|
|
5
|
+
require 'getoptlong'
|
|
6
|
+
require 'colorize'
|
|
7
|
+
|
|
8
|
+
class GridDecorator
|
|
9
|
+
def initialize
|
|
10
|
+
@top_border = ""
|
|
11
|
+
@bottom_border = ""
|
|
12
|
+
@vertical_separator = ""
|
|
13
|
+
@box_vertical_separator = ""
|
|
14
|
+
@row_separator = ""
|
|
15
|
+
@box_row_separator = ""
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def decorate(grid)
|
|
19
|
+
ret = @top_border
|
|
20
|
+
row_num = 0
|
|
21
|
+
grid.each_line do |line|
|
|
22
|
+
col_num = 0
|
|
23
|
+
line.chomp.each_char do |c|
|
|
24
|
+
if col_num % 3 == 0
|
|
25
|
+
ret += @box_vertical_separator
|
|
26
|
+
else
|
|
27
|
+
ret += @vertical_separator
|
|
28
|
+
end
|
|
29
|
+
ret += "#{c} "
|
|
30
|
+
col_num += 1
|
|
31
|
+
end
|
|
32
|
+
ret += @box_vertical_separator + "\n"
|
|
33
|
+
if (row_num + 1) % 3 == 0 and row_num != 8
|
|
34
|
+
ret += @box_row_separator
|
|
35
|
+
elsif row_num != 8
|
|
36
|
+
ret += @row_separator
|
|
37
|
+
end
|
|
38
|
+
row_num += 1
|
|
39
|
+
end
|
|
40
|
+
ret += @bottom_border
|
|
41
|
+
|
|
42
|
+
ret
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ASCIIGridDecorator < GridDecorator
|
|
47
|
+
def initialize
|
|
48
|
+
@top_border = "+-------" * 3 + "+\n"
|
|
49
|
+
@bottom_border = "+-------" * 3 + "+"
|
|
50
|
+
@vertical_separator = ""
|
|
51
|
+
@box_vertical_separator = "| "
|
|
52
|
+
@row_separator = ""
|
|
53
|
+
@box_row_separator = @top_border
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class BoxGridDecorator < GridDecorator
|
|
58
|
+
def initialize
|
|
59
|
+
@top_border = "┏━━━┯━━━┯━━━┳━━━┯━━━┯━━━┳━━━┯━━━┯━━━┓\n"
|
|
60
|
+
@bottom_border = "┗━━━┷━━━┷━━━┻━━━┷━━━┷━━━┻━━━┷━━━┷━━━┛"
|
|
61
|
+
@vertical_separator = "│ "
|
|
62
|
+
@box_vertical_separator = "┃ "
|
|
63
|
+
@row_separator = "┠───┼───┼───╂───┼───┼───╂───┼───┼───┨\n"
|
|
64
|
+
@box_row_separator = "┣━━━┿━━━┿━━━╋━━━┿━━━┿━━━╋━━━┿━━━┿━━━┫\n"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Printer
|
|
69
|
+
def print(sudoku, decorator)
|
|
70
|
+
puts decorator.decorate(sudoku.to_s + "\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class ColorPrinter < Printer
|
|
75
|
+
def print(sudoku, decorator)
|
|
76
|
+
grid = decorator.decorate(sudoku.to_s + "\n")
|
|
77
|
+
colors = String.colors.shuffle
|
|
78
|
+
colors.delete_if do |item|
|
|
79
|
+
[:black, :light_black, :white, :light_white].include? item
|
|
80
|
+
end
|
|
81
|
+
output = ""
|
|
82
|
+
i = 0
|
|
83
|
+
grid.each_char do |c|
|
|
84
|
+
if c.match?(/[1-9]/)
|
|
85
|
+
group_no = sudoku.grid_of_group_idxs[i]
|
|
86
|
+
if group_no
|
|
87
|
+
group_no %= colors.size
|
|
88
|
+
output << c.colorize(colors[group_no])
|
|
89
|
+
else
|
|
90
|
+
output << c
|
|
91
|
+
end
|
|
92
|
+
i += 1
|
|
93
|
+
else
|
|
94
|
+
output << c
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
puts output
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
printer = Printer.new
|
|
103
|
+
decorator = GridDecorator.new
|
|
104
|
+
|
|
105
|
+
opts = GetoptLong.new(
|
|
106
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
|
107
|
+
['--version', '-V', GetoptLong::NO_ARGUMENT],
|
|
108
|
+
['--pretty', GetoptLong::OPTIONAL_ARGUMENT],
|
|
109
|
+
['--color', GetoptLong::NO_ARGUMENT]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
opts.each do |opt, arg|
|
|
113
|
+
case opt
|
|
114
|
+
when '--help'
|
|
115
|
+
puts <<~EOF
|
|
116
|
+
Usage: #{$0} [options]
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
-h, --help display this help and exit
|
|
120
|
+
-V, --version output version information and exit
|
|
121
|
+
--pretty[=<format>] pretty-print the result in a given format
|
|
122
|
+
--color colorize the output
|
|
123
|
+
EOF
|
|
124
|
+
exit 0
|
|
125
|
+
when '--version'
|
|
126
|
+
puts "Kira #{Kira::VERSION}"
|
|
127
|
+
exit 0
|
|
128
|
+
when '--pretty'
|
|
129
|
+
if arg == "" or arg == "ascii"
|
|
130
|
+
decorator = ASCIIGridDecorator.new
|
|
131
|
+
elsif arg == "box"
|
|
132
|
+
decorator = BoxGridDecorator.new
|
|
133
|
+
end
|
|
134
|
+
when '--color'
|
|
135
|
+
printer = ColorPrinter.new
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
input = $stdin.read
|
|
141
|
+
sudoku = Kira::Sudoku.new(input)
|
|
142
|
+
unless sudoku.solve
|
|
143
|
+
raise StandardError.new("Over-constrained puzzle")
|
|
144
|
+
end
|
|
145
|
+
printer.print(sudoku, decorator)
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
STDERR.puts "Error: " + e.message
|
|
148
|
+
end
|
data/lib/kira.rb
ADDED
data/lib/kira/group.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
module Kira
|
|
4
|
+
|
|
5
|
+
# Represents a group of cells, the sum of which must be equal to a certain
|
|
6
|
+
# fixed number. It is initialized with an equation having the following form
|
|
7
|
+
# (whitespaces are ignored):
|
|
8
|
+
# (r1, c1) + (r2, c2) + ... = s
|
|
9
|
+
# where:
|
|
10
|
+
# rn - row number
|
|
11
|
+
# cn - column number
|
|
12
|
+
# s - sum of the values at the given indexes
|
|
13
|
+
class Group
|
|
14
|
+
def initialize(equation)
|
|
15
|
+
equation.delete!(" \t\n\r")
|
|
16
|
+
|
|
17
|
+
unless equation.match /(\(\d,\d\)+)*\(\d,\d\)=\d+/
|
|
18
|
+
raise ArgumentError.new("Equation has invalid format")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
*str_idxs, @sum = equation.split(/[=+]/)
|
|
22
|
+
@sum = @sum.to_i
|
|
23
|
+
@indexes = []
|
|
24
|
+
str_idxs.each do |idx|
|
|
25
|
+
row, col = idx.scan(/\d+/).map(&:to_i)
|
|
26
|
+
indexes.push(row*9 + col)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :indexes, :sum
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
(@indexes.map { |i| "(#{i / 9}, #{i % 9})" }).join(" + ") + " = #{@sum}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/kira/puzzle.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
module Kira
|
|
4
|
+
|
|
5
|
+
# Represents the state of a 9x9 sudoku puzzle.
|
|
6
|
+
class Puzzle
|
|
7
|
+
def initialize(grid)
|
|
8
|
+
grid.delete!(" \t\n\r")
|
|
9
|
+
|
|
10
|
+
if grid.length != 81
|
|
11
|
+
raise ArgumentError.new("Grid has invalid size")
|
|
12
|
+
elsif not grid.match("^[0-9.]{81}$")
|
|
13
|
+
raise ArgumentError.new("Grid contains invalid characters")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@grid = []
|
|
17
|
+
grid.each_char { |c| @grid.push(c.to_i) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :grid
|
|
21
|
+
|
|
22
|
+
# Traverses the row, column, and box containing the 'pos' and calls the
|
|
23
|
+
# 'proc' with the current index as an argument on each step. Note that it
|
|
24
|
+
# visits some cells more than once.
|
|
25
|
+
def scan(pos, &proc)
|
|
26
|
+
# pos - (pos % 9):
|
|
27
|
+
# index of the first cell in the row containing 'pos'.
|
|
28
|
+
#
|
|
29
|
+
# pos % 9:
|
|
30
|
+
# index of the first cell in the column containing 'pos'.
|
|
31
|
+
#
|
|
32
|
+
# (pos - (pos % 3)):
|
|
33
|
+
# index of the left-most cell in the box containing 'pos'.
|
|
34
|
+
#
|
|
35
|
+
# (pos - (pos % 3)) % 9:
|
|
36
|
+
# index of the first cell in the column containing the top-left corner
|
|
37
|
+
# of the box.
|
|
38
|
+
#
|
|
39
|
+
# (pos - (pos % 27)):
|
|
40
|
+
# index of the first cell in the row containing the top-left corner of
|
|
41
|
+
# the box.
|
|
42
|
+
#
|
|
43
|
+
# (pos - (pos % 3)) % 9 + (pos - (pos % 27)):
|
|
44
|
+
# index of the top-left corner of the box containing 'pos'.
|
|
45
|
+
|
|
46
|
+
# Scan row
|
|
47
|
+
9.times do |i|
|
|
48
|
+
proc.call((pos - (pos % 9)) + i)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scan col
|
|
52
|
+
9.times do |i|
|
|
53
|
+
proc.call((pos % 9) + i*9)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Scan box
|
|
57
|
+
corner_idx = (pos - (pos % 3)) % 9 + (pos - (pos % 27))
|
|
58
|
+
3.times do |i|
|
|
59
|
+
3.times do |j|
|
|
60
|
+
proc.call(corner_idx + i*9 + j)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias :update :scan
|
|
66
|
+
|
|
67
|
+
# Returns true if the 'val' on the given 'pos' does not repeat in a column,
|
|
68
|
+
# row or box. The 'pos' is a 1-dimensional, 0-based index.
|
|
69
|
+
def valid?(val, pos)
|
|
70
|
+
if val != 0
|
|
71
|
+
scan(pos) { |i| if val == @grid[i] then return false end }
|
|
72
|
+
end
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_s
|
|
77
|
+
string = ""
|
|
78
|
+
0.upto(80) do |i|
|
|
79
|
+
if @grid[i] == 0
|
|
80
|
+
string << "."
|
|
81
|
+
else
|
|
82
|
+
string << @grid[i].to_s
|
|
83
|
+
end
|
|
84
|
+
if ((i + 1) % 9 == 0 and i != 80)
|
|
85
|
+
string << "\n"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
string
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def [](idx)
|
|
93
|
+
@grid[idx]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def []=(idx, val)
|
|
97
|
+
if idx > 80
|
|
98
|
+
raise IndexError.new("Index out of range")
|
|
99
|
+
elsif not val.between?(0, 9)
|
|
100
|
+
raise ArgumentError.new("Value out of range")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@grid[idx] = val
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/kira/sudoku.rb
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'kira/group'
|
|
4
|
+
require 'kira/puzzle'
|
|
5
|
+
|
|
6
|
+
module Kira
|
|
7
|
+
|
|
8
|
+
# A Killer Sudoku solver. It is initialized with an optional string
|
|
9
|
+
# containing a grid followed by any number of equations. The grid consists
|
|
10
|
+
# of digits from 1 to 9 and dots representing an empty cell. Each equation
|
|
11
|
+
# has to be on its own line.
|
|
12
|
+
class Sudoku
|
|
13
|
+
def initialize(string)
|
|
14
|
+
grid = ""
|
|
15
|
+
@groups = []
|
|
16
|
+
string.each_line do |line|
|
|
17
|
+
if line.match("^[1-9\. \t\n\r]*$")
|
|
18
|
+
grid << line
|
|
19
|
+
else
|
|
20
|
+
@groups.push(Group.new(line))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if grid == ""
|
|
25
|
+
grid << '.' * 81
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@puzzle = Puzzle.new(grid)
|
|
29
|
+
|
|
30
|
+
# '@grid_of_group_idxs' maps the cell's index to the index of the group
|
|
31
|
+
# to which the cell belongs.
|
|
32
|
+
@grid_of_group_idxs = Array.new(81)
|
|
33
|
+
0.upto(@groups.size - 1) do |i|
|
|
34
|
+
@groups[i].indexes.each { |j| @grid_of_group_idxs[j] = i }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :puzzle, :groups, :grid_of_group_idxs
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
@puzzle.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def solve(start = 0)
|
|
45
|
+
# Find next empty cell.
|
|
46
|
+
until @puzzle[start] == 0 or @puzzle[start] == nil
|
|
47
|
+
start += 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if @puzzle[start] == nil
|
|
51
|
+
return true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
possibilities = []
|
|
55
|
+
1.upto(9) do |v|
|
|
56
|
+
if valid?(v, start)
|
|
57
|
+
possibilities.push(v)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
possibilities.each do |p|
|
|
62
|
+
@puzzle[start] = p
|
|
63
|
+
|
|
64
|
+
if solve(start + 1)
|
|
65
|
+
return true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@puzzle[start] = 0
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns true if the 'val' on the given 'pos' does not repeat in a column,
|
|
74
|
+
# row, box or group and all the equations are satisfied.
|
|
75
|
+
def valid?(val, pos)
|
|
76
|
+
unless @puzzle.valid?(val, pos)
|
|
77
|
+
return false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
group_idx = @grid_of_group_idxs[pos]
|
|
81
|
+
if group_idx == nil
|
|
82
|
+
return true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
g = @groups[group_idx]
|
|
86
|
+
sum = val
|
|
87
|
+
|
|
88
|
+
# Set to true when the group contains an empty cell at position other
|
|
89
|
+
# than 'pos'.
|
|
90
|
+
empty = false
|
|
91
|
+
|
|
92
|
+
g.indexes.each do |idx|
|
|
93
|
+
v = @puzzle.grid[idx]
|
|
94
|
+
|
|
95
|
+
if idx != pos
|
|
96
|
+
if v == val
|
|
97
|
+
return false
|
|
98
|
+
end
|
|
99
|
+
if v == 0
|
|
100
|
+
empty = true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sum += v
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
unless (sum == g.sum and not empty) or (empty and sum < g.sum)
|
|
108
|
+
return false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/kira/version.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kira
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wadim X. Janikowski
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-03-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email: wadim.janikowski@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- kira
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- bin/kira
|
|
21
|
+
- lib/kira.rb
|
|
22
|
+
- lib/kira/group.rb
|
|
23
|
+
- lib/kira/puzzle.rb
|
|
24
|
+
- lib/kira/sudoku.rb
|
|
25
|
+
- lib/kira/version.rb
|
|
26
|
+
homepage: https://github.com/wadiim/kira
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata: {}
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.1.4
|
|
46
|
+
signing_key:
|
|
47
|
+
specification_version: 4
|
|
48
|
+
summary: A (killer) sudoku solver.
|
|
49
|
+
test_files: []
|