This page describes the process of transforming raw JSON Lines from the MJAI protocol into typed Go objects and subsequently into domain-level events. This transformation acts as an Anti-Corruption Layer (ACL), ensuring that the core game logic remains decoupled from the specific string-based representations and JSON structures of the MJAI protocol.
The decoding process follows a two-stage pipeline:
inbound.Message interface based on the type field.inbound.Message is converted into an event.Event via the ToEvent() method, which performs validation and maps MJAI primitives (like seat integers and tile strings) to domain types.The following diagram illustrates how a raw MJAI message string flows through the system to become a domain event.
"Decoding Flow: MJAI String to Domain Event"
Sources: internal/adapter/mjai/inbound/parse_message.go1-20 internal/adapter/mjai/inbound/parse_event.go1-20
The ParseMessage function serves as the primary entry point. It inspects the type field of the incoming JSON and dispatches it to the appropriate struct using a type-switch or lookup mechanism.
Every inbound message struct must implement the inboundMessage marker interface and a ToEvent method:
The dispatch logic in ParseMessage maps MJAI type strings to their respective Go structs:
| MJAI Type | Go Struct | Domain Event |
|---|---|---|
tsumo | inbound.Tsumo | event.Draw |
dahai | inbound.Dahai | event.Discard |
chi | inbound.Chi | event.Chii |
pon | inbound.Pon | event.Pon |
kan | inbound.Ankan | event.ConcealedKan |
daiminkan | inbound.Daiminkan | event.CalledKan |
kakan | inbound.Kakan | event.PromotedKan |
reach | inbound.Reach | event.Riichi |
reach_accepted | inbound.ReachAccepted | event.RiichiAccepted |
hora | inbound.Hora | event.Win |
ryukyoku | inbound.Ryukyoku | event.EndRound |
start_kyoku | inbound.StartKyoku | event.StartRound |
dora | inbound.Dora | event.Dora |
Sources: internal/adapter/mjai/inbound/parse_message.go1-40 internal/adapter/mjai/inbound/dahai.go9-16 internal/adapter/mjai/inbound/tsumo.go9-15 internal/adapter/mjai/inbound/chi.go9-17 internal/adapter/mjai/inbound/pon.go9-17 internal/adapter/mjai/inbound/daiminkan.go9-18 internal/adapter/mjai/inbound/kakan.go9-17 internal/adapter/mjai/inbound/dora.go9-14
Each message struct implements ToEvent() to translate protocol-specific fields into domain types. This involves several helper functions defined in convert.go.
"Mapping MJAI Primitives to Domain Types"
The Dahai message maps the actor (int) to a seat.Seat and the pai (string) to a tile.Tile. It also carries the tsumogiri boolean.
internal/adapter/mjai/inbound/dahai.go18-37
StartKyoku is the most complex message, requiring the parsing of:
bakaze: Wind string to wind.Wind via wind.NewWind. internal/adapter/mjai/inbound/start_kyoku.go56-59oya: Dealer index to seat.Seat. internal/adapter/mjai/inbound/start_kyoku.go61-64tehais: A 2D array of tile strings to a fixed-size array [4][13]tile.Tile. internal/adapter/mjai/inbound/start_kyoku.go37-49scores: Optional score array. internal/adapter/mjai/inbound/start_kyoku.go51-54The Hora message handles optionality. Some MJAI implementations or logs omit the pai (winning tile) or scores. The event.Win struct uses pointers to handle these optional fields, and ToEvent performs the necessary nil checks.
internal/adapter/mjai/inbound/hora.go30-71 internal/domain/game/event/win.go9-18
Sources: internal/adapter/mjai/inbound/convert.go1-50 internal/adapter/mjai/inbound/dahai.go18-37 internal/adapter/mjai/inbound/start_kyoku.go26-81 internal/adapter/mjai/inbound/hora.go30-71 internal/adapter/mjai/inbound/chi.go19-44 internal/adapter/mjai/inbound/pon.go19-44
The codebase includes experimental support for go-json-experiment/json (jsonv2). This is used in ParseMessage to leverage more efficient unmarshalling techniques where available. The logic remains backward compatible with encoding/json while providing a path for performance optimization in the message-heavy loop of an MJAI game. In tests, json/v2 is explicitly imported and used to verify output messages.
Sources: internal/adapter/mjai/inbound/parse_message.go5-15 internal/adapter/mjai/runtime/golden_test.go5
Inbound decoding is heavily tested using table-driven tests that verify both successful conversions and error handling for invalid MJAI messages (e.g., out-of-range actor indices or invalid tile codes).
tsumogiri flags and tile code parsing via ParseMessage and ParseEvent. internal/adapter/mjai/inbound/dahai_test.go12-100deltas and scores are handled correctly when absent from the JSON, and that invalid actor/target indices trigger errors. internal/adapter/mjai/inbound/hora_test.go11-85Chi, Pon, and Kan variants are tested to ensure the consumed tile arrays are parsed correctly into domain tile.Tile slices. internal/adapter/mjai/inbound/chi_test.go1-20 internal/adapter/mjai/inbound/pon_test.go1-20mjairuntime package uses golden files to verify that the entire loop—from inbound JSON to AI decision and outbound JSON—remains consistent. internal/adapter/mjai/runtime/golden_test.go16-133Sources: internal/adapter/mjai/inbound/dahai_test.go12-100 internal/adapter/mjai/inbound/hora_test.go11-85 internal/adapter/mjai/inbound/chi_test.go1-20 internal/adapter/mjai/inbound/pon_test.go1-20 internal/adapter/mjai/runtime/golden_test.go16-133
Refresh this wiki