n65 0.5.0 → 1.0.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +28 -0
  4. data/.gitignore +1 -1
  5. data/.rubocop.yml +125 -0
  6. data/Gemfile +3 -1
  7. data/README.md +3 -20
  8. data/Rakefile +15 -1
  9. data/bin/n65 +2 -0
  10. data/data/opcodes.yaml +39 -39
  11. data/everdrive_transfer/everdrive.rb +175 -0
  12. data/examples/pulse_chord.asm +1 -1
  13. data/examples/scales.asm +182 -0
  14. data/lib/n65/directives/ascii.rb +4 -19
  15. data/lib/n65/directives/bytes.rb +20 -35
  16. data/lib/n65/directives/dw.rb +22 -36
  17. data/lib/n65/directives/enter_scope.rb +14 -30
  18. data/lib/n65/directives/exit_scope.rb +7 -17
  19. data/lib/n65/directives/inc.rb +14 -30
  20. data/lib/n65/directives/incbin.rb +6 -24
  21. data/lib/n65/directives/ines_header.rb +66 -27
  22. data/lib/n65/directives/label.rb +8 -21
  23. data/lib/n65/directives/org.rb +9 -19
  24. data/lib/n65/directives/segment.rb +5 -19
  25. data/lib/n65/directives/space.rb +6 -18
  26. data/lib/n65/front_end.rb +36 -39
  27. data/lib/n65/instruction.rb +123 -159
  28. data/lib/n65/instruction_base.rb +6 -18
  29. data/lib/n65/memory_space.rb +51 -71
  30. data/lib/n65/opcodes.rb +3 -5
  31. data/lib/n65/parser.rb +20 -38
  32. data/lib/n65/regexes.rb +20 -21
  33. data/lib/n65/symbol_table.rb +58 -89
  34. data/lib/n65/version.rb +3 -1
  35. data/lib/n65.rb +120 -121
  36. data/n65.gemspec +17 -12
  37. data/nes_lib/nes.sym +2 -2
  38. data/spec/.rubocop.yml +4 -0
  39. data/spec/assembler_spec.rb +84 -0
  40. data/spec/lib/n65/memory_space_spec.rb +147 -0
  41. data/spec/lib/n65/symbol_table_spec.rb +291 -0
  42. data/utils/opcode_table_to_yaml.rb +65 -67
  43. data.tar.gz.sig +0 -0
  44. metadata +84 -41
  45. metadata.gz.sig +0 -0
  46. data/examples/music_driver.asm +0 -202
  47. data/test/test_memory_space.rb +0 -82
  48. data/test/test_symbol_table.rb +0 -238
  49. data/utils/midi/Makefile +0 -3
  50. data/utils/midi/c_scale.mid +0 -0
  51. data/utils/midi/convert +0 -0
  52. data/utils/midi/guitar.mid +0 -0
  53. data/utils/midi/include/event.h +0 -93
  54. data/utils/midi/include/file.h +0 -57
  55. data/utils/midi/include/helpers.h +0 -14
  56. data/utils/midi/include/track.h +0 -45
  57. data/utils/midi/lil_melody.mid +0 -0
  58. data/utils/midi/mi_feabhra.mid +0 -0
  59. data/utils/midi/midi_to_nes.rb +0 -204
  60. data/utils/midi/source/convert.cpp +0 -16
  61. data/utils/midi/source/event.cpp +0 -96
  62. data/utils/midi/source/file.cpp +0 -37
  63. data/utils/midi/source/helpers.cpp +0 -46
  64. data/utils/midi/source/track.cpp +0 -37
data/lib/n65.rb CHANGED
@@ -1,65 +1,82 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'n65/version'
2
4
  require_relative 'n65/symbol_table'
3
5
  require_relative 'n65/memory_space'
4
6
  require_relative 'n65/parser'
5
7
 
6
8
  module N65
7
-
8
9
  class Assembler
9
10
  attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
10
11
 
