Skip to content

Commit ae0eb46

Browse files
committed
Merge branch 'tests'
2 parents afd957f + 1ca31c1 commit ae0eb46

File tree

9 files changed

+359
-0
lines changed

9 files changed

+359
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ tetris.lbl
99
tetris.map
1010
tetris.dbg
1111
release/*
12+
target

tests/Cargo.lock

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "gym-tests"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
gumdrop = "0.8.1"
8+
rusticnes-core = { git = "https://github.com/zeta0134/rusticnes-core.git", version = "0.2.0" }

tests/src/block.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#[derive(Debug, PartialEq)]
2+
pub enum Block {
3+
T, J, Z, O, S, L, I, Unknown(u8)
4+
}
5+
6+
impl From<u8> for Block {
7+
fn from(value: u8) -> Self {
8+
match value {
9+
0x2 => Block::T,
10+
0x7 => Block::J,
11+
0x8 => Block::Z,
12+
0xA => Block::O,
13+
0xB => Block::S,
14+
0xE => Block::L,
15+
0x12 => Block::I,
16+
_ => Block::Unknown(value),
17+
}
18+
}
19+
}

tests/src/labels.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::collections::HashMap;
2+
use std::sync::OnceLock;
3+
4+
fn parse_debug_line(line: &str) -> (String, u16) {
5+
let pairs: Vec<&str> = line.split(',').collect();
6+
7+
let mut name = String::new();
8+
let mut val = 0;
9+
10+
for pair in pairs {
11+
let key_val: Vec<&str> = pair.split('=').collect();
12+
if key_val.len() == 2 {
13+
let key = key_val[0].trim();
14+
let value = key_val[1].trim();
15+
16+
match key {
17+
"name" => name = value.trim_matches('"').to_string(),
18+
"val" => {
19+
if let Ok(hex_value) = u16::from_str_radix(value.trim_start_matches("0x"), 16) {
20+
val = hex_value;
21+
}
22+
}
23+
_ => {}
24+
}
25+
}
26+
}
27+
28+
(name, val)
29+
}
30+
31+
fn labels() -> &'static HashMap<String, u16> {
32+
static LABELS: OnceLock<HashMap<String, u16>> = OnceLock::new();
33+
LABELS.get_or_init(|| {
34+
let text = include_str!("../../tetris.dbg");
35+
let mut labels: HashMap<String, u16> = HashMap::new();
36+
37+
for line in text.lines() {
38+
let (name, val) = parse_debug_line(line);
39+
40+
if !name.is_empty() {
41+
labels.insert(name, val);
42+
}
43+
}
44+
45+
labels
46+
})
47+
}
48+
49+
fn addrs() -> &'static HashMap<u16, String> {
50+
static LABELS: OnceLock<HashMap<u16, String>> = OnceLock::new();
51+
LABELS.get_or_init(|| {
52+
let text = include_str!("../../tetris.dbg");
53+
let mut labels: HashMap<u16, String> = HashMap::new();
54+
55+
for line in text.lines() {
56+
let (name, val) = parse_debug_line(line);
57+
58+
if !name.is_empty() {
59+
labels.insert(val, name);
60+
}
61+
}
62+
63+
labels
64+
})
65+
}
66+
67+
pub fn get(label: &str) -> u16 {
68+
*labels().get(label).expect(&format!("label {} not found", label))
69+
}
70+
71+
pub fn from_addr(addr: u16) -> Option<&'static String> {
72+
addrs().get(&addr)
73+
}

tests/src/main.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
mod labels;
2+
mod pushdown;
3+
mod util;
4+
mod block;
5+
mod sps;
6+
7+
use gumdrop::Options;
8+
9+
fn parse_hex(s: &str) -> Result<u32, std::num::ParseIntError> {
10+
u32::from_str_radix(s, 16)
11+
}
12+
13+
#[derive(Debug, Options)]
14+
struct TestOptions {
15+
help: bool,
16+
#[options(help = "run tests")]
17+
test: bool,
18+
#[options(help = "set SPS seed", parse(try_from_str = "parse_hex"))]
19+
sps_seed: u32,
20+
#[options(help = "print SPS pieces")]
21+
sps_qty: u32,
22+
}
23+
24+
fn main() {
25+
let options = TestOptions::parse_args_default_or_exit();
26+
27+
if options.test {
28+
pushdown::test();
29+
println!("pushdown works!");
30+
}
31+
32+
if options.sps_qty > 0 {
33+
let mut blocks = sps::SPS::new();
34+
35+
blocks.set_input((
36+
((options.sps_seed >> 16) & 0xFF) as u8,
37+
((options.sps_seed >> 8) & 0xFF) as u8,
38+
(options.sps_seed& 0xFF) as u8,
39+
));
40+
41+
for i in 0..options.sps_qty {
42+
print!("{:?}", blocks.next());
43+
}
44+
}
45+
46+
// TODO: cycle counts for modes
47+
}

tests/src/pushdown.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use crate::{labels, util};
2+
3+
pub fn test() {
4+
let mut emu = util::emulator(None);
5+
6+
for pushdown in 2..15 {
7+
[0..1000, 24500..25500].into_iter().for_each(|range| {
8+
for score in range {
9+
util::set_score(&mut emu, score);
10+
11+
emu.registers.pc = labels::get("addPushDownPoints");
12+
emu.memory.iram_raw[labels::get("holdDownPoints") as usize] = pushdown;
13+
14+
util::run_to_return(&mut emu, false);
15+
16+
let reference = pushdown_impl(pushdown, score as u16) as u32;
17+
18+
assert_eq!(reference, util::get_score(&mut emu) - score);
19+
}
20+
});
21+
}
22+
}
23+
24+
// reference implementation - tested against the original game
25+
// may seem weird - designed to be translated to assembly
26+
fn pushdown_impl(pushdown: u8, score: u16) -> u16 {
27+
let ones = score % 10;
28+
let hundredths = score % 100;
29+
let mut newscore = ones as u8 + (pushdown - 1);
30+
if newscore & 0xF > 9 {
31+
newscore += 6;
32+
}
33+
34+
let low = (newscore & 0xF) as u16;
35+
let high = ((newscore & 0xF0) / 16 * 10) as u16;
36+
37+
let mut newscore = high + (hundredths - ones);
38+
let nextscore = newscore + low;
39+
40+
if nextscore <= 100 {
41+
newscore = nextscore;
42+
}
43+
44+
newscore + (score - hundredths) - score
45+
}

tests/src/sps.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use rusticnes_core::nes::NesState;
2+
use crate::{labels, util, block};
3+
4+
pub struct SPS {
5+
emu: NesState,
6+
}
7+
8+
impl SPS {
9+
pub fn new() -> Self {
10+
Self {
11+
emu: util::emulator(None),
12+
}
13+
}
14+
15+
pub fn set_input(&mut self, seed: (u8, u8, u8)) {
16+
self.emu.memory.iram_raw[(labels::get("set_seed_input") + 0) as usize] = seed.0;
17+
self.emu.memory.iram_raw[(labels::get("set_seed") + 0) as usize] = seed.0;
18+
self.emu.memory.iram_raw[(labels::get("set_seed_input") + 1) as usize] = seed.1;
19+
self.emu.memory.iram_raw[(labels::get("set_seed") + 1) as usize] = seed.1;
20+
self.emu.memory.iram_raw[(labels::get("set_seed_input") + 2) as usize] = seed.2;
21+
self.emu.memory.iram_raw[(labels::get("set_seed") + 2) as usize] = seed.2;
22+
}
23+
24+
pub fn next(&mut self) -> block::Block {
25+
self.emu.registers.pc = labels::get("pickTetriminoSeed");
26+
27+
util::run_to_return(&mut self.emu, false);
28+
29+
self.emu.memory.iram_raw[labels::get("spawnID") as usize].into()
30+
}
31+
}

tests/src/util.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use rusticnes_core::nes::NesState;
2+
use rusticnes_core::{ cartridge, opcodes, opcode_info };
3+
use crate::labels;
4+
5+
pub fn emulator(rom: Option<&[u8]>) -> NesState {
6+
let rom = rom.unwrap_or(include_bytes!("../../tetris.nes"));
7+
let mut emu = NesState::new(Box::new(cartridge::mapper_from_file(rom)).unwrap());
8+
9+
emu.power_on();
10+
11+
emu
12+
}
13+
14+
pub fn run_to_return(emu: &mut NesState, print: bool) {
15+
opcodes::push(emu, 0);
16+
opcodes::push(emu, 0);
17+
18+
loop {
19+
if print {
20+
print_step(emu);
21+
} else {
22+
emu.step();
23+
}
24+
25+
if emu.registers.pc < 3 {
26+
break;
27+
}
28+
}
29+
}
30+
31+
pub fn print_step(emu: &mut NesState) {
32+
if let Some(label) = labels::from_addr(emu.registers.pc) {
33+
println!("{}:", label);
34+
}
35+
36+
print!("{:x} ", emu.registers.pc);
37+
38+
emu.step();
39+
40+
println!("{}", opcode_info::disassemble_instruction(emu.cpu.opcode, 0, 0).0);
41+
}
42+
43+
pub fn set_score(emu: &mut NesState, score: u32) {
44+
let score_addr = labels::get("score");
45+
let binscore_addr = labels::get("binScore");
46+
let bcd_str = format!("{:06}", score);
47+
let bcd_a = i64::from_str_radix(&bcd_str[0..2], 16).unwrap();
48+
let bcd_b = i64::from_str_radix(&bcd_str[2..4], 16).unwrap();
49+
let bcd_c = i64::from_str_radix(&bcd_str[4..6], 16).unwrap();
50+
51+
emu.memory.iram_raw[(score_addr + 2) as usize] = bcd_a as u8;
52+
emu.memory.iram_raw[(score_addr + 1) as usize] = bcd_b as u8;
53+
emu.memory.iram_raw[score_addr as usize] = bcd_c as u8;
54+
emu.memory.iram_raw[binscore_addr as usize] = score as u8;
55+
emu.memory.iram_raw[(binscore_addr + 1) as usize] = (score >> 8) as u8;
56+
emu.memory.iram_raw[(binscore_addr + 2) as usize] = (score >> 16) as u8;
57+
}
58+
59+
pub fn get_score(emu: &mut NesState) -> u32 {
60+
let binscore_addr = labels::get("binScore");
61+
emu.memory.iram_raw[binscore_addr as usize] as u32
62+
+ ((emu.memory.iram_raw[(binscore_addr + 1) as usize] as u32) << 8)
63+
+ ((emu.memory.iram_raw[(binscore_addr + 2) as usize] as u32) << 16)
64+
}

0 commit comments

Comments
 (0)