badline 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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +40 -0
  4. data/exe/badline +55 -0
  5. data/lib/badline/address_bus.rb +206 -0
  6. data/lib/badline/addressable.rb +61 -0
  7. data/lib/badline/cartridge/magic_desk.rb +23 -0
  8. data/lib/badline/cartridge/ocean.rb +33 -0
  9. data/lib/badline/cartridge/standard.rb +31 -0
  10. data/lib/badline/cartridge.rb +75 -0
  11. data/lib/badline/chrout_trap.rb +39 -0
  12. data/lib/badline/cia/timer.rb +122 -0
  13. data/lib/badline/cia.rb +189 -0
  14. data/lib/badline/color_memory.rb +16 -0
  15. data/lib/badline/computer.rb +100 -0
  16. data/lib/badline/control_ports.rb +24 -0
  17. data/lib/badline/cpu.rb +239 -0
  18. data/lib/badline/cycleable.rb +35 -0
  19. data/lib/badline/gui/application.rb +94 -0
  20. data/lib/badline/gui/joy_map.rb +19 -0
  21. data/lib/badline/gui/key_map.rb +34 -0
  22. data/lib/badline/gui/palette.rb +35 -0
  23. data/lib/badline/gui/pane.rb +35 -0
  24. data/lib/badline/gui/screen_pane.rb +50 -0
  25. data/lib/badline/gui/window.rb +46 -0
  26. data/lib/badline/gui.rb +11 -0
  27. data/lib/badline/instruction.rb +334 -0
  28. data/lib/badline/instruction_set/arithmetic.rb +119 -0
  29. data/lib/badline/instruction_set/bitwise.rb +131 -0
  30. data/lib/badline/instruction_set/branch.rb +78 -0
  31. data/lib/badline/instruction_set/flag.rb +63 -0
  32. data/lib/badline/instruction_set/illegal.rb +278 -0
  33. data/lib/badline/instruction_set/inc_dec.rb +71 -0
  34. data/lib/badline/instruction_set/stack.rb +104 -0
  35. data/lib/badline/instruction_set/transfer.rb +137 -0
  36. data/lib/badline/instruction_set.rb +77 -0
  37. data/lib/badline/integer_helper.rb +39 -0
  38. data/lib/badline/joystick.rb +25 -0
  39. data/lib/badline/kernal_trap/file.rb +54 -0
  40. data/lib/badline/kernal_trap/load.rb +63 -0
  41. data/lib/badline/kernal_trap/save.rb +42 -0
  42. data/lib/badline/kernal_trap.rb +5 -0
  43. data/lib/badline/keyboard.rb +58 -0
  44. data/lib/badline/keyboard_buffer.rb +33 -0
  45. data/lib/badline/media.rb +59 -0
  46. data/lib/badline/memory.rb +43 -0
  47. data/lib/badline/rom.rb +23 -0
  48. data/lib/badline/roms/README +18 -0
  49. data/lib/badline/roms/basic.rom +0 -0
  50. data/lib/badline/roms/character.rom +0 -0
  51. data/lib/badline/roms/kernal.rom +0 -0
  52. data/lib/badline/sid.rb +25 -0
  53. data/lib/badline/status.rb +56 -0
  54. data/lib/badline/storage/crt_file.rb +53 -0
  55. data/lib/badline/storage/d64_image.rb +21 -0
  56. data/lib/badline/storage/d71_image.rb +13 -0
  57. data/lib/badline/storage/d81_image.rb +14 -0
  58. data/lib/badline/storage/disk_image.rb +71 -0
  59. data/lib/badline/storage/host_directory.rb +49 -0
  60. data/lib/badline/storage/p00.rb +24 -0
  61. data/lib/badline/storage.rb +28 -0
  62. data/lib/badline/time_of_day.rb +101 -0
  63. data/lib/badline/traps.rb +15 -0
  64. data/lib/badline/version.rb +5 -0
  65. data/lib/badline/vic/bank.rb +65 -0
  66. data/lib/badline/vic/display_state.rb +78 -0
  67. data/lib/badline/vic/graphics_mode.rb +139 -0
  68. data/lib/badline/vic/registers.rb +170 -0
  69. data/lib/badline/vic/sequencer.rb +237 -0
  70. data/lib/badline/vic/sprite.rb +121 -0
  71. data/lib/badline/vic/sprites.rb +112 -0
  72. data/lib/badline/vic.rb +192 -0
  73. data/lib/badline.rb +29 -0
  74. metadata +131 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KeyboardBuffer