11
- ##### Custom exceptions
12
12
  class AddressOutOfRange < StandardError; end
13
13
  class InvalidSegment < StandardError; end
14
14
  class WriteOutOfBounds < StandardError; end
15
15
  class INESHeaderAlreadySet < StandardError; end
16
16
  class FileNotFound < StandardError; end
17
17
 
18
+ State = Data.define(:program_counter, :segment, :bank, :scope)
18
19
 
19
- ####
20
- ## Assemble from an asm file to a nes ROM
21
- def self.from_file(infile, outfile)
22
- fail(FileNotFound, infile) unless File.exists?(infile)
20
+ # Assemble from an asm file to a NES ROM
21
+ # TODO: This really needs a logger instead of all these unless quiet conditions
22
+ def self.from_file(infile, options)
23
+ raise(FileNotFound, infile) unless File.exist?(infile)
23
24
 
24
- assembler = self.new
25
+ assembler = new
25
26
  program = File.read(infile)
27
+ output_file = options[:output_file]
26
28
 
27
- puts "Building #{infile}"
28
- ## Process each line in the file
29
- program.split(/\n/).each_with_index do |line, line_number|
29
+ puts "Building #{infile}" unless options[:quiet]
30
+ # Process each line in the file
31
+ program.each_line.each_with_index do |line, line_number|
30
32
  begin
31
33
  assembler.assemble_one_line(line)
32
34
  rescue StandardError => e
33
- STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
35
+ warn("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
34
36
  exit(1)
35
37
  end
36
- print '.'
38
+ print '.' unless options[:quiet]
37
39
  end
38
- puts
40
+ puts unless options[:quiet]
39
41
 
40
- ## Second pass to resolve any missing symbols.
41
- print "Second pass, resolving symbols..."
42
+ # Second pass to resolve any missing symbols.
43
+ print 'Second pass, resolving symbols...' unless options[:quiet]
42
44
  assembler.fulfill_promises
43
- puts " Done."
44
-
45
- ## Let's not export the symbol table to a file anymore
46
- ## Will add an option for this later.
47
- #print "Writing symbol table to #{outfile}.yaml..."
48
- #File.open("#{outfile}.yaml", 'w') do |fp|
49
- #fp.write(assembler.symbol_table.export_to_yaml)
50
- #end
51
- #puts "Done."
52
-
53
- ## For right now, let's just emit the first prog bank
54
- File.open(outfile, 'w') do |fp|
55
- fp.write(assembler.emit_binary_rom)
45
+ puts ' Done.' unless options[:quiet]
46
+
47
+ # Optionally write out a symbol map
48
+ if options[:write_symbol_table]
49
+ print "Writing symbol table to #{output_file}.yaml..." unless options[:quiet]
50
+ File.open("#{output_file}.yaml", 'w') do |fp|
51
+ fp.write(assembler.symbol_table.export_to_yaml)
52
+ end
53
+ puts 'Done.' unless options[:quiet]
56
54
  end
57
- puts "All Done :)"
58
- end
59
55
 
56
+ # Optionally write out cycle count for subroutines
57
+ if options[:cycle_count]
58
+ print "Writing subroutine cycle counts to #{output_file}.cycles.yaml..." unless options[:quiet]
59
+ File.open("#{output_file}.cycles.yaml", 'w') do |fp|
60
+ fp.write(assembler.symbol_table.export_cycle_count_yaml)
61
+ end
62
+ puts 'Done.' unless options[:quiet]
63
+ end
64
+
65
+ # Emit the complete binary ROM
66
+ rom = assembler.emit_binary_rom
67
+ File.open(output_file, 'w') do |fp|
68
+ fp.write(rom)
69
+ end
70
+
71
+ return if options[:quiet]
60
72
 
61
- ####
62
- ## Initialize with a bank 1 of prog space for starters
73
+ rom_size = rom.size
74
+ rom_size_hex = format('%x', rom_size)
75
+ assembler.print_bank_usage
76
+ puts "Total size: $#{rom_size_hex}, #{rom_size} bytes"
77
+ end
78
+
79
+ # Initialize with a bank 1 of prog space for starters
63
80
  def initialize
64
81
  @ines_header = nil
65
82
  @program_counter = 0x0
@@ -68,176 +85,158 @@ module N65
68
85
  @symbol_table = SymbolTable.new
69
86
  @promises = []
70
87
  @virtual_memory = {
71
- :prog => [MemorySpace.create_prog_rom],
72
- :char => []
88
+ prog: [MemorySpace.create_prog_rom],
89
+ char: []
73
90
  }
74
91
  end
75
92
 
76
-
77
- ####
78
- ## Return an object that contains the assembler's current state
93
+ # Return an object that contains the assembler's current state
79
94
  def get_current_state
80
95
  saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
81
96
  saved_scope = symbol_table.scope_stack.dup
82
- OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope)
97
+ State.new(saved_program_counter, saved_segment, saved_bank, saved_scope)
83
98
  end
