For those who don’t know me, I’m a huge mahjong fan. Over the past several years I’ve been collecting mahjong video games in an effort to play and document every console mahjong game that was released (I have to draw the line at consoles because of all the PC and arcade releases). One of those resonated with me, not because it was especially good, but because it was interesting, silly, and approachable.
まじゃべんちゃー, or Mahjaventure, is a head-to-head mahjong adventure game with light RPG elements. You play an unnamed hero who must travel the kingdom, playing mahjong to defeat the Mahjlord’s minions, and eventually the Mahjlord himself. Unlike most RPG or other story-driven mahjong games, this one could be played with little to no ability to read Japanese, which is why I spent so much time playing it.
Eventually I started to share this game with my other mahjong friends, and we decided it would be fun to make a translated version available. After learning how to find at extract the game’s text, replace the Japanese characters with Roman characters, injecting translated text that pointed to the new characters, and learning how the control codes (little flags at the end of text strings that signal the game to draw a new line, stop and wait for the player to press a button, display an image, etc.) through trial and error, we finally ended up with a good translation of the game. Except…
Exporting text only works with dialogue. We also needed the menus and interface translated as well. There were a few difficulties with that. First, without knowing what the text said, we couldn’t search the game’s ROM for those text strings to translate, so we would have to play every corner of the game to make sure we found all of it. Second, Japanese takes up significantly less screen space than English, so fitting “Tsumo” where “ツモ” used to be just didn’t work. We even tried custom characters to try and fit “Tsumo” into two character tiles, but this method wasn’t memory-efficient and left the game looking inconsistent. The solution was to be able to tell the game to draw the interface in a way that could accommodate the longer text. Unfortunately, there was only one way I could think of to solve this problem: Learn to write code for the NES.
The Nintendo Entertainment System and it’s Japanese brother the Famicom, run on a variant of the 6802 that was custom made by Ricoh. Referencing this backed-up copy of the Nerdy Nights guide to writing assembly language for the NES, I got to work learning how the NES works from a programmer’s perspective. Thanks to resources like I Am Error and The Manga Guide to Microprocessors, as well as games like TIS-100 and Shenzhen I/O, I had an idea of what I was getting myself into.
So where am I at now? I managed to build myself a template that doesn’t throw errors when I compile it, then I started seeing if I could do some simple math to calculate mahjong scores within the NES. The scoring system for mahjong is a little complicated, but it follows the formula Base Points = Fu*2^(2+Han). From there, there’s other factors that determine your total score, but I thought base points was a good place to start.
;;;;;;;;;;;;;;; ; iNES header ; ;;;;;;;;;;;;;;; ; .inesprg is the number of 16KB banks of PRG code. ; Remember that the .bank directives each identify 8KB ; so a .inesprg 1 = 16KB = 2 8KB banks .inesprg 1 ; .ineschr is the number of 8KB banks of CHR data. .ineschr 1 ; .inesmap defines the memory mapper being used. ; 0 = NROM (default) .inesmap 0 ; .inesmir is background mirroring. ; 0 = horizontal mirroring (vertical arrangement) ; 1 = vertical mirroring (horizontal arrangement) .inesmir 1 ;;;;;;; ; PRG ; ;;;;;;; .bank 0 .org $C000 RESET: SEI ; Disable IRQs CLD ; Disable decimal mode LDX #$04 ; 4 han LDY #$28 ; 40 fu ; base points = fu*2^(2+han) TXA ; Move han into accumulator ADC #$02 ; Add 2 TAX ; Move accumulator into X (should be 6) LDA #$02 ; Move 2 into accumulator to start 2^n DEX ; Subtract 1 from X for the initial 2^1 EXPONENTLOOP: ASL A ; Multiple accumulator by 2 DEX ; subtract 1 from X CPX 0 ; Check if X = 0 BNE EXPONENTLOOP ; Keep multiplying until X = 0. ; A should be 64 ($40) TAX ; Move accumulator to X ; X = $40 (64), Y = $28 (40) ; Need to multiply them together $0A00 ; Fuck, that's a 16-bit value .bank 1 .org $E000 NMI: RTI ;;;;;;;;;;;;;;;;;; ; Define Vectors ; ;;;;;;;;;;;;;;;;;; .org $FFFA ; NMI, RESET, and IRQ live in $FFFA, $FFFB, and $FFFC .dw NMI ; NMI happens once at the start of each VBlank .dw RESET ; RESET happens once at boot and every time the ; the Reset button is pressed .dw 0 ; Interrupt Request ;;;;;;; ; CHR ; ;;;;;;; .bank 2 .org $0000 .incbin "mario.chr" ; Imports binary data from mario.chr ; Needs to full the 8KB CHR ROM/bank
Wow. Yeah. Okay. Most of the top and bottom don’t really matter all that much; what we’re looking at is the middle bit. There are 3 registers we’re using here, X, Y, and A. A is the accumulator, where all the math is done. X and Y are convenient places to store number while doing things like running a loop some number of times. What I’m doing is loading dummy scores into the X and Y registers, then running through the scoring formula in order of operations. I need to add 2+han. I could have used INX twice to increment the X register by 1 twice, but instead I decided to transfer the X register into the accumulator (TXA), add 2 (ADC #$02), then transfer it back into the X register (TAX).
Now I need to figure out 2 to the power of 6. The 6502 doesn’t do multiplication or division, never mind exponents, so instead I loaded 2 into the accumulator (LDA #$02), then used a command called Arithmetic Shift Left (ASL A) to shift the binary values of the accumulator to the left by one digit. It’s confusing to understand at first, but if you think about it in decimal, if you have 01, then move the 1 to the left, you end up with 10. You’ve successfully multiplied your starting value by 10 just by moving the digit one place over. Similarly, binary is a base-2 system, so moving digits to the left multiplies them by 2. EXPONENTLOOP is a loop that shifts the starting value of 0000 0010 (or 2 in decimal), and decrements the X register (DEX) by 1 every time until X = 0, at which point Branch Not Equal (BNE) returns false, setting us free of the loop and moving the new value of 64.
But now we run into an issue. The X, Y, and A registers are all 1 byte, meaning the largest value they can hold is 255. The next step in our formula requires us to multiply 40*64, which is 2560, which you may notice is slightly larger than our 255 max. There are ways around that, which probably involve things like the carry flag, which I haven’t learned too much about. With enough time I could probably figure out how to make the math work, but I also risk learning bad habits by doing so, so I’ll just put this aside for now and move onto the next part of Nerdy Night’s lesson plan.