bangkok 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.
- data/ChangeLog +5 -0
- data/Credits +6 -0
- data/README +227 -0
- data/Rakefile +83 -0
- data/TODO +26 -0
- data/bin/bangkok +40 -0
- data/examples/announcer.rb +71 -0
- data/examples/game.pgn +15 -0
- data/examples/program_changes.rb +17 -0
- data/install.rb +95 -0
- data/lib/bangkok.rb +2 -0
- data/lib/bangkok/board.rb +110 -0
- data/lib/bangkok/chessgame.rb +50 -0
- data/lib/bangkok/gamelistener.rb +181 -0
- data/lib/bangkok/info.rb +6 -0
- data/lib/bangkok/move.rb +106 -0
- data/lib/bangkok/piece.rb +243 -0
- data/lib/bangkok/square.rb +47 -0
- data/test/mock_game_listener.rb +22 -0
- data/test/test_piece.rb +106 -0
- data/test/test_square.rb +109 -0
- metadata +75 -0
data/examples/game.pgn
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
[Event "?"]
|
2
|
+
[Site "Reykjavik open"]
|
3
|
+
[Date "1994.??.??"]
|
4
|
+
[Round "4"]
|
5
|
+
[White "Stefan Briem"]
|
6
|
+
[Black "Olafur Thorsson"]
|
7
|
+
[Result "0-1"]
|
8
|
+
[ECO "A02"]
|
9
|
+
|
10
|
+
1. f4 Nf6 2. Nf3 c5 3. e3 d5 4. d4 Bf5 5. c3 e6 6. Bd3 Bxd3 7. Qxd3 Nc6
|
11
|
+
8. O-O Be7 9. Nbd2 O-O 10. b3 Qc7 11. Ne5 a6 12. a4 Rab8 13. Nxc6 Qxc6
|
12
|
+
14. Ba3 b5 15. axb5 axb5 16. c4 Rfe8 17. Rfc1 cxd4 18. Bxe7 Rxe7
|
13
|
+
19. exd4 bxc4 20. bxc4 dxc4 21. Rxc4 Qd6 22. g3 Rc7 23. Ra5 g6 24. Rxc7
|
14
|
+
Qxc7 25. Rc5 Qa7 26. Qc3 Nd5 27. Qc1 Qa4 28. Rc4 Qa6 29. Nf3 Rb3 30. Nd2
|
15
|
+
Rd3 31. Ne4 Ne3 32. Rc8+ Kg7 33. Nf2 Ra3 34. Rb8 Ra1 35. Rb1 Qb7 0-1
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file is used to configure the MIDI output for a chess match.
|
2
|
+
|
3
|
+
# color :piece, program_number
|
4
|
+
|
5
|
+
black :K, 0
|
6
|
+
black :Q, 8
|
7
|
+
black :R, 16
|
8
|
+
black :B, 24
|
9
|
+
black :N, 32
|
10
|
+
black :P, 4
|
11
|
+
|
12
|
+
white :K, 4
|
13
|
+
white :Q, 12
|
14
|
+
white :R, 20
|
15
|
+
white :B, 28
|
16
|
+
white :N, 36
|
17
|
+
white :P, 44
|
data/install.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'find'
|
3
|
+
require 'ftools'
|
4
|
+
|
5
|
+
include Config
|
6
|
+
|
7
|
+
$ruby = CONFIG['ruby_install_name']
|
8
|
+
|
9
|
+
##
|
10
|
+
# Install a binary file. We patch in on the way through to
|
11
|
+
# insert a #! line. If this is a Unix install, we name
|
12
|
+
# the command (for example) 'bangkok' and let the shebang line
|
13
|
+
# handle running it. Under windows, we add a '.rb' extension
|
14
|
+
# and let file associations to their stuff
|
15
|
+
#
|
16
|
+
|
17
|
+
def installBIN(from, opfile)
|
18
|
+
|
19
|
+
tmp_dir = nil
|
20
|
+
for t in [".", "/tmp", "c:/temp", $bindir]
|
21
|
+
stat = File.stat(t) rescue next
|
22
|
+
if stat.directory? and stat.writable?
|
23
|
+
tmp_dir = t
|
24
|
+
break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
fail "Cannot find a temporary directory" unless tmp_dir
|
29
|
+
tmp_file = File.join(tmp_dir, "_tmp")
|
30
|
+
|
31
|
+
File.open(from) do |ip|
|
32
|
+
File.open(tmp_file, "w") do |op|
|
33
|
+
ruby = File.join($realbindir, $ruby)
|
34
|
+
op.puts "#!#{ruby} -w"
|
35
|
+
op.write ip.read
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if CONFIG["target_os"] =~ /mswin/i
|
40
|
+
opfile_path = File.join($bindir, opfile)
|
41
|
+
File::install(tmp_file, opfile_path, 0755, true)
|
42
|
+
File.open(File.join($bindir, opfile + '.cmd'), 'w') { | f |
|
43
|
+
f.puts "@ruby \"#{opfile_path}\" %*"
|
44
|
+
}
|
45
|
+
else
|
46
|
+
File::install(tmp_file, File.join($bindir, opfile), 0755, true)
|
47
|
+
end
|
48
|
+
File::unlink(tmp_file)
|
49
|
+
end
|
50
|
+
|
51
|
+
$sitedir = CONFIG["sitelibdir"]
|
52
|
+
unless $sitedir
|
53
|
+
version = CONFIG["MAJOR"]+"."+CONFIG["MINOR"]
|
54
|
+
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
55
|
+
$sitedir = $:.find {|x| x =~ /site_ruby/}
|
56
|
+
if !$sitedir
|
57
|
+
$sitedir = File.join($libdir, "site_ruby")
|
58
|
+
elsif $sitedir !~ Regexp.quote(version)
|
59
|
+
$sitedir = File.join($sitedir, version)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
$bindir = CONFIG["bindir"]
|
64
|
+
|
65
|
+
$realbindir = $bindir
|
66
|
+
|
67
|
+
bindir = CONFIG["bindir"]
|
68
|
+
if (destdir = ENV['DESTDIR'])
|
69
|
+
$bindir = destdir + $bindir
|
70
|
+
$sitedir = destdir + $sitedir
|
71
|
+
|
72
|
+
File::makedirs($bindir)
|
73
|
+
File::makedirs($sitedir)
|
74
|
+
end
|
75
|
+
|
76
|
+
bangkok_dest = File.join($sitedir, "bangkok")
|
77
|
+
File::makedirs(bangkok_dest, true)
|
78
|
+
File::chmod(0755, bangkok_dest)
|
79
|
+
|
80
|
+
# The library files
|
81
|
+
|
82
|
+
files = Dir.chdir('lib') { Dir['**/*.rb'] }
|
83
|
+
|
84
|
+
for fn in files
|
85
|
+
fn_dir = File.dirname(fn)
|
86
|
+
target_dir = File.join($sitedir, fn_dir)
|
87
|
+
if ! File.exist?(target_dir)
|
88
|
+
File.makedirs(target_dir)
|
89
|
+
end
|
90
|
+
File::install(File.join('lib', fn), File.join($sitedir, fn), 0644, true)
|
91
|
+
end
|
92
|
+
|
93
|
+
# and the executable
|
94
|
+
|
95
|
+
installBIN("bin/bangkok", "bangkok")
|
data/lib/bangkok.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'bangkok/square'
|
2
|
+
require 'bangkok/piece'
|
3
|
+
|
4
|
+
class Board
|
5
|
+
|
6
|
+
def initialize(listener)
|
7
|
+
@listener = listener
|
8
|
+
@pieces = []
|
9
|
+
[:R, :N, :B, :Q, :K, :B, :N, :R].each_with_index { | sym, file |
|
10
|
+
@pieces << Piece.create(self, listener, :white, sym, Square.new(file, 0))
|
11
|
+
@pieces << Piece.create(self, listener, :black, sym, Square.new(file, 7))
|
12
|
+
}
|
13
|
+
8.times { | file |
|
14
|
+
@pieces << Piece.create(self, listener, :white, :P, Square.new(file, 1))
|
15
|
+
@pieces << Piece.create(self, listener, :black, :P, Square.new(file, 6))
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply(move)
|
20
|
+
if move.castle?
|
21
|
+
apply_castle(move)
|
22
|
+
else
|
23
|
+
piece = find_piece(move)
|
24
|
+
other_piece = at(move.square) unless move.castle?
|
25
|
+
piece.move_to(move.square)
|
26
|
+
if other_piece # capture
|
27
|
+
unless move.capture?
|
28
|
+
raise "error: piece found at target (#{other_piece}) but move" +
|
29
|
+
" #{move} is not a capture"
|
30
|
+
end
|
31
|
+
@listener.capture(piece, other_piece)
|
32
|
+
remove_from_board(other_piece)
|
33
|
+
end
|
34
|
+
|
35
|
+
if move.pawn_promotion?
|
36
|
+
raise "error: trying to promote a non-pawn" unless piece.piece == :P
|
37
|
+
|
38
|
+
@listener.pawn_to_queen(piece)
|
39
|
+
color = piece.color
|
40
|
+
remove_from_board(piece) # will also trigger the listener
|
41
|
+
@pieces << Piece.create(self, color, :Q, move.square)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply_castle(move)
|
47
|
+
new_king_file = nil
|
48
|
+
new_rook_file = nil
|
49
|
+
king = @pieces.detect{ | p | p.piece == :K && p.color == move.color }
|
50
|
+
rook = nil
|
51
|
+
|
52
|
+
if move.queenside_castle?
|
53
|
+
new_king_file = king.square.square.file - 2
|
54
|
+
rook = @pieces.detect{ | p |
|
55
|
+
p.piece == :R && p.color == move.color && p.square.file == 0
|
56
|
+
}
|
57
|
+
new_rook_file = rook.square.file + 3
|
58
|
+
else # kingside castle
|
59
|
+
new_king_file = king.square.file + 2
|
60
|
+
rook = @pieces.detect{ | p |
|
61
|
+
p.piece == :R && p.color == move.color && p.square.file == 7
|
62
|
+
}
|
63
|
+
new_rook_file = rook.square.file - 2
|
64
|
+
end
|
65
|
+
|
66
|
+
king.move_to(Square.new(new_king_file, king.square.rank))
|
67
|
+
rook.move_to(Square.new(new_rook_file, rook.square.rank))
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_from_board(piece)
|
71
|
+
@pieces.delete(piece)
|
72
|
+
piece.move_off_board()
|
73
|
+
end
|
74
|
+
|
75
|
+
def empty_at?(square)
|
76
|
+
return at(square).nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
def at(square)
|
80
|
+
return @pieces.detect { | p | p.square == square }
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_piece(move)
|
84
|
+
candidates = @pieces.find_all { | p | p.could_perform_move(move) }
|
85
|
+
case candidates.length
|
86
|
+
when 0
|
87
|
+
raise "error: no pieces found for move #{move}"
|
88
|
+
when 1
|
89
|
+
return candidates[0]
|
90
|
+
else # Disambiguate using move's orig. rank or file
|
91
|
+
if move.from_rank_or_file.rank.nil? # file is non-nil
|
92
|
+
candidates = candidates.find_all { | p |
|
93
|
+
p.square.file == move.from_rank_or_file.file
|
94
|
+
}
|
95
|
+
else
|
96
|
+
candidates = candidates.find_all { | p |
|
97
|
+
p.square.rank == move.from_rank_or_file.rank
|
98
|
+
}
|
99
|
+
end
|
100
|
+
case candidates.length
|
101
|
+
when 0
|
102
|
+
raise "error: disambiguation found no pieces for #{move}"
|
103
|
+
when 1
|
104
|
+
return candidates[0]
|
105
|
+
else
|
106
|
+
raise "error: too many pieces match #{move}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bangkok/gamelistener'
|
4
|
+
require 'bangkok/board'
|
5
|
+
require 'bangkok/move'
|
6
|
+
|
7
|
+
# A ChessGame can read a chess play file and write a MIDI file created from
|
8
|
+
# the moves in the file.
|
9
|
+
class ChessGame
|
10
|
+
|
11
|
+
def initialize(listener = GameListener.new)
|
12
|
+
@listener = listener
|
13
|
+
end
|
14
|
+
|
15
|
+
# Read the chess game and turn it into Moves.
|
16
|
+
def read_moves(io)
|
17
|
+
game_text = read(io)
|
18
|
+
@moves = []
|
19
|
+
game_text.scan(/\d+\.\s+(\S+)\s+(\S+)/).each { | white, black |
|
20
|
+
@moves << Move.new(:white, white)
|
21
|
+
@moves << Move.new(:black, black) unless black == '1-0' || black == '0-1'
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Read the chess game. Set player names and return a string containing the
|
26
|
+
# chess moves ("1. f4 Nf6 2. Nf3 c5...").
|
27
|
+
def read(io)
|
28
|
+
game_text = ''
|
29
|
+
io.each { | line |
|
30
|
+
line.chomp!
|
31
|
+
case line
|
32
|
+
when /\[(.*)\]/ # New games starting (if multi-game file)
|
33
|
+
when /^\s*$/
|
34
|
+
else
|
35
|
+
game_text << ' '
|
36
|
+
game_text << line
|
37
|
+
end
|
38
|
+
}
|
39
|
+
game_text
|
40
|
+
end
|
41
|
+
|
42
|
+
# Writes a MIDI file.
|
43
|
+
def play(io)
|
44
|
+
@listener.start_game(io)
|
45
|
+
board = Board.new(@listener)
|
46
|
+
@moves.each { | move | board.apply(move) }
|
47
|
+
@listener.end_game
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
begin
|
2
|
+
require 'midilib'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
require_gem 'midilib'
|
6
|
+
end
|
7
|
+
include MIDI
|
8
|
+
|
9
|
+
# The pieces and board call methods on an instance of GameListener, which
|
10
|
+
# turns those events into MIDI events.
|
11
|
+
class GameListener
|
12
|
+
PIECE_MIDI_INFO = {}
|
13
|
+
PIECE_MIDI_INFO[:white] = {}
|
14
|
+
PIECE_MIDI_INFO[:black] = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def white(piece_sym, program_change)
|
18
|
+
GameListener::PIECE_MIDI_INFO[:white][piece_sym] = program_change
|
19
|
+
end
|
20
|
+
def black(piece_sym, program_change)
|
21
|
+
GameListener::PIECE_MIDI_INFO[:black][piece_sym] = program_change
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Fill in some default values, in case the user does not specify any new ones.
|
26
|
+
black :K, 0
|
27
|
+
black :Q, 8
|
28
|
+
black :R, 16
|
29
|
+
black :B, 24
|
30
|
+
black :N, 32
|
31
|
+
black :P, 4
|
32
|
+
white :K, 4
|
33
|
+
white :Q, 12
|
34
|
+
white :R, 20
|
35
|
+
white :B, 28
|
36
|
+
white :N, 36
|
37
|
+
white :P, 44
|
38
|
+
|
39
|
+
class GameListener
|
40
|
+
|
41
|
+
attr_reader :seq # MIDI::Sequence
|
42
|
+
|
43
|
+
# TODO remove this and specify it some other way
|
44
|
+
begin
|
45
|
+
require 'chess_config' # Fills in PIECE_MIDI_INFO
|
46
|
+
rescue LoadError
|
47
|
+
end
|
48
|
+
|
49
|
+
PIECE_NAMES = {
|
50
|
+
:P => "Pawn",
|
51
|
+
:R => "Rook",
|
52
|
+
:N => "Night",
|
53
|
+
:B => "Bishop",
|
54
|
+
:Q => "Queen",
|
55
|
+
:K => "King"
|
56
|
+
}
|
57
|
+
|
58
|
+
# Build notes to play for ranks
|
59
|
+
RANK_TO_NOTE = {}
|
60
|
+
RANK_TO_NOTE[:white] = [64, 66, 68, 69, 71, 73, 75, 76]
|
61
|
+
RANK_TO_NOTE[:black] = RANK_TO_NOTE[:white].reverse
|
62
|
+
|
63
|
+
# Build array that maps file number to pan value
|
64
|
+
FILE_TO_PAN = []
|
65
|
+
8.times { | i | FILE_TO_PAN << ((127.0/7.0) * i).to_i }
|
66
|
+
|
67
|
+
# Build array that maps rank number to volume value
|
68
|
+
RANK_TO_VOL = []
|
69
|
+
4.times { | i | RANK_TO_VOL << 10 + ((117.0 / 3.0) * i).to_i }
|
70
|
+
4.times { | i | RANK_TO_VOL << 127 - (((117.0 / 3.0) * i).to_i) }
|
71
|
+
|
72
|
+
def track_of(piece)
|
73
|
+
i = [:white, :black].index(piece.color) * 6 +
|
74
|
+
[:P, :R, :N, :B, :Q, :K].index(piece.piece)
|
75
|
+
@seq.tracks[i + 1] # 0'th track is temp track
|
76
|
+
end
|
77
|
+
|
78
|
+
def channel_of(piece)
|
79
|
+
[:white, :black].index(piece.color) * 8 +
|
80
|
+
[:P, :R, :N, :B, :Q, :K].index(piece.piece)
|
81
|
+
end
|
82
|
+
|
83
|
+
# --
|
84
|
+
# ================================================================
|
85
|
+
# Listener interface
|
86
|
+
# ================================================================
|
87
|
+
# ++
|
88
|
+
|
89
|
+
def start_game(io)
|
90
|
+
@io = io
|
91
|
+
@seq = Sequence.new
|
92
|
+
track = Track.new(@seq) # Tempo track
|
93
|
+
track.name = "Tempo track"
|
94
|
+
@seq.tracks << track
|
95
|
+
|
96
|
+
@cached_quarter_delta = @seq.note_to_delta('quarter')
|
97
|
+
|
98
|
+
create_tracks()
|
99
|
+
@time_from_start = 0
|
100
|
+
end
|
101
|
+
|
102
|
+
def end_game
|
103
|
+
# When we created events, we set their start times, not their delta times.
|
104
|
+
# Now is the time to fix that.
|
105
|
+
@seq.tracks.each { | t | t.recalc_delta_from_times }
|
106
|
+
@seq.write(io)
|
107
|
+
end
|
108
|
+
|
109
|
+
def move(piece, from, to)
|
110
|
+
if to.on_board?
|
111
|
+
midi_for_position(piece, from)
|
112
|
+
midi_for_position(piece, Square.new((from.file.to_f + to.file.to_f) / 2,
|
113
|
+
(from.rank.to_f + to.rank.to_f) / 2))
|
114
|
+
midi_for_position(piece, to)
|
115
|
+
end
|
116
|
+
# Do nothing if the piece moves off the board, because either capture()
|
117
|
+
# or pawn_to_queen() will be called.
|
118
|
+
end
|
119
|
+
|
120
|
+
def capture(attacker, loser)
|
121
|
+
end
|
122
|
+
|
123
|
+
def check
|
124
|
+
end
|
125
|
+
|
126
|
+
def checkmate
|
127
|
+
end
|
128
|
+
|
129
|
+
def pawn_to_queen(pawn)
|
130
|
+
end
|
131
|
+
|
132
|
+
# --
|
133
|
+
# ================================================================
|
134
|
+
# End of listener interface
|
135
|
+
# ================================================================
|
136
|
+
# ++
|
137
|
+
|
138
|
+
def create_tracks
|
139
|
+
[:white, :black].each_with_index { | color, chan_base_offset |
|
140
|
+
[:P, :R, :N, :B, :Q, :K].each_with_index { | piece_sym, chan_offset |
|
141
|
+
track = Track.new(@seq)
|
142
|
+
@seq.tracks << track
|
143
|
+
|
144
|
+
track.name = "#{color.to_s.capitalize} #{PIECE_NAMES[piece_sym]}"
|
145
|
+
|
146
|
+
program_num = PIECE_MIDI_INFO[color][piece_sym]
|
147
|
+
track.instrument = GM_PATCH_NAMES[program_num]
|
148
|
+
track.events << ProgramChange.new(chan_base_offset * 8 + chan_offset,
|
149
|
+
program_num)
|
150
|
+
}
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def midi_for_position(piece, square)
|
155
|
+
track = track_of(piece)
|
156
|
+
channel = channel_of(piece)
|
157
|
+
|
158
|
+
# pan
|
159
|
+
e = Controller.new(channel, CC_PAN, FILE_TO_PAN[square.file])
|
160
|
+
e.time_from_start = @time_from_start
|
161
|
+
track.events << e
|
162
|
+
|
163
|
+
# volume
|
164
|
+
e = Controller.new(channel, CC_VOLUME, RANK_TO_VOL[square.rank])
|
165
|
+
e.time_from_start = @time_from_start
|
166
|
+
track.events << e
|
167
|
+
|
168
|
+
# note on and off
|
169
|
+
note = RANK_TO_NOTE[piece.color][square.rank]
|
170
|
+
e = NoteOnEvent.new(channel, note, 127)
|
171
|
+
e.time_from_start = @time_from_start
|
172
|
+
track.events << e
|
173
|
+
|
174
|
+
e = NoteOffEvent.new(channel, note, 127)
|
175
|
+
e.time_from_start = @time_from_start + @cached_quarter_delta
|
176
|
+
track.events << e
|
177
|
+
|
178
|
+
@time_from_start = e.time_from_start
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|