84
99
 
85
-
86
- ####
87
- ## Set the current state from an OpenStruct
88
- def set_current_state(struct)
89
- @program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
90
- symbol_table.scope_stack = struct.scope.dup
100
+ # Set the current state
101
+ def set_current_state(state)
102
+ @program_counter, @current_segment, @current_bank = state.program_counter, state.segment, state.bank
103
+ symbol_table.scope_stack = state.scope.dup
91
104
  end
92
105
 
93
-
94
- ####
95
- ## This is the main assemble method, it parses one line into an object
96
- ## which when given a reference to this assembler, controls the assembler
97
- ## itself through public methods, executing assembler directives, and
98
- ## emitting bytes into our virtual memory spaces. Empty lines or lines
99
- ## with only comments parse to nil, and we just ignore them.
106
+ # This is the main assemble method, it parses one line into an object
107
+ # which when given a reference to this assembler, controls the assembler
108
+ # itself through public methods, executing assembler directives, and
109
+ # emitting bytes into our virtual memory spaces. Empty lines or lines
110
+ # with only comments parse to nil, and we just ignore them.
100
111
  def assemble_one_line(line)
101
112
  parsed_object = Parser.parse(line)
113
+ return if parsed_object.nil?
102
114
 
103
- unless parsed_object.nil?
104
- exec_result = parsed_object.exec(self)
115
+ exec_result = parsed_object.exec(self)
105
116
 
106
- ## If we have returned a promise save it for the second pass
107
- @promises << exec_result if exec_result.kind_of?(Proc)
117
+ # TODO: I could perhaps keep a tally of cycles used per top level scope here
118
+ if parsed_object.respond_to?(:cycles)
119
+ @symbol_table.add_cycles(parsed_object.cycles)
108
120
  end
109
- end
110
121
 
122
+ # If we have returned a promise save it for the second pass
123
+ @promises << exec_result if exec_result.is_a?(Proc)
124
+ end
111
125
 
112
- ####
113
- ## This will empty out our promise queue and try to fullfil operations
114
- ## that required an undefined symbol when first encountered.
115
- def fulfill_promises
116
- while promise = @promises.pop
117
- promise.call
126
+ # Assemble the given string
127
+ def assemble_string(string)
128
+ string.each_line do |line|
129
+ assemble_one_line(line)
118
130
  end
131
+ fulfill_promises
132
+ self
119
133
  end
120
134
 
135
+ # This will empty out our promise queue and try to fullfil operations
136
+ # that required an undefined symbol when first encountered.
137
+ def fulfill_promises
138
+ @promises.pop.call while @promises.any?
139
+ end
121
140
 
122
- ####
123
- ## This rewinds the state of the assembler, so a promise can be
124
- ## executed with a previous state, for example if we can't resolve
125
- ## a symbol right now, and want to try during the second pass
141
+ # This rewinds the state of the assembler, so a promise can be
142
+ # executed with a previous state, for example if we can't resolve
143
+ # a symbol right now, and want to try during the second pass
126
144
  def with_saved_state(&block)
