sangi 0.1.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/LICENSE +21 -0
- data/README.md +111 -0
- data/exe/sangi +5 -0
- data/lib/sangi/board.rb +35 -0
- data/lib/sangi/cell.rb +60 -0
- data/lib/sangi/cli.rb +142 -0
- data/lib/sangi/config.rb +75 -0
- data/lib/sangi/engine.rb +75 -0
- data/lib/sangi/engines/addition_engine.rb +184 -0
- data/lib/sangi/engines/copy_engine.rb +85 -0
- data/lib/sangi/engines/subtraction_borrowing.rb +124 -0
- data/lib/sangi/engines/subtraction_engine.rb +134 -0
- data/lib/sangi/errors.rb +8 -0
- data/lib/sangi/event.rb +14 -0
- data/lib/sangi/exporter/text_exporter.rb +41 -0
- data/lib/sangi/expression.rb +20 -0
- data/lib/sangi/parser.rb +49 -0
- data/lib/sangi/renderer/ascii_renderer.rb +151 -0
- data/lib/sangi/renderer/cell_renderer.rb +97 -0
- data/lib/sangi/repl.rb +45 -0
- data/lib/sangi/rod.rb +16 -0
- data/lib/sangi/rod_factory.rb +13 -0
- data/lib/sangi/row.rb +51 -0
- data/lib/sangi/signed_number.rb +23 -0
- data/lib/sangi/step.rb +16 -0
- data/lib/sangi/step_builder.rb +87 -0
- data/lib/sangi/step_viewer.rb +124 -0
- data/lib/sangi/terminal_width_warning.rb +56 -0
- data/lib/sangi/version.rb +3 -0
- data/lib/sangi.rb +26 -0
- data/sangi.gemspec +31 -0
- metadata +77 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
module Renderer
|
|
3
|
+
class AsciiRenderer
|
|
4
|
+
LABEL_WIDTH = 7
|
|
5
|
+
CELL_WIDTH = CellRenderer::CELL_WIDTH
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
@cell_renderer = CellRenderer.new(config)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(step, total_steps:)
|
|
13
|
+
lines = []
|
|
14
|
+
lines.concat(header_lines(step, total_steps))
|
|
15
|
+
lines << ""
|
|
16
|
+
lines.concat(message_lines(step))
|
|
17
|
+
lines << ""
|
|
18
|
+
lines.concat(board_lines(step.board))
|
|
19
|
+
lines << ""
|
|
20
|
+
lines.concat(value_lines(step.board))
|
|
21
|
+
lines << ""
|
|
22
|
+
lines << "keys: n next / p prev / r reset / e end / a auto / q quit / ? help"
|
|
23
|
+
lines.join("\n") + "\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def header_lines(step, total_steps)
|
|
29
|
+
state = step.numeric_state
|
|
30
|
+
[
|
|
31
|
+
"expr: #{state[:source]}",
|
|
32
|
+
"normalized: #{state[:normalized]}",
|
|
33
|
+
"result: #{state[:result_value]}",
|
|
34
|
+
"step: #{step.index}/#{total_steps}",
|
|
35
|
+
"mode: #{@config.mode} zero: #{@config.zero_mode} sign: #{@config.sign_mode}"
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def message_lines(step)
|
|
40
|
+
message = selected_message(step)
|
|
41
|
+
lines = ["Step #{step.index}: #{step.title}"]
|
|
42
|
+
lines << message if message && !message.empty?
|
|
43
|
+
lines
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def selected_message(step)
|
|
47
|
+
case @config.explain_mode
|
|
48
|
+
when :none then nil
|
|
49
|
+
when :learn then step.learn_message || step.brief_message
|
|
50
|
+
else step.brief_message
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def board_lines(board)
|
|
55
|
+
places = (0...board.place_count).to_a.reverse
|
|
56
|
+
lines = [place_header(places), border(places.size)]
|
|
57
|
+
%i[a b work].each do |row_name|
|
|
58
|
+
row = board.row(row_name)
|
|
59
|
+
row_cells = render_row_cells(row, places)
|
|
60
|
+
row_cells.each_with_index do |cell_line_parts, index|
|
|
61
|
+
label = index.zero? ? row_label(row, board.highlight) : ""
|
|
62
|
+
lines << format("%-#{LABEL_WIDTH}s", label) + "|" + cell_line_parts.join("|") + "|"
|
|
63
|
+
end
|
|
64
|
+
lines << border(places.size)
|
|
65
|
+
end
|
|
66
|
+
lines
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_row_cells(row, places)
|
|
70
|
+
slash_place = slash_place_for(row)
|
|
71
|
+
rendered = places.map do |place|
|
|
72
|
+
@cell_renderer.render(
|
|
73
|
+
row.cells.fetch(place),
|
|
74
|
+
row_sign: row.sign,
|
|
75
|
+
slash: slash_place == place
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
CellRenderer::CELL_HEIGHT.times.map do |line_index|
|
|
79
|
+
parts = rendered.map { |cell_lines| cell_lines[line_index] }
|
|
80
|
+
color_negative_row?(row) ? parts.map { |part| colorize(part) } : parts
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def place_header(places)
|
|
85
|
+
format("%-#{LABEL_WIDTH}s", "") +
|
|
86
|
+
places.map { |place| center(place_label(place), CELL_WIDTH) }.join(" ")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def border(place_count)
|
|
90
|
+
format("%-#{LABEL_WIDTH}s", "") + "+" + (("-" * CELL_WIDTH) + "+") * place_count
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def row_label(row, highlight)
|
|
94
|
+
label = case row.name
|
|
95
|
+
when :a then "A"
|
|
96
|
+
when :b then "B"
|
|
97
|
+
else "Work"
|
|
98
|
+
end
|
|
99
|
+
label += "-" if show_modern_negative_marker?(row)
|
|
100
|
+
label += " *" if highlight && highlight[:row] == row.name
|
|
101
|
+
label
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def value_lines(board)
|
|
105
|
+
[
|
|
106
|
+
"values:",
|
|
107
|
+
format(" A: %s", board.row(:a).value),
|
|
108
|
+
format(" B: %s", board.row(:b).value),
|
|
109
|
+
format(" Work: %s", board.row(:work).value)
|
|
110
|
+
]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def place_label(place)
|
|
114
|
+
labels = %w[ones tens hundreds thousands]
|
|
115
|
+
labels.fetch(place, "10^#{place}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def center(text, width)
|
|
119
|
+
text = text[0, width]
|
|
120
|
+
left = (width - text.length) / 2
|
|
121
|
+
right = width - text.length - left
|
|
122
|
+
(" " * left) + text + (" " * right)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def slash_place_for(row)
|
|
126
|
+
return nil unless row.sign.negative?
|
|
127
|
+
return nil unless %i[slash dual].include?(effective_sign_mode)
|
|
128
|
+
|
|
129
|
+
row.cells.index { |cell| cell.value.positive? }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def show_modern_negative_marker?(row)
|
|
133
|
+
return false unless row.sign.negative? && row.magnitude.positive?
|
|
134
|
+
|
|
135
|
+
%i[modern dual].include?(effective_sign_mode)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def color_negative_row?(row)
|
|
139
|
+
row.sign.negative? && row.magnitude.positive? && effective_sign_mode == :color && @config.color
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def effective_sign_mode
|
|
143
|
+
@config.sign_mode == :color && !@config.color ? :modern : @config.sign_mode
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def colorize(text)
|
|
147
|
+
"\e[31m#{text}\e[0m"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
module Renderer
|
|
3
|
+
class CellRenderer
|
|
4
|
+
CELL_WIDTH = 11
|
|
5
|
+
CELL_HEIGHT = 5
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render(cell, row_sign: 1, slash: false)
|
|
12
|
+
lines = blank_lines
|
|
13
|
+
if cell.value.zero?
|
|
14
|
+
render_zero(lines)
|
|
15
|
+
elsif cell.orientation == :vertical
|
|
16
|
+
render_vertical(lines, cell)
|
|
17
|
+
else
|
|
18
|
+
render_horizontal(lines, cell)
|
|
19
|
+
end
|
|
20
|
+
put_char(lines, 0, 9, "/") if slash && row_sign.negative?
|
|
21
|
+
lines
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def blank_lines
|
|
27
|
+
Array.new(CELL_HEIGHT) { " " * CELL_WIDTH }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_zero(lines)
|
|
31
|
+
case @config.zero_mode
|
|
32
|
+
when :digit
|
|
33
|
+
put_text(lines, 1, 5, "0")
|
|
34
|
+
when :circle
|
|
35
|
+
put_text(lines, 1, 5, "o")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_vertical(lines, cell)
|
|
40
|
+
cell.five_rods.size.times do |index|
|
|
41
|
+
put_text(lines, [index, CELL_HEIGHT - 1].min, 3, "-----")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unit_row = cell.five_rods.empty? ? 2 : [cell.five_rods.size, CELL_HEIGHT - 1].min
|
|
45
|
+
unit_positions(cell.unit_rods.size).each do |column|
|
|
46
|
+
put_char(lines, unit_row, column, "|")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_horizontal(lines, cell)
|
|
51
|
+
five_positions(cell.five_rods.size).each do |column|
|
|
52
|
+
CELL_HEIGHT.times { |row| put_char(lines, row, column, "|") }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
start_row = cell.five_rods.empty? ? 0 : 1
|
|
56
|
+
cell.unit_rods.size.times do |index|
|
|
57
|
+
row = [start_row + index, CELL_HEIGHT - 1].min
|
|
58
|
+
put_text(lines, row, 3, "-----")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def unit_positions(count)
|
|
63
|
+
positions = {
|
|
64
|
+
1 => [5],
|
|
65
|
+
2 => [4, 6],
|
|
66
|
+
3 => [3, 5, 7],
|
|
67
|
+
4 => [2, 4, 6, 8],
|
|
68
|
+
5 => [1, 3, 5, 7, 9]
|
|
69
|
+
}
|
|
70
|
+
positions.fetch([count, 5].min, [])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def five_positions(count)
|
|
74
|
+
positions = {
|
|
75
|
+
1 => [5],
|
|
76
|
+
2 => [4, 6]
|
|
77
|
+
}
|
|
78
|
+
positions.fetch([count, 2].min, [])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def put_text(lines, row, column, text)
|
|
82
|
+
text.chars.each_with_index do |char, offset|
|
|
83
|
+
put_char(lines, row, column + offset, char)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def put_char(lines, row, column, char)
|
|
88
|
+
return if row.negative? || row >= CELL_HEIGHT
|
|
89
|
+
return if column.negative? || column >= CELL_WIDTH
|
|
90
|
+
|
|
91
|
+
chars = lines[row].chars
|
|
92
|
+
chars[column] = char
|
|
93
|
+
lines[row] = chars.join
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/sangi/repl.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class REPL
|
|
3
|
+
def initialize(config:, input: STDIN, output: STDOUT)
|
|
4
|
+
@config = config
|
|
5
|
+
@input = input
|
|
6
|
+
@output = output
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def start
|
|
10
|
+
loop do
|
|
11
|
+
@output.print "sangi> "
|
|
12
|
+
line = @input.gets
|
|
13
|
+
break if line.nil?
|
|
14
|
+
|
|
15
|
+
line = line.strip
|
|
16
|
+
next if line.empty?
|
|
17
|
+
break if %w[:q :quit].include?(line)
|
|
18
|
+
|
|
19
|
+
if line == ":help"
|
|
20
|
+
show_help
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
run_expression(line)
|
|
25
|
+
end
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def run_expression(source)
|
|
32
|
+
expression = Parser.new(@config).parse(source)
|
|
33
|
+
steps = Engine.new(@config).build_steps(expression)
|
|
34
|
+
renderer = Renderer::AsciiRenderer.new(@config)
|
|
35
|
+
StepViewer.new(steps: steps, renderer: renderer, input: @input, output: @output).start
|
|
36
|
+
rescue Sangi::Error => e
|
|
37
|
+
@output.puts e.message
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def show_help
|
|
41
|
+
@output.puts "式を入力すると計算を開始します。例: 128+47, 100-7, -12+7, 3--5"
|
|
42
|
+
@output.puts ":help ヘルプ / :q 終了 / :quit 終了"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/sangi/rod.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Rod
|
|
3
|
+
attr_reader :id, :kind
|
|
4
|
+
|
|
5
|
+
def initialize(id:, kind:)
|
|
6
|
+
raise ValidationError, "invalid rod kind" unless %i[unit five].include?(kind)
|
|
7
|
+
|
|
8
|
+
@id = id
|
|
9
|
+
@kind = kind
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def value
|
|
13
|
+
kind == :five ? 5 : 1
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/sangi/row.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Row
|
|
3
|
+
attr_reader :name
|
|
4
|
+
attr_accessor :sign, :cells
|
|
5
|
+
|
|
6
|
+
def self.from_integer(name:, value:, place_count:, rod_factory:)
|
|
7
|
+
sign = value <=> 0
|
|
8
|
+
digits = SignedNumber.from_integer(value.abs).digits
|
|
9
|
+
cells = place_count.times.map do |place|
|
|
10
|
+
Cell.from_digit(
|
|
11
|
+
place_index: place,
|
|
12
|
+
digit: digits.fetch(place, 0),
|
|
13
|
+
rod_factory: rod_factory
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
new(name: name, sign: sign, cells: cells)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(name:, sign:, cells:)
|
|
20
|
+
@name = name
|
|
21
|
+
@sign = sign
|
|
22
|
+
@cells = cells
|
|
23
|
+
normalize_zero_sign!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def magnitude
|
|
27
|
+
cells.each_with_index.sum do |cell, place_index|
|
|
28
|
+
cell.value * (10**place_index)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def value
|
|
33
|
+
current_magnitude = magnitude
|
|
34
|
+
return 0 if current_magnitude.zero?
|
|
35
|
+
|
|
36
|
+
sign * current_magnitude
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalize_zero_sign!
|
|
40
|
+
@sign = 0 if magnitude.zero?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clone_deep
|
|
44
|
+
Row.new(
|
|
45
|
+
name: name,
|
|
46
|
+
sign: sign,
|
|
47
|
+
cells: cells.map(&:clone_deep)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class SignedNumber
|
|
3
|
+
attr_reader :sign, :digits
|
|
4
|
+
|
|
5
|
+
def self.from_integer(value)
|
|
6
|
+
sign = value <=> 0
|
|
7
|
+
digits = value.abs.to_s.chars.reverse.map(&:to_i)
|
|
8
|
+
digits = [0] if digits.empty?
|
|
9
|
+
new(sign: sign, digits: digits)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(sign:, digits:)
|
|
13
|
+
@digits = digits.empty? ? [0] : digits
|
|
14
|
+
@sign = @digits.all?(&:zero?) ? 0 : sign
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_i
|
|
18
|
+
return 0 if sign.zero?
|
|
19
|
+
|
|
20
|
+
sign * digits.reverse.join.to_i
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/sangi/step.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class Step
|
|
3
|
+
attr_reader :index, :title, :brief_message, :learn_message
|
|
4
|
+
attr_reader :event, :board, :numeric_state
|
|
5
|
+
|
|
6
|
+
def initialize(index:, title:, brief_message:, learn_message:, event:, board:, numeric_state:)
|
|
7
|
+
@index = index
|
|
8
|
+
@title = title
|
|
9
|
+
@brief_message = brief_message
|
|
10
|
+
@learn_message = learn_message
|
|
11
|
+
@event = event
|
|
12
|
+
@board = board
|
|
13
|
+
@numeric_state = numeric_state
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Sangi
|
|
2
|
+
class StepBuilder
|
|
3
|
+
attr_reader :config, :expression, :board, :result_value, :rod_factory, :steps
|
|
4
|
+
|
|
5
|
+
def initialize(config:, expression:, board:, result_value:, rod_factory:)
|
|
6
|
+
@config = config
|
|
7
|
+
@expression = expression
|
|
8
|
+
@board = board
|
|
9
|
+
@result_value = result_value
|
|
10
|
+
@rod_factory = rod_factory
|
|
11
|
+
@steps = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def push(title:, brief_message:, event:, learn_message: nil)
|
|
15
|
+
steps << Step.new(
|
|
16
|
+
index: steps.size + 1,
|
|
17
|
+
title: title,
|
|
18
|
+
brief_message: brief_message,
|
|
19
|
+
learn_message: learn_message || brief_message,
|
|
20
|
+
event: event,
|
|
21
|
+
board: board.clone_deep,
|
|
22
|
+
numeric_state: numeric_state
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def place_rod(row:, place:, kind:, title: "棒を置く", brief_message: nil, learn_message: nil, event_type: :place_rod)
|
|
27
|
+
cell = work_cell(row, place)
|
|
28
|
+
cell.add_rod(rod_factory.build(kind))
|
|
29
|
+
board.highlight = { row: row, place_index: place }
|
|
30
|
+
push(
|
|
31
|
+
title: title,
|
|
32
|
+
brief_message: brief_message || "#{row_label(row)}の#{place_name(place)}に#{rod_name(kind)}棒を1本置きます。",
|
|
33
|
+
learn_message: learn_message || "#{place_name(place)}に#{rod_value_name(kind)}を表す棒を1本追加します。",
|
|
34
|
+
event: Event.new(type: event_type, row: row, place_index: place, kind: kind)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def remove_rod(row:, place:, kind:, title: "棒を取り除く", brief_message: nil, learn_message: nil, event_type: :remove_rod)
|
|
39
|
+
cell = work_cell(row, place)
|
|
40
|
+
removed = cell.remove_rod(kind)
|
|
41
|
+
raise InternalError, "#{row_label(row)}の#{place_name(place)}に#{rod_name(kind)}棒がありません。" if removed.nil?
|
|
42
|
+
|
|
43
|
+
board.highlight = { row: row, place_index: place }
|
|
44
|
+
push(
|
|
45
|
+
title: title,
|
|
46
|
+
brief_message: brief_message || "#{row_label(row)}の#{place_name(place)}から#{rod_name(kind)}棒を1本取り除きます。",
|
|
47
|
+
learn_message: learn_message || "#{place_name(place)}から#{rod_value_name(kind)}を表す棒を1本取り除きます。",
|
|
48
|
+
event: Event.new(type: event_type, row: row, place_index: place, kind: kind)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def row_label(row)
|
|
53
|
+
row == :work ? "Work" : row.to_s.upcase
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def rod_name(kind)
|
|
57
|
+
kind == :five ? "five" : "unit"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def rod_value_name(kind)
|
|
61
|
+
kind == :five ? "5" : "1"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def place_name(place)
|
|
65
|
+
names = %w[一の位 十の位 百の位 千の位 万の位 十万の位 百万の位 千万の位 一億の位 十億の位]
|
|
66
|
+
names.fetch(place, "10^#{place}の位")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def work_cell(row, place)
|
|
72
|
+
board.row(row).cells.fetch(place)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def numeric_state
|
|
76
|
+
{
|
|
77
|
+
source: expression.source,
|
|
78
|
+
normalized: expression.normalized_source,
|
|
79
|
+
result_value: result_value,
|
|
80
|
+
left: expression.left,
|
|
81
|
+
right: expression.right,
|
|
82
|
+
effective_right: expression.effective_right,
|
|
83
|
+
work_value: board.row(:work).value
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
require "io/wait"
|
|
3
|
+
|
|
4
|
+
module Sangi
|
|
5
|
+
class StepViewer
|
|
6
|
+
AUTOPLAY_INTERVAL = 0.35
|
|
7
|
+
|
|
8
|
+
def initialize(steps:, renderer:, input: STDIN, output: STDOUT, clear_screen: true)
|
|
9
|
+
@steps = steps
|
|
10
|
+
@renderer = renderer
|
|
11
|
+
@input = input
|
|
12
|
+
@output = output
|
|
13
|
+
@clear_screen = clear_screen
|
|
14
|
+
@current_index = 0
|
|
15
|
+
@width_warning = TerminalWidthWarning.new(output: output)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
return if @steps.empty?
|
|
20
|
+
|
|
21
|
+
@exit_requested = false
|
|
22
|
+
loop do
|
|
23
|
+
render_current
|
|
24
|
+
handle_key(read_key)
|
|
25
|
+
break if @exit_requested
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_current
|
|
32
|
+
clear
|
|
33
|
+
warning = @width_warning.message(@steps)
|
|
34
|
+
@output.puts warning if warning
|
|
35
|
+
@output.print @renderer.render(@steps[@current_index], total_steps: @steps.size)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def next_step
|
|
39
|
+
@current_index = [@current_index + 1, @steps.size - 1].min
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def previous_step
|
|
43
|
+
@current_index = [@current_index - 1, 0].max
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_key(key)
|
|
47
|
+
case key
|
|
48
|
+
when "n", "\r", "\n"
|
|
49
|
+
next_step
|
|
50
|
+
when "p"
|
|
51
|
+
previous_step
|
|
52
|
+
when "r"
|
|
53
|
+
@current_index = 0
|
|
54
|
+
when "e"
|
|
55
|
+
@current_index = @steps.size - 1
|
|
56
|
+
when "a"
|
|
57
|
+
autoplay
|
|
58
|
+
when "?"
|
|
59
|
+
show_key_help
|
|
60
|
+
when "q", "\u0003"
|
|
61
|
+
@exit_requested = true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def autoplay
|
|
66
|
+
loop do
|
|
67
|
+
key = read_key_with_timeout(AUTOPLAY_INTERVAL)
|
|
68
|
+
break if handle_autoplay_key(key)
|
|
69
|
+
break if @current_index >= @steps.size - 1
|
|
70
|
+
|
|
71
|
+
@current_index += 1
|
|
72
|
+
render_current
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def handle_autoplay_key(key)
|
|
77
|
+
case key
|
|
78
|
+
when nil
|
|
79
|
+
false
|
|
80
|
+
when "a"
|
|
81
|
+
true
|
|
82
|
+
when "q", "\u0003"
|
|
83
|
+
@exit_requested = true
|
|
84
|
+
true
|
|
85
|
+
else
|
|
86
|
+
handle_key(key)
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show_key_help
|
|
92
|
+
@output.puts
|
|
93
|
+
@output.puts "n/Enter: next, p: previous, r: reset, e: end, a: auto, q: quit"
|
|
94
|
+
@output.puts "press any key to continue"
|
|
95
|
+
read_key
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read_key
|
|
99
|
+
return @input.getch if @input.is_a?(IO) && @input.respond_to?(:getch)
|
|
100
|
+
|
|
101
|
+
@input.gets&.chars&.first || "q"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def read_key_with_timeout(timeout)
|
|
105
|
+
return read_key if buffered_input_available?
|
|
106
|
+
return nil unless @input.is_a?(IO) && @input.respond_to?(:wait_readable)
|
|
107
|
+
return nil unless @input.wait_readable(timeout)
|
|
108
|
+
|
|
109
|
+
read_key
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def buffered_input_available?
|
|
113
|
+
@input.respond_to?(:pos) &&
|
|
114
|
+
@input.respond_to?(:string) &&
|
|
115
|
+
@input.pos < @input.string.length
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def clear
|
|
119
|
+
return unless @clear_screen
|
|
120
|
+
|
|
121
|
+
@output.print "\e[2J\e[H"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
|
|
3
|
+
module Sangi
|
|
4
|
+
class TerminalWidthWarning
|
|
5
|
+
def initialize(output:)
|
|
6
|
+
@output = output
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def message(steps)
|
|
10
|
+
width = terminal_width
|
|
11
|
+
return nil if width.nil? || steps.empty?
|
|
12
|
+
|
|
13
|
+
required = required_width(steps)
|
|
14
|
+
return nil if required <= width
|
|
15
|
+
|
|
16
|
+
"warning: terminal width is #{width}, but this board needs about #{required} columns; output may wrap."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def warn_once(steps, error_output:)
|
|
20
|
+
warning = message(steps)
|
|
21
|
+
error_output.puts warning if warning
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def terminal_width
|
|
27
|
+
return nil unless @output.respond_to?(:tty?) && @output.tty?
|
|
28
|
+
|
|
29
|
+
width_from_output || width_from_console
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def width_from_output
|
|
33
|
+
return nil unless @output.respond_to?(:winsize)
|
|
34
|
+
|
|
35
|
+
@output.winsize[1]
|
|
36
|
+
rescue SystemCallError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def width_from_console
|
|
41
|
+
console = IO.console
|
|
42
|
+
return nil unless console
|
|
43
|
+
|
|
44
|
+
console.winsize[1]
|
|
45
|
+
rescue SystemCallError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def required_width(steps)
|
|
50
|
+
max_places = steps.map { |step| step.board.place_count }.max || 0
|
|
51
|
+
Renderer::AsciiRenderer::LABEL_WIDTH +
|
|
52
|
+
(Renderer::CellRenderer::CELL_WIDTH + 1) * max_places +
|
|
53
|
+
1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|