use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
const MAPPER_NUMBER: u16 = 230;
pub struct Mapper230 {
base: BaseMapper,
contra_mode: bool,
reg: u8,
hard_reset_pending: bool,
}
impl Mapper230 {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
prg_bank_size_kb: 16,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(16 * 1024);
let mut mapper = Self {
base,
contra_mode: false,
reg: 0,
hard_reset_pending: true,
};
mapper.reset();
mapper
}
fn apply_banks(&mut self) {
if self.contra_mode {
let lo = (self.reg & 0x07) as i16;
self.base.select_prg_page(0, lo);
self.base.select_prg_page(1, 7);
self.base.set_mirroring_hv(false); } else {
if self.reg & 0x20 != 0 {
let page = (self.reg & 0x1F) as i16 + 8;
self.base.select_prg_page(0, page);
self.base.select_prg_page(1, page);
} else {
let base_page = (self.reg & 0x1E) as i16 + 8;
self.base.select_prg_page(0, base_page);
self.base.select_prg_page(1, base_page + 1);
}
let vertical = self.reg & 0x40 != 0;
self.base.set_mirroring_hv(!vertical); }
}
}
impl Mapper for Mapper230 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
if (0x8000..=0xFFFF).contains(&addr) {
self.reg = value;
self.apply_banks();
}
}
fn initialize_ram(&mut self, mode: crate::nes::console::RamInitMode) {
self.base.initialize_ram(mode);
self.contra_mode = false;
self.hard_reset_pending = true;
}
fn reset(&mut self) {
self.contra_mode = if self.hard_reset_pending {
self.hard_reset_pending = false;
true
} else {
!self.contra_mode
};
self.reg = 0;
self.apply_banks();
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.base.banking_snapshot();
snap.push(self.reg);
snap.push(self.contra_mode as u8);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
let expected_banking_len = self.base.banking_snapshot().len();
if data.len() >= expected_banking_len + 2 {
self.base.restore_banking(&data[..expected_banking_len]);
self.reg = data[expected_banking_len];
self.contra_mode = data[expected_banking_len + 1] != 0;
self.apply_banks();
} else {
self.base.restore_banking(data);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 48;
fn create_mapper230(prg_rom: Vec<u8>) -> Mapper230 {
Mapper230::new(MapperContext::new_for_test(
MAPPER_NUMBER,
prg_rom,
vec![],
NametableLayout::Vertical,
))
}
fn make_mapper() -> Mapper230 {
create_mapper230(banked_data(16 * 1024, PRG_BANKS))
}
fn soft_reset(mapper: &mut Mapper230) {
mapper.reset();
}
fn hard_reset(mapper: &mut Mapper230) {
mapper.initialize_ram(crate::nes::console::RamInitMode::Zero);
mapper.reset();
}
#[test]
fn mapper_230_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(16 * 1024, PRG_BANKS),
vec![],
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 230 should be registered in factory");
}
#[test]
fn power_on_lower_window_is_bank_0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x8000),
0,
"$8000 window should power on to bank 0 (Contra mode)"
);
}
#[test]
fn power_on_upper_window_is_bank_7() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 window should power on to bank 7 (fixed in Contra mode)"
);
}
#[test]
fn power_on_mirroring_is_vertical() {
let mapper = make_mapper();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Power-on mirroring should be Vertical (Contra mode)"
);
}
#[test]
fn contra_mode_write_selects_lower_bank_bits_2_to_0() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 5);
assert_eq!(mapper.read_prg(0x8000), 5);
assert_eq!(mapper.read_prg(0xC000), 7);
}
#[test]
fn contra_mode_upper_bank_is_always_7_regardless_of_write() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xFF);
assert_eq!(
mapper.read_prg(0x8000),
7,
"Lower bank should be value & 0x07 = 7"
);
assert_eq!(mapper.read_prg(0xC000), 7, "Upper bank fixed at 7");
}
#[test]
fn contra_mode_write_bank_3_lower_window_bank_3_upper_still_7() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 3);
assert_eq!(mapper.read_prg(0x8000), 3);
assert_eq!(mapper.read_prg(0xC000), 7);
}
#[test]
fn contra_mode_mirroring_stays_vertical_after_write() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xFF); assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn after_first_soft_reset_lower_window_is_bank_8() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
assert_eq!(
mapper.read_prg(0x8000),
8,
"After soft reset lower window should be bank 8 (22-in-1 default)"
);
}
#[test]
fn after_first_soft_reset_upper_window_is_bank_9() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
assert_eq!(
mapper.read_prg(0xC000),
9,
"After soft reset upper window should be bank 9 (22-in-1 default)"
);
}
#[test]
fn after_first_soft_reset_mirroring_is_horizontal() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"22-in-1 mode default mirroring should be Horizontal"
);
}
#[test]
fn twenty_two_in_one_paired_mode_bit5_clear() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x02);
assert_eq!(mapper.read_prg(0x8000), 10, "Lower window bank 10");
assert_eq!(mapper.read_prg(0xC000), 11, "Upper window bank 11");
}
#[test]
fn twenty_two_in_one_single_page_mode_bit5_set() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x23);
assert_eq!(mapper.read_prg(0x8000), 11, "Both windows bank 11");
assert_eq!(mapper.read_prg(0xC000), 11, "Both windows bank 11");
}
#[test]
fn twenty_two_in_one_mirroring_bit6_zero_is_horizontal() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x00); assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn twenty_two_in_one_mirroring_bit6_one_is_vertical() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x40); assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn twenty_two_in_one_paired_mode_odd_value_aligns_to_even_pair() {
let mut mapper = make_mapper();
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x03);
assert_eq!(
mapper.read_prg(0x8000),
10,
"Odd value should align to even pair: bank 10"
);
assert_eq!(mapper.read_prg(0xC000), 11, "Upper bank 11");
}
#[test]
fn second_soft_reset_returns_to_contra_mode() {
let mut mapper = make_mapper();
soft_reset(&mut mapper); soft_reset(&mut mapper); assert_eq!(mapper.read_prg(0x8000), 0);
assert_eq!(mapper.read_prg(0xC000), 7);
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn hard_reset_restores_contra_mode() {
let mut mapper = make_mapper();
soft_reset(&mut mapper); mapper.write_prg(0x8000, 0x40); hard_reset(&mut mapper); assert_eq!(mapper.read_prg(0x8000), 0, "Hard reset lower bank 0");
assert_eq!(mapper.read_prg(0xC000), 7, "Hard reset upper bank 7");
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"Hard reset mirroring Vertical"
);
}
#[test]
fn write_in_contra_mode_does_not_affect_22in1_banks_after_next_reset() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x05); soft_reset(&mut mapper); assert_eq!(mapper.read_prg(0x8000), 8);
assert_eq!(mapper.read_prg(0xC000), 9);
}
#[test]
fn chr_ram_is_readable_and_writable() {
let mut mapper = make_mapper();
mapper.write_chr(0x0000, 0xAB);
mapper.write_chr(0x1FFF, 0xCD);
assert_eq!(mapper.read_chr(0x0000), 0xAB);
assert_eq!(mapper.read_chr(0x1FFF), 0xCD);
}
#[test]
fn snapshot_and_restore_in_contra_mode() {
let prg = banked_data(16 * 1024, PRG_BANKS);
let mut mapper = create_mapper230(prg.clone());
mapper.write_prg(0x8000, 3);
let snap = mapper.registers_snapshot();
let mut restored = create_mapper230(prg);
restored.restore_registers(&snap);
assert_eq!(restored.read_prg(0x8000), 3);
assert_eq!(restored.read_prg(0xC000), 7);
assert_eq!(restored.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn snapshot_and_restore_in_22in1_mode() {
let prg = banked_data(16 * 1024, PRG_BANKS);
let mut mapper = create_mapper230(prg.clone());
soft_reset(&mut mapper);
mapper.write_prg(0x8000, 0x04);
let snap = mapper.registers_snapshot();
let mut restored = create_mapper230(prg);
restored.restore_registers(&snap);
assert_eq!(restored.read_prg(0x8000), 12);
assert_eq!(restored.read_prg(0xC000), 13);
assert_eq!(restored.get_mirroring(), NametableLayout::Horizontal);
}
}