127
- ## Save the current state of the assembler
128
- old_state = get_current_state
129
-
145
+ saved_state = get_current_state
130
146
  lambda do
131
-
132
- ## Set the assembler state back to the old state and run the block like that
133
- set_current_state(old_state)
147
+ set_current_state(saved_state)
134
148
  block.call(self)
135
149
  end
136
150
  end
137
151
 
138
-
139
- ####
140
- ## Write to memory space. Typically, we are going to want to write
141
- ## to the location of the current PC, current segment, and current bank.
142
- ## Bounds check is inside MemorySpace#write
152
+ # Write to memory space. Typically, we are going to want to write
153
+ # to the location of the current PC, current segment, and current bank.
143
154
  def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
144
155
  memory_space = get_virtual_memory_space(segment, bank)
145
156
  memory_space.write(pc, bytes)
146
157
  @program_counter += bytes.size
147
158
  end
148
159
 
149
-
150
- ####
151
- ## Set the iNES header
160
+ # Set the iNES header
152
161
  def set_ines_header(ines_header)
153
- fail(INESHeaderAlreadySet) unless @ines_header.nil?
162
+ raise(INESHeaderAlreadySet) unless @ines_header.nil?
163
+
154
164
  @ines_header = ines_header
155
165
  end
156
166
 
157
-
158
- ####
159
- ## Set the program counter
167
+ # Set the program counter
160
168
  def program_counter=(address)
161
- fail(AddressOutOfRange) unless address_within_range?(address)
169
+ raise(AddressOutOfRange) unless address_within_range?(address)
170
+
162
171
  @program_counter = address
163
172
  end
164
173
 
165
-
166
- ####
167
- ## Set the current segment, prog or char.
174
+ # Set the current segment, prog or char.
168
175
  def current_segment=(segment)
169
176
  segment = segment.to_sym
170
- unless valid_segment?(segment)
171
- fail(InvalidSegment, "#{segment} is not a valid segment. Try prog or char")
172
- end
177
+ raise(InvalidSegment, "#{segment} is not a valid segment. Try prog or char") unless valid_segment?(segment)
178
+
173
179
  @current_segment = segment
174
180
  end
175
181
 
176
-
177
- ####
178
- ## Set the current bank, create it if it does not exist
182
+ # Set the current bank, create it if it does not exist
179
183
  def current_bank=(bank_number)
180
184
  memory_space = get_virtual_memory_space(@current_segment, bank_number)
181
- if memory_space.nil?
182
- @virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
183
- end
185
+ @virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment) if memory_space.nil?
184
186
  @current_bank = bank_number
185
187
  end
186
188
 
187
-
188
- ####
189
- ## Emit a binary ROM
190
189
  def emit_binary_rom
191
190
  progs = @virtual_memory[:prog]
192
191
  chars = @virtual_memory[:char]
193
- puts "iNES Header"
194
- puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
195
- puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"
196
192
 
197
- rom_size = 0x10
198
- rom_size += MemorySpace::BankSizes[:prog] * progs.size
199
- rom_size += MemorySpace::BankSizes[:char] * chars.size
193
+ rom_size = 0x10
194
+ rom_size += MemorySpace::BANK_SIZES[:prog] * progs.size
195
+ rom_size += MemorySpace::BANK_SIZES[:char] * chars.size
200
196
 
201
- puts "= Output ROM will be #{rom_size} bytes"
202
197
  rom = MemorySpace.new(rom_size, :rom)
203
198
 
204
199
  offset = 0x0
205
200
  offset += rom.write(0x0, @ines_header.emit_bytes)
206
201
 
207
202
  progs.each do |prog|
