mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
A complete, playable port of the card game War: title screen with 0/1/2 player menu, animated deal, sliding cards, deck-count HUD, a "WAR!" tie-break with buried cards, and a victory screen with a fanfare. Source split across examples/war/*.ne (constants, assets, audio, deck/queue logic, RNG, render helpers, and one state file per game state) and pulled in via examples/war.ne. Drives nearly every NEScript subsystem at once: custom 88-tile sprite sheet (card frames, ranks, suits, font, BIG WAR letters); felt background nametable; pulse-1 / pulse-2 / noise sfx; looping march on pulse 2; an 8-bit Galois LFSR PRNG; queue-based decks that conserve cards across rounds; a phase machine inside the Playing state that handles draw/reveal/win/war/check; and an autopilot that boots straight into 0-PLAYERS mode so the headless jsnes harness captures real gameplay at frame 180. While building this I uncovered five compiler bugs / limitations in the v0.1 implementation; each is documented with a minimal reproduction, root cause, current workaround, and proposed fix in examples/war/COMPILER_BUGS.md. The most painful was the parameter-VarId aliasing one (#1b) — two functions sharing a parameter NAME end up sharing a single zero-page slot mapping across the whole program. Once those compiler bugs are fixed, the workarounds in war/*.ne should be reverted in the same PR. https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
41 lines
1.3 KiB
Text
41 lines
1.3 KiB
Text
// war/rng.ne — 8-bit Galois LFSR pseudo-random number generator.
|
|
//
|
|
// A classic taps = 0xB8 Galois LFSR: on every call, shift the
|
|
// state right by one and XOR with the taps polynomial if the old
|
|
// low bit was 1. The period is 255 (all non-zero states), which
|
|
// is plenty for a card shuffle.
|
|
//
|
|
// The state lives in the global `rng_state` byte declared in
|
|
// war/state.ne. Seeded from a running tick counter on title exit
|
|
// (so the jsnes golden harness gets a deterministic shuffle).
|
|
|
|
// Advance the LFSR one step and return the new state. Every caller
|
|
// treats the return value as the next random byte.
|
|
fun rand_u8() -> u8 {
|
|
var s: u8 = rng_state
|
|
var lsb: u8 = s & 1
|
|
s = s >> 1
|
|
if lsb != 0 {
|
|
s = s ^ 0xB8
|
|
}
|
|
// Guard against the degenerate all-zero state: if we ever
|
|
// roll into zero the LFSR is stuck, so reseed from a fixed
|
|
// non-zero constant. In practice this only happens on a
|
|
// bad initial seed — we start at 0xA7 so the cycle stays
|
|
// healthy.
|
|
if s == 0 {
|
|
s = 0xA7
|
|
}
|
|
rng_state = s
|
|
return s
|
|
}
|
|
|
|
// Seed the LFSR from an arbitrary byte. Zero is remapped to the
|
|
// same fallback constant rand_u8() uses so the period stays 255.
|
|
fun rng_seed(seed: u8) {
|
|
if seed == 0 {
|
|
rng_state = 0xA7
|
|
} else {
|
|
rng_state = seed
|
|
}
|
|
}
|