5
+ ADDRESS = 0x0277
6
+ COUNT = 0xc6
7
+ CAPACITY = 10
8
+
9
+ def type_text(text)
10
+ codes = text.bytes.map { |b| ascii_to_petscii(b) }
11
+ on_init { @pending_keys = codes }
12
+ end
13
+
14
+ private
15
+
16
+ def feed_keyboard
17
+ return unless ram.peek(COUNT).zero?
18
+
19
+ chunk = @pending_keys.shift(CAPACITY)
20
+ ram.write(ADDRESS, chunk)
21
+ ram.poke(COUNT, chunk.length)
22
+ @pending_keys = nil if @pending_keys.empty?
23
+ end
24
+
25
+ def ascii_to_petscii(byte)
26
+ case byte
27
+ when 0x61..0x7a then byte - 0x20
28
+ when 0x41..0x5a then byte + 0x80
29
+ else byte
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ AUTOSTART = %(lO"*",8,1\rrun\r)
6
+ BASIC_START = 0x0801
7
+
8
+ DISK_TYPES = {
9
+ ".d64" => Storage::D64Image,
10
+ ".d71" => Storage::D71Image,
11
+ ".d81" => Storage::D81Image
12
+ }.freeze
13
+
14
+ class << self
15
+ def attach(computer, path, autostart: true)
16
+ if File.directory?(path)
17
+ computer.mount(Storage::HostDirectory.new(path))
18
+ "Mounted #{path} as device 8"
19
+ elsif File.extname(path).downcase == ".crt"
20
+ attach_cartridge(computer, path)
21
+ elsif (image = DISK_TYPES[File.extname(path).downcase])
22
+ attach_disk(computer, image.new(path), path, autostart:)
23
+ else
24
+ attach_prg(computer, path, autostart:)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def attach_cartridge(computer, path)
31
+ computer.attach_cartridge(Cartridge.from_file(path))
32
+ "Attached cartridge #{path}"
33
+ end
34
+
35
+ def attach_disk(computer, image, path, autostart:)
36
+ computer.mount(image)
37
+ computer.type_text(AUTOSTART) if autostart
38
+ "Mounted #{path} as device 8"
39
+ end
40
+
41
+ def attach_prg(computer, path, autostart:)
42
+ bytes = File.binread(path).bytes
43
+ bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
44
+ computer.on_init { start_prg(computer, bytes, autostart:) }
45
+ "Loading #{path}"
46
+ end
47
+
48
+ def start_prg(computer, data, autostart:)
49
+ load_addr = computer.load_prg(data)
50
+ # Run only makes sense for programs at BASIC start.
51
+ return unless autostart && load_addr == BASIC_START
52
+
53
+ end_addr = load_addr + data.length - 2
54
+ computer.ram.write(0x2d, [end_addr & 0xff, end_addr >> 8])
55
+ computer.type_text("run\r")
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Memory
5
+ include Addressable
6
+
7
+ def initialize(initial = [], length: 2**16, start: 0)
8
+ addressable_at(start, length:)
9
+ @storage = zero_fill(initial)
10
+ end
11
+
12
+ def peek(addr)
13
+ @storage[index(addr)]
14
+ end
15
+
16
+ def poke(addr, value)
17
+ @storage[index(addr)] = value
18
+ value
19
+ end
20
+
21
+ def read(addr, length)
22
+ (addr...(addr + length)).to_a.map { |a| peek(a) }
23
+ end
24
+
25
+ def write(addr, bytes)
26
+ Array(bytes).each_with_index { |b, i| poke(addr + i, b) }
27
+ end
28
+
29
+ private
30
+
31
+ def blank_value
32
+ 0
33
+ end
34
+
35
+ def zero_fill(initial)
36
+ array = initial.dup
37
+ 0.upto(length - 1) do |i|
38
+ array[i] ||= blank_value
39
+ end
40
+ array
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class ROM < Memory
5
+ class << self
6
+ def load(filename, start = 0x0)
7
+ data = File.read(file_path(filename)).bytes
8
+ new(data, length: data.length, start: start)
9
+ end
10
+
11
+ private
12
+
13
+ def file_path(filename)
14
+ File.join(File.dirname(__FILE__), "roms", filename)
15
+ end
16
+ end
17
+
18
+ def poke(_addr, _value)
19
+ raise ReadOnlyMemoryError
20
+ end
21
+ alias []= poke
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ Commodore 64 ROMs
2
+ =================
3
+
4
+ Downloaded from http://www.unusedino.de/ec64/technical2.html
5
+ Credited in turn to ftp://ftp.funet.fi/pub/cbm
6
+
7
+ basic.rom
8
+ ---------
9
+ Commodore 64 BASIC V2. The first and only revision.
10
+
11
+ kernal.rom
12
+ ----------
13
+ Commodore 64 KERNAL ROM Revision 3. The last revision, also used in
14
+ the C128's C64 mode.
15
+
16
+ character.rom
17
+ -------------
18
+ The character generator ROM.
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class SID
5
+ include Addressable
6
+
7
+ def initialize
8
+ addressable_at(0xd400, length: 2**10)
9
+ @registers = Memory.new(length: 2**5)
10
+ end
11
+
12
+ def peek(addr)
13
+ i = index(addr) % (2**5)
14
+ case i
15
+ when 0x1d..0x1f then 0xff # Unused memory
16
+ else @registers.peek(i)
17
+ end
18
+ end
19
+
20
+ def poke(addr, value)
21
+ i = index(addr) % (2**5)
22
+ @registers.poke(i, value)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Status
5
+ attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
6
+
7
+ def initialize(flags = [], value: 0x0)
8
+ @flags = flags
9
+
10
+ @bitmask = create_mask { |f| f.is_a?(Symbol) }
11
+ @low_mask = create_mask { |f| f.is_a?(Integer) && f.zero? }
12
+ @high_mask = create_mask { |f| f.is_a?(Integer) && f == 1 }
13
+
14
+ self.value = value
15
+
16
+ define_accessors!
17
+ end
18
+
19
+ def value=(new_value)
20
+ @value = (new_value | high_mask) & ~low_mask
21
+ end
22
+
23
+ private
24
+
25
+ def create_mask(&predicate)
26
+ flags.each.with_index.inject(0) do |mask, (flag, i)|
27
+ mask + (predicate.call(flag) ? 1 << i : 0)
28
+ end
29
+ end
30
+
31
+ def define_accessors!
32
+ flags.each_with_index do |name, i|
33
+ next unless name.is_a?(Symbol)
34
+
35
+ mask = 1 << i
36
+ define_singleton_method("#{name}=") do |enabled|
37
+ update(mask, enabled)
38
+ end
39
+ define_singleton_method("#{name}?") do
40
+ !@value.nobits?(mask)
41
+ end
42
+ define_singleton_method(name) do
43
+ @value.nobits?(mask) ? 0 : 1
44
+ end
45
+ end
46
+ end
47
+
48
+ def update(mask, enabled)
49
+ self.value = if enabled && enabled != 0
50
+ value | mask
51
+ else
52
+ value & ~mask
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class CRTFile
6
+ class FormatError < StandardError; end
7
+
8
+ SIGNATURE = "C64 CARTRIDGE ".b
9
+ CHIP_SIGNATURE = "CHIP".b
10
+ CHIP_HEADER_SIZE = 0x10
11
+ RAM_CHIP = 1
12
+
13
+ Chip = Data.define(:chip_type, :bank, :address, :data)
14
+
15
+ attr_reader :hardware_type, :exrom, :game, :name, :chips
16
+
17
+ def initialize(path)
18
+ parse(File.binread(path))
19
+ end
20
+
21
+ private
22
+
23
+ def parse(bytes)
24
+ raise FormatError, "Missing CRT signature" unless bytes.start_with?(SIGNATURE)
25
+
26
+ @hardware_type = bytes[0x16, 2].unpack1("n")
27
+ @exrom = bytes.getbyte(0x18)
28
+ @game = bytes.getbyte(0x19)
29
+ @name = bytes[0x20, 32].unpack1("Z*")
30
+ @chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
31
+ end
32
+
33
+ def parse_chips(bytes, offset)
34
+ chips = []
35
+ while offset < bytes.length
36
+ chip, offset = parse_chip(bytes, offset)
37
+ chips << chip
38
+ end
39
+ chips
40
+ end
41
+
42
+ def parse_chip(bytes, offset)
43
+ raise FormatError, "Bad CHIP packet at offset #{offset}" unless bytes[offset, 4] == CHIP_SIGNATURE
44
+
45
+ packet_length = bytes[offset + 4, 4].unpack1("N")
46
+ chip_type, bank, address, size = bytes[offset + 8, 8].unpack("n4")
47
+ data = chip_type == RAM_CHIP ? [] : bytes[offset + CHIP_HEADER_SIZE, size].bytes
48
+ [Chip.new(chip_type:, bank:, address:, data:),
49
+ offset + [packet_length, CHIP_HEADER_SIZE + data.length].max]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class D64Image < DiskImage
6
+ private
7
+
8
+ def directory_track = 18
9
+ def directory_sector = 1
10
+
11
+ def sectors_in(track)
12
+ case track
13
+ when 1..17 then 21
14
+ when 18..24 then 19
15
+ when 25..30 then 18
16
+ else 17
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class D71Image < D64Image
6
+ private
7
+
8
+ def sectors_in(track)
9
+ track > 35 ? super(track - 35) : super
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class D81Image < DiskImage
6
+ private
7
+
8
+ def directory_track = 40
9
+ def directory_sector = 3
10
+
11
+ def sectors_in(_track) = 40
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class DiskImage
6
+ SECTOR_SIZE = 256
7
+ ENTRY_SIZE = 32
8
+ ENTRIES_PER_SECTOR = 8
9
+ FILETYPE_PRG = 0x02
10
+ NAME_PADDING = 0xa0
11
+
12
+ def initialize(path)
13
+ @bytes = File.binread(path).bytes
14
+ end
15
+
16
+ def read_file(name)
17
+ pattern = Storage.matcher(name)
18
+ entry = entries.find { |e| pattern.match?(e[:name]) }
19
+ read_chain(entry[:track], entry[:sector]) if entry
20
+ end
21
+
22
+ private
23
+
24
+ def entries
25
+ @entries ||= each_sector(directory_track, directory_sector)
26
+ .flat_map { |data| parse_entries(data) }
27
+ end
28
+
29
+ def parse_entries(data)
30
+ (0...ENTRIES_PER_SECTOR).filter_map do |i|
31
+ entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
32
+ next unless (entry[2] & 0x07) == FILETYPE_PRG
33
+
34
+ { name: decode_name(entry[5, 16]),
35
+ track: entry[3],
36
+ sector: entry[4] }
37
+ end
38
+ end
39
+
40
+ def decode_name(bytes)
41
+ Storage.ascii(bytes.take_while { |b| b != NAME_PADDING }).downcase
42
+ end
43
+
44
+ def read_chain(track, sector)
45
+ each_sector(track, sector).flat_map do |data|
46
+ data[0].zero? ? data[2..data[1]] : data[2..]
47
+ end
48
+ end
49
+
50
+ def each_sector(track, sector)
51
+ return to_enum(:each_sector, track, sector) unless block_given?
52
+
53
+ visited = {}
54
+ while track != 0 && !visited[[track, sector]]
55
+ visited[[track, sector]] = true
56
+ data = sector_at(track, sector)
57
+ yield data
58
+ track, sector = data[0, 2]
59
+ end
60
+ end
61
+
62
+ def sector_at(track, sector)
63
+ @bytes[sector_offset(track, sector), SECTOR_SIZE]
64
+ end
65
+
66
+ def sector_offset(track, sector)
67
+ ((1...track).sum { |t| sectors_in(t) } + sector) * SECTOR_SIZE
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class HostDirectory
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def read_file(name)
11
+ entry = find(name)
12
+ return unless entry
13
+
14
+ bytes = File.binread(File.join(@path, entry[:file])).bytes
15
+ P00.wraps?(bytes) ? P00.data(bytes) : bytes
16
+ end
17
+
18
+ def write_file(name, bytes)
19
+ host_name = "#{name.downcase.tr('/', '_')}.prg"
20
+ File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
21
+ end
22
+
23
+ private
24
+
25
+ def find(name)
26
+ pattern = Storage.matcher(name)
27
+ entries.find { |e| pattern.match?(e[:name]) }
28
+ end
29
+
30
+ def entries
31
+ Dir.children(@path).sort.filter_map { |f| entry_for(f) }
32
+ end
33
+
34
+ def entry_for(file)
35
+ case File.extname(file).downcase
36
+ when ".prg"
37
+ { name: File.basename(file, ".*").downcase, file: }
38
+ when ".p00"
39
+ p00_entry(file)
40
+ end
41
+ end
42
+
43
+ def p00_entry(file)
44
+ header = File.binread(File.join(@path, file), P00::HEADER_SIZE).bytes
45
+ { name: P00.name(header), file: } if P00.wraps?(header)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ module P00
6
+ MAGIC = "C64File\x00".bytes.freeze
7
+ HEADER_SIZE = 26
8
+
9
+ class << self
10
+ def wraps?(bytes)
11
+ bytes[0, 8] == MAGIC
12
+ end
13
+
14
+ def name(bytes)
15
+ Storage.ascii(bytes[8, 16].take_while(&:positive?)).downcase
16
+ end
17
+
18
+ def data(bytes)
19
+ bytes[HEADER_SIZE..]
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/storage/p00"
4
+ require "badline/storage/host_directory"
5
+ require "badline/storage/disk_image"
6
+ require "badline/storage/d64_image"
7
+ require "badline/storage/d71_image"
8
+ require "badline/storage/d81_image"
9
+ require "badline/storage/crt_file"
10
+
11
+ module Badline
12
+ module Storage
13
+ class << self
14
+ # Folds shifted PETSCII letters to their ASCII equivalents.
15
+ def ascii(bytes)
16
+ bytes.map { |b| b.between?(0xc1, 0xda) ? b - 0x80 : b }.pack("C*")
17
+ end
18
+
19
+ # CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
20
+ def matcher(name)
21
+ escaped = Regexp.escape(name.downcase)
22
+ .gsub('\*', ".*")
23
+ .gsub('\?', ".")
24
+ Regexp.new("\\A#{escaped}\\z")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class TimeOfDay
5
+ include IntegerHelper
6
+
7
+ CLOCK_HZ = 985_248 # PAL only for now.
8
+
9
+ def initialize(clock_hz: CLOCK_HZ)
10
+ # The accumulator advances 10 per cycle, so a tenth of a second has
11
+ # passed when it reaches clock_hz. Integer math keeps it exact.
12
+ @cycles_per_tenth = clock_hz
13
+ @accumulator = 0
14
+ @clock = { tenths: 0, seconds: 0, minutes: 0, hours: 12, pm: false }
15
+ @alarm = { tenths: 0, seconds: 0, minutes: 0, hours: 0, pm: false }
16
+ @latch = nil
17
+ @stopped = false
18
+ end
19
+
20
+ def cycle!
21
+ return if @stopped
22
+
23
+ @accumulator += 10
24
+ return if @accumulator < @cycles_per_tenth
25
+
26
+ @accumulator -= @cycles_per_tenth
27
+ advance
28
+ yield if block_given? && alarm?
29
+ end
30
+
31
+ def tenths
32
+ value = (@latch || @clock)[:tenths]
33
+ @latch = nil
34
+ bcd(value)
35
+ end
36
+
37
+ def seconds
38
+ bcd((@latch || @clock)[:seconds])
39
+ end
40
+
41
+ def minutes
42
+ bcd((@latch || @clock)[:minutes])
43
+ end
44
+
45
+ def hours
46
+ @latch ||= @clock.dup
47
+ bcd(@latch[:hours]) | (@latch[:pm] ? 0x80 : 0)
48
+ end
49
+
50
+ def write(field, value, alarm:)
51
+ (alarm ? @alarm : @clock)[field] = bcd_to_i(value)
52
+ # Start the clock again when writing tenths.
53
+ resume if field == :tenths && !alarm
54
+ end
55
+
56
+ # 12-hour BCD value, bit 7 is the AM/PM flag.
57
+ def write_hours(value, alarm:)
58
+ target = alarm ? @alarm : @clock
59
+ target[:hours] = bcd_to_i(value & 0x7f)
60
+ target[:pm] = value.anybits?(0x80)
61
+ # Halt the clock when writing hours, so that it doesn't
62
+ # advance mid-update.
63
+ @stopped = true unless alarm
64
+ end
65
+
66
+ private
67
+
68
+ def resume
69
+ @stopped = false
70
+ @accumulator = 0
71
+ end
72
+
73
+ def advance
74
+ @clock[:tenths] += 1
75
+ return if @clock[:tenths] < 10
76
+
77
+ @clock[:tenths] = 0
78
+ @clock[:seconds] += 1
79
+ return if @clock[:seconds] < 60
80
+
81
+ @clock[:seconds] = 0
82
+ @clock[:minutes] += 1
83
+ return if @clock[:minutes] < 60
84
+
85
+ @clock[:minutes] = 0
86
+ advance_hour
87
+ end
88
+
89
+ def advance_hour
90
+ @clock[:hours] += 1
91
+ @clock[:pm] = !@clock[:pm] if @clock[:hours] == 12
92
+ @clock[:hours] = 1 if @clock[:hours] > 12
93
+ end
94
+
95
+ def alarm?
96
+ %i[tenths seconds minutes hours pm].all? do |field|
97
+ @clock[field] == @alarm[field]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Traps
5
+ def install_trap(addr, &handler)
6
+ (@traps ||= {})[addr] = handler
7
+ end
8
+
9
+ private
10
+
11
+ def run_traps
12
+ @traps[@program_counter]&.call if @traps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ VERSION = "0.1.0"
5
+ end