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.
@@ -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
@@ -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")
@@ -0,0 +1,2 @@
1
+ # See the README file.
2
+ require 'bangkok/chessgame'
@@ -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