208
- offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog]))
203
+ offset += rom.write(offset, prog.read(0x8000, MemorySpace::BANK_SIZES[:prog]))
209
204
  end
210
205
 
211
206
  chars.each do |char|
212
- offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char]))
207
+ offset += rom.write(offset, char.read(0x0, MemorySpace::BANK_SIZES[:char]))
213
208
  end
214
209
  rom.emit_bytes.pack('C*')
215
210
  end
216
211
 
212
+ # TODO: Use StringIO to build output
213
+ def print_bank_usage
214
+ puts
215
+ puts 'ROM Structure {'
216
+ puts ' iNES 1.0 Header: $10 bytes'
217
217
 
218
- private
218
+ @virtual_memory[:prog].each_with_index do |prog_rom, bank_number|
219
+ puts " PROG ROM bank #{bank_number}: #{prog_rom.usage_info}"
220
+ end
219
221
 
222
+ @virtual_memory[:char].each_with_index do |char_rom, bank_number|
223
+ puts " CHAR ROM bank #{bank_number}: #{char_rom.usage_info}"
224
+ end
225
+ puts '}'
226
+ end
227
+
228
+ private
220
229
 
221
- ####
222
- ## Get virtual memory space
223
230
  def get_virtual_memory_space(segment, bank_number)
224
231
  @virtual_memory[segment][bank_number]
225
232
  end
226
233
 
227
-
228
- ####
229
- ## Is this a 16-bit address within range?
230
234
  def address_within_range?(address)
231
235
  address >= 0 && address < 2**16
232
236
  end
233
237
 
234
-
235
- ####
236
- ## Is this a valid segment?
237
238
  def valid_segment?(segment)
238
- [:prog, :char].include?(segment)
239
+ %i[prog char].include?(segment)
239
240
  end
240
-
241
241
  end
242
-
243
242
  end
data/n65.gemspec CHANGED
@@ -1,23 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'n65/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "n65"
8
+ spec.name = 'n65'
8
9
  spec.version = N65::VERSION
9
- spec.authors = ["Safiire"]
10
- spec.email = ["safiire@irkenkitties.com"]
11
- spec.summary = %q{An NES assembler for the 6502 microprocessor written in Ruby}
12
- spec.description = %q{An NES assembler for the 6502 microprocessor written in Ruby}
13
- spec.homepage = "http://github.com/safiire/n65"
14
- spec.license = "GPL2"
10
+ spec.authors = ['Safiire']
11
+ spec.email = ['safiire@irkenkitties.com']
12
+ spec.summary = 'An NES assembler for the 6502 microprocessor'
13
+ spec.description = 'An NES assembler for the 6502 microprocessor in pure Ruby'
14
+ spec.homepage = 'http://github.com/safiire/n65'
15
+ spec.license = 'GPL-2.0-or-later'
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0")
17
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.required_ruby_version = '>= 3.2.0'
20
23
 
21
- spec.add_development_dependency "bundler", "~> 1.7"
22
- spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'rspec'
27
+ spec.add_development_dependency 'rubocop'
23
28
  end
data/nes_lib/nes.sym CHANGED
@@ -1,6 +1,6 @@
1
1
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2
2
  ; Let's start a little library for naming parts of memory in the NES
3
- ;
3
+ ;
4
4
  ; Including this file will not emit any instructions or data into your binary
5
5
  ; It only defines symbols in the symbol table to name memory addresses
6
6
  ;
@@ -75,7 +75,7 @@
75
75
  .space controller2 1 ; $4017
76
76
  .scope mapper
77
77
  .org $8000
78
- .space unrom 1
78
+ .space unrom 1
79
79
  .org $8000
80
80
  .space cnrom 1
81
81
  .org $8000
