Back to home

Game History

2025/11/01

Paifu

Long before I even started working on any game history viewer, I'd need to actually record game history. There is a concept in Mahjong called a Paifu, a piece of paper which can contain all the relevant information in a single hand. Group a bunch of Paifu together, and you've got a history of a game, right?

A Paifu as depicted in Saki.

To write a Paifu, first I had to understand it. Thankfully, there's english information to be gleaned from the Riichi Wiki and especially a lengthy blog post by Jellicode.

My first idea was to structure my Paifu in a way so they were 1-to-1 with the data structure of the Tenhou paifu. I wrote the code, but there were some teething issues. Tenhou has a different internal structure of IDs for tiles, and there was plenty of opportunity for confusion. Therefore, that piece of code was left dormant for possible later reuse.

I decided to come up with my own Paifu format. Now, I'm using the term "format" here lightly, because I knew I wanted something human readable. I settled on a Markdown document just using plain text for all the information. Tiles are simply represented by their Unicode code points.

I decided to still follow the "rules" for how the Tenhou Paifu tool handles things like calls, end of round results and so on. Working with my own format while applying those rules were a lot easier than trying to immediately map it to a foreign format.

A screenshot of the final Paifu.md format

The section at the top contains meta-information about the game being played. The Guid you see is what I chose to represent the ID of the entire game. Because clients stay connected to the host for 1 game and 1 game only (afterwards, you're kicked back to the main menu), I'm using Connection.Host.Id. That saves me some work from trying to keep it synchronized between all clients.

In theory I could just group hands into games by reading each file and finding this Guid, but I think NTFS will disapprove of having a thousand Paifu files in a single folder, so I've decided to split them up into folders later.

The datetime is obviously the time the hand was played, and the rest states what round was played in plain text. Doras are listed as well. The only really "Markdown" part of the document ended up being the # (header) used to represent different sections of the file.

Each player section has their seat, their Steam ID and their displayname at the time. Then follows their starting tiles, their draws, discards and how they ended the hand. In this painful example, I was responsible for paying a 12000 ron (and 300 more for honba).

You'll note that everything is in japanese. This actually has a benefit beyond yielding accurate terms; Japanese doesn't really have any spaces. This means I can reliably use spaces to separate values, not just for human readability, but also for machine readability.

A screenshot of the final Paifu.md format, with the text highlighted so you can see the spaces

This was accumulating game history data for a few months while I worked on other things. I was gathering the data both on disk (in the Data filesystem) and in the Stats service. I was storing it in the Stats service so I could quickly find the best hand ever, and to keep a rolling total of your final scores. I also kept it on disk because the Stats service does not seem to have an easy way to get to individual results.

After I finished the tutorial, I decided it was time to start working on the user interface which would actually use all this accumulated data. I started by drawing up a simple concept in Paint.NET.

I think you can tell I'm not a big UI guy. Yikes.

This conveniently let me work on stuff from top to bottom with the difficulty increasing along the way. The first step was re-creating the layout in Razor. Having a concept in mind already made it much easier to build the initial structure.

If you're not an s&box (or Razor?) developer, this might confuse you. The Razor implementation in the engine doesn't have any sensible defaults for layout. If you're not already familiar with HTML, you might struggle to build a layout that's feasible to style into a good shape.

There we go! I had a static demo of my concept.

Getting the friend leaderboard and best hand was really easy, but I was going to have to do extra work for the tiles to appear. When I started recording data, I had only included the Paifu.md, so I would need to parse the entire thing to get the actual tiles of the finished hand. I decided to include a "handInfo" parameter in future data so I could bypass this extra processing, once I was done working on this.

I was going to need a Paifu.md reader regardless, so I got to work. It would be ideal if it was separated from the rest of the s&box logic — my Riichi Mahjong code is already mostly isolated. Therefore, I developed the Paifu reader completely inside Visual Studio with unit testing as my guide.

I ran into a slight roadblock here. I had decided that red 5 tiles would be represented by 🟥 (Large Red Square) + the regular 5. This doesn't show up visually in any editor, but I figured it was obvious enough. However, it posed a problem for parsing the tiles, as I could no longer simply split a call like 🀋🀋🟥🀋【ポン】 into characters to get each tile.

What followed was some wrestling with C#'s strings, but I was able to get it to work. I even had to employ some use of StringInfo use because .Length is not what it seems.

Any high-minded ideas about printing the best hand as straight up Unicode left at this point. I was going to embed the actual tile pictures in here, either as SVGs or the textures I've generated from them. I was also going to have to figure out where tiles were called from, and put that in my Paifu reader.

Thankfully, it turned out that reusing my generated textures was easier than expected.

Note that the rotated indicator is not accurate in this screenshot. I left that work for later.

As I was working on the bottom half of the screen, I ran into another showstopper. Remember when I said above that I was saving myself some work by using Connection.Host.Id as a unique identifier for each game? It turns out that a client's Connection Guid is static for as long as s&box is running. This means that if you played multiple games in a row without restarting your game, they would share the same id.

Unfortunately, I only discovered this after I had started collecting game history data, and I now had some broken data. Fortunately, I only had 5 total recorded games, so at least I discovered it early. Syncing a Game Id wasn't much of a problem after all, I just had to randomize it on the host and it would get synced down to the clients as a part of the snapshot. Since it doesn't change and lobbies are destroyed between games, this is actually fine.

When I was integrating the game history into the rest of the menu, I ran into an issue where the half mouse-click would carry over into the new scene, causing havoc. I had to change my menu to trigger scene loading on mouse up to prevent inputs from being misinterpreted.

I made a public release of the game history feature when the list of games + hands was ready, but I still had a lot of work to do to make the individual Paifu visible. I also sorted out some side-objectives at that point, such as storing Paifu in folders and fixing the Game Id issue.

The Paifu viewer itself was a mostly incremental process with many sharp rocks to step on. The first struggle was to how to make it even appear as a layered window on top. First I tried to include it as a separate component, but rather than rendering both PanelComponents over eachother, this makes them split the screenspace down the middle. After that I tried to include it as a prefab (so it would have its own Screen Panel component), but that didn't work for some reason I'm forgetting. In the end, I decided to just include it directly in the main history panel if it is time to be shown.

The next sharp rock was how to make it behave as a modal, and having it control its own Close logic. C#'s strict typing makes passing functions as Razor component arguments a bit of a pain, so I had to add some indirection here.

// HandHistory.razor
@if (DisplayedHand is not null)
{
	<PaifuViewer Paifu=@DisplayedHand Close=@ClosePaifuViewer />
}

// ...

Mahjong.Paifu DisplayedHand { get; set; } = null;

void DisplayHand(Mahjong.Paifu hand)
{
	DisplayedHand = hand;
}
void ClosePaifuViewer()
{
	DisplayedHand = null;
}

// PaifuViewer.razor
public System.Action Close { get; set; }

After I got this working, I realised I didn't have a concept in mind for the design. I decided to just make it look like how a printed Paifu would look, with everything listed in order. I had already written most of the code I needed when I was making the game history, so it was mostly just a matter of displaying it on the page from then on. Of course, doing the layout had plenty of back and forth between CSS and the game, trying to make things layout properly. After a long day, I had something I felt proud of.

The paifu viewer ingame, showing a hand where I had a massive 7700 point ron.

Once I had all that working, I could start looking through my already accumulated game history in search for more sharp rocks to step on. For example, I had to get all the different manner of Kans working properly.

Reconstructing the final state of hands was not super troublesome, except knowing the direction a call was taken from has been left as an exercise for the future. It requires peeking at what the other players are doing, but the code I wrote focuses solely on a single player. In retrospect, this might have been a mistake, and it would have been a good idea to make a full turn order from the information before extracting a single player's finished hand.

However, that's a refactor I'm not willing to do. It will just stand as advice for anyone who's thinking of parsing my Paifu.md in the future.

After all that, it was a piece of cake to make the best hand display also open up a Paifu viewer so you can inspect your best game. I knew I still had some bugs to iron out, but that could be solved with more playtesting, so I published it.