bangkok 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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