data/spec/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ Metrics/BlockLength:
4
+ Enabled: false
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha2'
4
+ require_relative '../lib/n65'
5
+
6
+ RSpec.describe(N65::Assembler) do
7
+ describe 'assembling example files' do
8
+ let(:examples) do
9
+ File.join(__dir__, '..', 'examples')
10
+ end
11
+
12
+ let(:source) do
13
+ File.read(filename)
14
+ end
15
+
16
+ let(:assembler) do
17
+ described_class.new
18
+ end
19
+
20
+ let(:binary) do
21
+ Dir.chdir(examples) do
22
+ assembler.assemble_string(source).emit_binary_rom
23
+ end
24
+ end
25
+
26
+ let(:sha256) do
27
+ Digest::SHA256.hexdigest(binary)
28
+ end
29
+
30
+ context 'when assembling beep.asm' do
31
+ let(:filename) { 'beep.asm' }
32
+ let(:expected) { 'afce4dd85323d86dd6b2f8e4c9e7d704607b0480c3cf087bd8f66b37b8ddf269' }
33
+
34
+ it 'have the correct sha256 digest' do
35
+ expect(sha256).to eq(expected)
36
+ end
37
+ end
38
+
39
+ context 'when assembling demo.asm' do
40
+ let(:filename) { 'demo.asm' }
41
+ let(:expected) { 'ff687730a0a4022519b177c1cd92f18eaafaa2e9606f9a579371e9c62ffcb726' }
42
+
43
+ it 'have the correct sha256 digest' do
44
+ expect(sha256).to eq(expected)
45
+ end
46
+ end
47
+
48
+ context 'when assembling scales.asm' do
49
+ let(:filename) { 'scales.asm' }
50
+ let(:expected) { '2c6c586f3a9d35212a1a02e064a8bb71ec26ac857ed6572f217c10161f2de102' }
51
+
52
+ it 'have the correct sha256 digest' do
53
+ expect(sha256).to eq(expected)
54
+ end
55
+ end
56
+
57
+ context 'when assembling pulse_chord.asm' do
58
+ let(:filename) { 'pulse_chord.asm' }
59
+ let(:expected) { '77712d504edbe9b30261939a9d72f51e14ee3029da04287b879dffbb36eb9b12' }
60
+
61
+ it 'have the correct sha256 digest' do
62
+ expect(sha256).to eq(expected)
63
+ end
64
+ end
65
+
66
+ context 'when assembling noise.asm' do
67
+ let(:filename) { 'noise.asm' }
68
+ let(:expected) { 'ea790d892ecd2bf76f711fbcb8a2f8e9b4dbaa5dffb56b691924c81666d23673' }
69
+
70
+ it 'have the correct sha256 digest' do
71
+ expect(sha256).to eq(expected)
72
+ end
73
+ end
74
+
75
+ context 'when assembling mario2.asm' do
76
+ let(:filename) { 'mario2.asm' }
77
+ let(:expected) { '0974b799eac7ffb8835fcbd518a5cdeeec734d5d9476cd11cb6b75491c6ef488' }
78
+
79
+ it 'have the correct sha256 digest' do
80
+ expect(sha256).to eq(expected)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../lib/n65/memory_space'
4
+
5
+ RSpec.describe(N65::MemorySpace) do
6
+ describe '.new' do
7
+ context 'when provided a size and type' do
8
+ let(:bank) { described_class.new(size, type) }
9
+ let(:size) { 0x100 }
10
+ let(:type) { :prog }
11
+
12
+ it 'returns a zeroed bank' do
13
+ expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
14
+ end
15
+
16
+ it 'returns the requested sized bank' do
17
+ expect(bank.emit_bytes.size).to be(size)
18
+ end
19
+ end
20
+ end
21
+
22
+ describe '.create_prog_rom' do
23
+ context 'when creating a new prog rom' do
24
+ let(:bank) { described_class.create_prog_rom }
25
+
26
+ it 'returns a zeroed bank' do
27
+ expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
28
+ end
29
+
30
+ it 'returns the correct sized bank' do
31
+ expect(bank.emit_bytes.size).to eq(0x4000)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '.create_char_rom' do
37
+ context 'when creating a new prog rom' do
38
+ let(:bank) { described_class.create_char_rom }
39
+
40
+ it 'returns a zeroed bank' do
41
+ expect(bank.emit_bytes.all?(&:zero?)).to eq(true)
42
+ end
43
+
44
+ it 'returns the correct sized bank' do
45
+ expect(bank.emit_bytes.size).to eq(0x2000)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe '#read prog rom' do
51
+ let(:address) { 0xC100 }
52
+ let(:mirroed_address) { 0x8100 }
53
+ let(:bank) { described_class.create_prog_rom }
54
+ let(:data) { 'hi there'.bytes }
55
+
56
+ before { bank.write(address, data) }
57
+
58
+ context 'when reading from the bank' do
59
+ it 'can read back from the 0xC000 base address' do
60
+ expect(bank.read(address, data.size)).to eq(data)
61
+ end
62
+
63
+ it 'can read back from the 0x8000 mirrored address' do
64
+ expect(bank.read(mirroed_address, data.size)).to eq(data)
65
+ end
66
+ end
67
+
68
+ context 'when attempting to read out of bounds' do
69
+ it 'throws an error' do
70
+ expect { bank.read(0x0, 0x10) }.to raise_error(described_class::AccessOutsideProgRom)
71
+ end
72
+
73
+ it 'throws an error' do
74
+ expect { bank.read(0xffff, 0x10) }.to raise_error(described_class::AccessOutsideProgRom)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe '#read char rom' do
80
+ let(:address) { 0x100 }
81
+ let(:bank) { described_class.create_char_rom }
82
+ let(:data) { 'hi there'.bytes }
83
+
84
+ before { bank.write(address, data) }
85
+
86
+ context 'when reading from the bank' do
87
+ it 'can read from the bank' do
88
+ expect(bank.read(address, data.size)).to eq(data)
89
+ end
90
+ end
91
+
92
+ context 'when attempting to read out of bounds' do
93
+ it 'throws an error' do
94
+ expect { bank.read(0x2000, 0x10) }.to raise_error(described_class::AccessOutsideCharRom)
95
+ end
96
+ end
97
+ end
98
+
99
+ describe '#write prog rom' do
100
+ let(:address) { 0xC100 }
101
+ let(:mirroed_address) { 0x8100 }
102
+ let(:bank) { described_class.create_prog_rom }
103
+ let(:data) { 'hi there'.bytes }
104
+
105
+ before { bank.write(mirroed_address, data) }
106
+
107
+ context 'when reading from the bank' do
108
+ it 'can read back from the 0xC000 base address' do
109
+ expect(bank.read(address, data.size)).to eq(data)
110
+ end
111
+
112
+ it 'can read back from the 0x8000 mirrored address' do
113
+ expect(bank.read(mirroed_address, data.size)).to eq(data)
114
+ end
115
+ end
116
+
117
+ context 'when attempting to write out of bounds' do
118
+ it 'throws an error' do
119
+ expect { bank.write(0x0, data) }.to raise_error(described_class::AccessOutsideProgRom)
120
+ end
121
+
122
+ it 'throws an error' do
123
+ expect { bank.write(0xffff, data) }.to raise_error(described_class::AccessOutsideProgRom)
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '#write char rom' do
129
+ let(:address) { 0x100 }
130
+ let(:bank) { described_class.create_char_rom }
131
+ let(:data) { 'hi there'.bytes }
132
+
133
+ before { bank.write(address, data) }
134
+
135
+ context 'when reading from the bank' do
136
+ it 'can read from the bank' do
137
+ expect(bank.read(address, data.size)).to eq(data)
138
+ end
139
+ end
140
+
141
+ context 'when attempting to write out of bounds' do
142
+ it 'throws an error' do
143
+ expect { bank.write(0x2000, data) }.to raise_error(described_class::AccessOutsideCharRom)
144
+ end
145
+ end
146
+ end
147
+ end