use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
const REG_SELECT_R: u16 = 0;
const REG_INVERT_MODE: u16 = 1;
const REG_SET_P_AND_S: u16 = 2;
const REG_INCREMENT_MODE: u16 = 3;
pub struct Mapper173 {
base: BaseMapper,
p: u8,
r: u8,
s: bool,
increment_mode: bool,
invert_mode: bool,
output: u8,
}
impl Mapper173 {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_chr_banking: true,
prg_bank_size_kb: 32,
chr_bank_size_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(32 * 1024);
base.configure_chr_banking(8 * 1024);
let mut mapper = Self {
base,
p: 0,
r: 0,
s: false,
increment_mode: false,
invert_mode: false,
output: 0,
};
mapper.apply_output_to_banks();
mapper
}
fn status_nibble(&self) -> u8 {
(((self.s ^ self.invert_mode) as u8) << 3) | (self.r & 0x07)
}
fn is_4100_masked_window(addr: u16) -> bool {
addr & 0xE100 == 0x4100
}
fn is_status_read_address(addr: u16) -> bool {
Self::is_4100_masked_window(addr) && (addr & 0x0003) == REG_SELECT_R
}
fn apply_output_to_banks(&mut self) {
self.base.select_prg_page(0, 0); let chr_bank = (self.output & 0x01) | ((!self.invert_mode as u8 & 0x01) << 1);
self.base.select_chr_page(0, chr_bank as i16);
}
}
impl Mapper for Mapper173 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn read_prg(&self, addr: u16) -> u8 {
if Self::is_status_read_address(addr) {
return self.status_nibble();
}
if (0x8000..=0xFFFF).contains(&addr) {
return self.base.read_prg_rom(addr);
}
0
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if Self::is_status_read_address(addr) {
return (open_bus & 0xF0) | self.status_nibble();
}
self.base
.read_prg_open_bus(addr, open_bus, |a| self.read_prg(a))
}
fn write_prg(&mut self, addr: u16, value: u8) {
if Self::is_4100_masked_window(addr) {
match addr & 0x0003 {
REG_SELECT_R => {
if self.increment_mode {
self.r = self.r.wrapping_add(1) & 0x07;
} else if self.invert_mode {
self.r = (!self.p) & 0x07;
} else {
self.r = self.p & 0x07;
}
}
REG_INVERT_MODE => {
self.invert_mode = value & 0x01 != 0;
self.apply_output_to_banks();
}
REG_SET_P_AND_S => {
self.s = value & 0x08 != 0;
self.p = value & 0x07;
}
REG_INCREMENT_MODE => {
self.increment_mode = value & 0x01 != 0;
}
_ => {}
}
return;
}
if (addr & 0x8000) != 0 {
self.output = self.r & 0x07;
self.apply_output_to_banks();
}
}
fn reset(&mut self) {
self.p = 0;
self.r = 0;
self.s = false;
self.increment_mode = false;
self.invert_mode = false;
self.output = 0;
self.apply_output_to_banks();
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![
self.p,
self.r,
self.s as u8,
self.increment_mode as u8,
self.invert_mode as u8,
self.output,
]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < 6 {
return;
}
self.p = data[0] & 0x07;
self.r = data[1] & 0x07;
self.s = data[2] & 0x01 != 0;
self.increment_mode = data[3] & 0x01 != 0;
self.invert_mode = data[4] & 0x01 != 0;
self.output = data[5] & 0x07;
self.apply_output_to_banks();
}
}
#[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_32K: usize = 1;
const CHR_BANKS_8K: usize = 4;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(32 * 1024, PRG_BANKS_32K);
let chr = banked_data(8 * 1024, CHR_BANKS_8K);
create_mapper(MapperContext::new_for_test(
173,
prg,
chr,
NametableLayout::Horizontal,
))
.expect("Mapper 173 must be creatable")
}
fn set_r(mapper: &mut dyn Mapper, p: u8, invert: bool, increment: bool, s: bool) {
mapper.write_prg(0x4102, ((s as u8) << 3) | (p & 0x07));
mapper.write_prg(0x4101, u8::from(invert));
mapper.write_prg(0x4103, u8::from(increment));
mapper.write_prg(0x4100, 0);
}
#[test]
fn mapper_173_is_registered_in_factory() {
let prg = banked_data(32 * 1024, PRG_BANKS_32K);
let chr = banked_data(8 * 1024, CHR_BANKS_8K);
let result = create_mapper(MapperContext::new_for_test(
173,
prg,
chr,
NametableLayout::Horizontal,
));
assert!(result.is_ok(), "Mapper 173 must be creatable via factory");
}
#[test]
fn power_on_chr_is_bank_2() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x0000),
2,
"power-on: invert=0 → CHR bank = (0&1)|(1<<1) = 2"
);
}
#[test]
fn invert_zero_gives_chr_a14_high() {
let mut mapper = make_mapper();
set_r(mapper.as_mut(), 0, false, false, false);
mapper.write_prg(0x8000, 0); assert_eq!(
mapper.read_chr(0x0000),
2,
"Invert=0 → CHR bank bit 1 = 1 (bank 2)"
);
}
#[test]
fn invert_one_gives_chr_a14_low() {
let mut mapper = make_mapper();
mapper.write_prg(0x4102, 0b000); mapper.write_prg(0x4101, 1); mapper.write_prg(0x4103, 0);
mapper.write_prg(0x4100, 0); mapper.write_prg(0x8000, 0);
assert_eq!(
mapper.read_chr(0x0000),
1,
"Invert=1 → CHR bank bit 1 = 0, bit 0 from output = 1"
);
}
#[test]
fn output_bit_0_selects_chr_bank_within_pair() {
let mut mapper = make_mapper();
mapper.write_prg(0x4101, 1);
mapper.write_prg(0x4102, 0b001); mapper.write_prg(0x4103, 0);
mapper.write_prg(0x4101, 1); mapper.write_prg(0x4100, 0); mapper.write_prg(0x8000, 0); assert_eq!(
mapper.read_chr(0x0000),
0,
"output=6, invert=1 → CHR bank 0"
);
mapper.write_prg(0x4102, 0b111); mapper.write_prg(0x4101, 1);
mapper.write_prg(0x4100, 0); mapper.write_prg(0x8000, 0); assert_eq!(
mapper.read_chr(0x0000),
0,
"output=0, invert=1 → CHR bank 0"
);
}
#[test]
fn writing_4101_immediately_updates_chr_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x4101, 0);
assert_eq!(
mapper.read_chr(0x0000),
2,
"invert=0 immediately → CHR bank 2"
);
mapper.write_prg(0x4101, 1);
assert_eq!(
mapper.read_chr(0x0000),
0,
"invert=1 immediately → CHR bank 0 (without $8000 write)"
);
}
#[test]
fn read_4100_returns_status_nibble() {
let mut mapper = make_mapper();
set_r(mapper.as_mut(), 0b101, false, false, true); assert_eq!(
mapper.read_prg(0x4100) & 0x0F,
0b1101,
"Status nibble: S XOR V=0 → 1, RRR=101"
);
}
#[test]
fn prg_is_fixed_32k_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "PRG always fixed to bank 0");
}
#[test]
fn snapshot_round_trips() {
let mut mapper = make_mapper();
set_r(mapper.as_mut(), 0b011, true, false, true);
mapper.write_prg(0x8000, 0);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_chr(0x0000),
mapper.read_chr(0x0000),
"CHR bank preserved by snapshot"
);
assert_eq!(
restored.read_prg(0x4100) & 0x0F,
mapper.read_prg(0x4100) & 0x0F,
"Status nibble preserved by snapshot"
);
}
#[test]
fn reset_clears_all_state() {
let mut mapper = make_mapper();
set_r(mapper.as_mut(), 0b111, true, true, true);
mapper.write_prg(0x8000, 0);
mapper.reset();
assert_eq!(mapper.read_prg(0x4100) & 0x0F, 0, "Status 0 after reset");
assert_eq!(
mapper.read_chr(0x0000),
2,
"CHR bank 2 after reset (invert=0 → !invert=1)"
);
}
}