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 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
@@ -0,0 +1,2 @@
1
+ require 'kira/version'
2
+ require 'kira/sudoku'
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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kira
4
+ VERSION = '0.2.0'
5
+ end
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: []