feat(wire): carry a word's senses on PlayedWord and GameStarted

`message Sense {pos, gloss}`; fields 7 and 9, wire-compatible; `wsapi.Dictionary` gains `Meanings`; looked up once per move.
This commit is contained in:
tiennm99 committed 2026-09-08 22:48:26 +07:00
1 parent 80f216d56b
commit 9e74f6f959
13 files changed
+450 -118

No files matched your search

+16
View File
@@ -171,6 +171,20 @@ message PlayedWord {
// the table the chain also has to say whose the other words were, and a seat
// id says that without the client matching display names.
string player_id = 6;
// What the word means, at most five senses. Empty when the dictionary has
// no definition for the word; the client then says so rather than hiding
// the row's panel, so every word in the chain behaves the same.
repeated Sense meanings = 7;
}
// Sense is one definition of a word as Wiktionary gives it: the part of
// speech it sits under, in Vietnamese ("danh từ"), empty when the heading was
// not one the builder knows, and the definition stripped of markup at at most
// 200 characters. Plain text both: the client renders them as text, never as
// markup.
message Sense {
string pos = 1;
string gloss = 2;
}
// PlayerSlot is one seat in the room, rendered for one recipient.
@@ -235,6 +249,8 @@ message GameStarted {
// Whose turn it is. my_turn above says whether it is yours; this says whose
// it is when it is not, which a two-player game never had to.
string turn_player_id = 8;
// The opening word's senses, as PlayedWord.meanings for a played word.
repeated Sense opening_meanings = 9;
}
// TurnUpdate follows every turn change and goes to every player, serialized
+3 -2
View File
@@ -1,6 +1,7 @@
"n
"¤
hòa bìnhbình  ×ø©‘3(0°ê:
p1Ngư�i chơi 0:
p2 Khách m�i *(0:
p3Khách 2 Bp1
p3Khách 2 Bp1J4
danh từ'Tình trạng không có chiến tranh.
+5 -3
View File
@@ -1,7 +1,9 @@
*€

*É
h
bình yên * binh yên2p2yên Àóù©‘3(@J
bình yên * binh yên2p2:6
tính từ(Yên lành, không có gì xáo động.: Xem bình an.yên Àóù©‘3(@J
p1Người chơi 0J
p2 Khách mời *(0J
p3Khách 2 Rp3
+173 -88
View File
@@ -1084,7 +1084,11 @@ type PlayedWord struct {
// Which seat played it. by_me answers "was this mine"; with four people at
// the table the chain also has to say whose the other words were, and a seat
// id says that without the client matching display names.
PlayerId string `protobuf:"bytes,6,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
PlayerId string `protobuf:"bytes,6,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
// What the word means, at most five senses. Empty when the dictionary has
// no definition for the word; the client then says so rather than hiding
// the row's panel, so every word in the chain behaves the same.
Meanings []*Sense `protobuf:"bytes,7,rep,name=meanings,proto3" json:"meanings,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1161,6 +1165,70 @@ func (x *PlayedWord) GetPlayerId() string {
return ""
}
func (x *PlayedWord) GetMeanings() []*Sense {
if x != nil {
return x.Meanings
}
return nil
}
// Sense is one definition of a word as Wiktionary gives it: the part of
// speech it sits under, in Vietnamese ("danh từ"), empty when the heading was
// not one the builder knows, and the definition stripped of markup at at most
// 200 characters. Plain text both: the client renders them as text, never as
// markup.
type Sense struct {
state protoimpl.MessageState `protogen:"open.v1"`
Pos string `protobuf:"bytes,1,opt,name=pos,proto3" json:"pos,omitempty"`
Gloss string `protobuf:"bytes,2,opt,name=gloss,proto3" json:"gloss,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Sense) Reset() {
*x = Sense{}
mi := &file_noitu_v1_game_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Sense) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Sense) ProtoMessage() {}
func (x *Sense) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Sense.ProtoReflect.Descriptor instead.
func (*Sense) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{15}
}
func (x *Sense) GetPos() string {
if x != nil {
return x.Pos
}
return ""
}
func (x *Sense) GetGloss() string {
if x != nil {
return x.Gloss
}
return ""
}
// PlayerSlot is one seat in the room, rendered for one recipient.
//
// The recipient's own row is in the list like everybody else's, marked by
@@ -1192,7 +1260,7 @@ type PlayerSlot struct {
func (x *PlayerSlot) Reset() {
*x = PlayerSlot{}
mi := &file_noitu_v1_game_proto_msgTypes[15]
mi := &file_noitu_v1_game_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1204,7 +1272,7 @@ func (x *PlayerSlot) String() string {
func (*PlayerSlot) ProtoMessage() {}
func (x *PlayerSlot) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[15]
mi := &file_noitu_v1_game_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1217,7 +1285,7 @@ func (x *PlayerSlot) ProtoReflect() protoreflect.Message {
// Deprecated: Use PlayerSlot.ProtoReflect.Descriptor instead.
func (*PlayerSlot) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{15}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{16}
}
func (x *PlayerSlot) GetPlayerId() string {
@@ -1294,7 +1362,7 @@ type PlayerScore struct {
func (x *PlayerScore) Reset() {
*x = PlayerScore{}
mi := &file_noitu_v1_game_proto_msgTypes[16]
mi := &file_noitu_v1_game_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1306,7 +1374,7 @@ func (x *PlayerScore) String() string {
func (*PlayerScore) ProtoMessage() {}
func (x *PlayerScore) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[16]
mi := &file_noitu_v1_game_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1319,7 +1387,7 @@ func (x *PlayerScore) ProtoReflect() protoreflect.Message {
// Deprecated: Use PlayerScore.ProtoReflect.Descriptor instead.
func (*PlayerScore) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{16}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{17}
}
func (x *PlayerScore) GetPlayerId() string {
@@ -1387,14 +1455,16 @@ type GameStarted struct {
Players []*PlayerScore `protobuf:"bytes,7,rep,name=players,proto3" json:"players,omitempty"`
// Whose turn it is. my_turn above says whether it is yours; this says whose
// it is when it is not, which a two-player game never had to.
TurnPlayerId string `protobuf:"bytes,8,opt,name=turn_player_id,json=turnPlayerId,proto3" json:"turn_player_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
TurnPlayerId string `protobuf:"bytes,8,opt,name=turn_player_id,json=turnPlayerId,proto3" json:"turn_player_id,omitempty"`
// The opening word's senses, as PlayedWord.meanings for a played word.
OpeningMeanings []*Sense `protobuf:"bytes,9,rep,name=opening_meanings,json=openingMeanings,proto3" json:"opening_meanings,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GameStarted) Reset() {
*x = GameStarted{}
mi := &file_noitu_v1_game_proto_msgTypes[17]
mi := &file_noitu_v1_game_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1406,7 +1476,7 @@ func (x *GameStarted) String() string {
func (*GameStarted) ProtoMessage() {}
func (x *GameStarted) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[17]
mi := &file_noitu_v1_game_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1419,7 +1489,7 @@ func (x *GameStarted) ProtoReflect() protoreflect.Message {
// Deprecated: Use GameStarted.ProtoReflect.Descriptor instead.
func (*GameStarted) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{17}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{18}
}
func (x *GameStarted) GetOpeningWord() string {
@@ -1478,6 +1548,13 @@ func (x *GameStarted) GetTurnPlayerId() string {
return ""
}
func (x *GameStarted) GetOpeningMeanings() []*Sense {
if x != nil {
return x.OpeningMeanings
}
return nil
}
// TurnUpdate follows every turn change and goes to every player, serialized
// once per recipient so by_me and my_turn are correct for each.
//
@@ -1501,7 +1578,7 @@ type TurnUpdate struct {
func (x *TurnUpdate) Reset() {
*x = TurnUpdate{}
mi := &file_noitu_v1_game_proto_msgTypes[18]
mi := &file_noitu_v1_game_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1513,7 +1590,7 @@ func (x *TurnUpdate) String() string {
func (*TurnUpdate) ProtoMessage() {}
func (x *TurnUpdate) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[18]
mi := &file_noitu_v1_game_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1526,7 +1603,7 @@ func (x *TurnUpdate) ProtoReflect() protoreflect.Message {
// Deprecated: Use TurnUpdate.ProtoReflect.Descriptor instead.
func (*TurnUpdate) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{18}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{19}
}
func (x *TurnUpdate) GetPlayed() *PlayedWord {
@@ -1596,7 +1673,7 @@ type MoveRejected struct {
func (x *MoveRejected) Reset() {
*x = MoveRejected{}
mi := &file_noitu_v1_game_proto_msgTypes[19]
mi := &file_noitu_v1_game_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1608,7 +1685,7 @@ func (x *MoveRejected) String() string {
func (*MoveRejected) ProtoMessage() {}
func (x *MoveRejected) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[19]
mi := &file_noitu_v1_game_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1621,7 +1698,7 @@ func (x *MoveRejected) ProtoReflect() protoreflect.Message {
// Deprecated: Use MoveRejected.ProtoReflect.Descriptor instead.
func (*MoveRejected) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{19}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{20}
}
func (x *MoveRejected) GetReason() RejectReason {
@@ -1663,7 +1740,7 @@ type GameOver struct {
func (x *GameOver) Reset() {
*x = GameOver{}
mi := &file_noitu_v1_game_proto_msgTypes[20]
mi := &file_noitu_v1_game_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1675,7 +1752,7 @@ func (x *GameOver) String() string {
func (*GameOver) ProtoMessage() {}
func (x *GameOver) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[20]
mi := &file_noitu_v1_game_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1688,7 +1765,7 @@ func (x *GameOver) ProtoReflect() protoreflect.Message {
// Deprecated: Use GameOver.ProtoReflect.Descriptor instead.
func (*GameOver) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{20}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{21}
}
func (x *GameOver) GetIWon() bool {
@@ -1739,7 +1816,7 @@ type PlayerEliminated struct {
func (x *PlayerEliminated) Reset() {
*x = PlayerEliminated{}
mi := &file_noitu_v1_game_proto_msgTypes[21]
mi := &file_noitu_v1_game_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1751,7 +1828,7 @@ func (x *PlayerEliminated) String() string {
func (*PlayerEliminated) ProtoMessage() {}
func (x *PlayerEliminated) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[21]
mi := &file_noitu_v1_game_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1764,7 +1841,7 @@ func (x *PlayerEliminated) ProtoReflect() protoreflect.Message {
// Deprecated: Use PlayerEliminated.ProtoReflect.Descriptor instead.
func (*PlayerEliminated) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{21}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{22}
}
func (x *PlayerEliminated) GetPlayerId() string {
@@ -1814,7 +1891,7 @@ type ServerError struct {
func (x *ServerError) Reset() {
*x = ServerError{}
mi := &file_noitu_v1_game_proto_msgTypes[22]
mi := &file_noitu_v1_game_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1826,7 +1903,7 @@ func (x *ServerError) String() string {
func (*ServerError) ProtoMessage() {}
func (x *ServerError) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[22]
mi := &file_noitu_v1_game_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1839,7 +1916,7 @@ func (x *ServerError) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerError.ProtoReflect.Descriptor instead.
func (*ServerError) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{22}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{23}
}
func (x *ServerError) GetCode() string {
@@ -1866,7 +1943,7 @@ type Pong struct {
func (x *Pong) Reset() {
*x = Pong{}
mi := &file_noitu_v1_game_proto_msgTypes[23]
mi := &file_noitu_v1_game_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1878,7 +1955,7 @@ func (x *Pong) String() string {
func (*Pong) ProtoMessage() {}
func (x *Pong) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[23]
mi := &file_noitu_v1_game_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1891,7 +1968,7 @@ func (x *Pong) ProtoReflect() protoreflect.Message {
// Deprecated: Use Pong.ProtoReflect.Descriptor instead.
func (*Pong) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{23}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{24}
}
func (x *Pong) GetClientTimeMs() int64 {
@@ -1942,7 +2019,7 @@ type RoomState struct {
func (x *RoomState) Reset() {
*x = RoomState{}
mi := &file_noitu_v1_game_proto_msgTypes[24]
mi := &file_noitu_v1_game_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1954,7 +2031,7 @@ func (x *RoomState) String() string {
func (*RoomState) ProtoMessage() {}
func (x *RoomState) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[24]
mi := &file_noitu_v1_game_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1967,7 +2044,7 @@ func (x *RoomState) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomState.ProtoReflect.Descriptor instead.
func (*RoomState) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{24}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{25}
}
func (x *RoomState) GetRoomCode() string {
@@ -2035,7 +2112,7 @@ type ChatMessage struct {
func (x *ChatMessage) Reset() {
*x = ChatMessage{}
mi := &file_noitu_v1_game_proto_msgTypes[25]
mi := &file_noitu_v1_game_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2047,7 +2124,7 @@ func (x *ChatMessage) String() string {
func (*ChatMessage) ProtoMessage() {}
func (x *ChatMessage) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[25]
mi := &file_noitu_v1_game_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2060,7 +2137,7 @@ func (x *ChatMessage) ProtoReflect() protoreflect.Message {
// Deprecated: Use ChatMessage.ProtoReflect.Descriptor instead.
func (*ChatMessage) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{25}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{26}
}
func (x *ChatMessage) GetFromMe() bool {
@@ -2114,7 +2191,7 @@ type ChatHistory struct {
func (x *ChatHistory) Reset() {
*x = ChatHistory{}
mi := &file_noitu_v1_game_proto_msgTypes[26]
mi := &file_noitu_v1_game_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2126,7 +2203,7 @@ func (x *ChatHistory) String() string {
func (*ChatHistory) ProtoMessage() {}
func (x *ChatHistory) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[26]
mi := &file_noitu_v1_game_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2139,7 +2216,7 @@ func (x *ChatHistory) ProtoReflect() protoreflect.Message {
// Deprecated: Use ChatHistory.ProtoReflect.Descriptor instead.
func (*ChatHistory) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{26}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{27}
}
func (x *ChatHistory) GetMessages() []*ChatMessage {
@@ -2171,7 +2248,7 @@ type ServerMessage struct {
func (x *ServerMessage) Reset() {
*x = ServerMessage{}
mi := &file_noitu_v1_game_proto_msgTypes[27]
mi := &file_noitu_v1_game_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2183,7 +2260,7 @@ func (x *ServerMessage) String() string {
func (*ServerMessage) ProtoMessage() {}
func (x *ServerMessage) ProtoReflect() protoreflect.Message {
mi := &file_noitu_v1_game_proto_msgTypes[27]
mi := &file_noitu_v1_game_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2196,7 +2273,7 @@ func (x *ServerMessage) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerMessage.ProtoReflect.Descriptor instead.
func (*ServerMessage) Descriptor() ([]byte, []int) {
return file_noitu_v1_game_proto_rawDescGZIP(), []int{27}
return file_noitu_v1_game_proto_rawDescGZIP(), []int{28}
}
func (x *ServerMessage) GetPayload() isServerMessage_Payload {
@@ -2433,7 +2510,7 @@ const file_noitu_v1_game_proto_rawDesc = "" +
"session_id\x18\x01 \x01(\tR\tsessionId\x12!\n" +
"\fresume_token\x18\x02 \x01(\tR\vresumeToken\x12)\n" +
"\x10protocol_version\x18\x03 \x01(\rR\x0fprotocolVersion\x12+\n" +
"\x11accepted_nickname\x18\x04 \x01(\tR\x10acceptedNickname\"\x9e\x01\n" +
"\x11accepted_nickname\x18\x04 \x01(\tR\x10acceptedNickname\"\xcb\x01\n" +
"\n" +
"PlayedWord\x12\x12\n" +
"\x04word\x18\x01 \x01(\tR\x04word\x12\x13\n" +
@@ -2441,7 +2518,11 @@ const file_noitu_v1_game_proto_rawDesc = "" +
"\x06points\x18\x03 \x01(\rR\x06points\x12\x1c\n" +
"\tsyllables\x18\x04 \x01(\rR\tsyllables\x12\x14\n" +
"\x05typed\x18\x05 \x01(\tR\x05typed\x12\x1b\n" +
"\tplayer_id\x18\x06 \x01(\tR\bplayerId\"\xb5\x01\n" +
"\tplayer_id\x18\x06 \x01(\tR\bplayerId\x12+\n" +
"\bmeanings\x18\a \x03(\v2\x0f.noitu.v1.SenseR\bmeanings\"/\n" +
"\x05Sense\x12\x10\n" +
"\x03pos\x18\x01 \x01(\tR\x03pos\x12\x14\n" +
"\x05gloss\x18\x02 \x01(\tR\x05gloss\"\xb5\x01\n" +
"\n" +
"PlayerSlot\x12\x1b\n" +
"\tplayer_id\x18\x01 \x01(\tR\bplayerId\x12\x12\n" +
@@ -2460,7 +2541,7 @@ const file_noitu_v1_game_proto_rawDesc = "" +
"eliminated\x18\x05 \x01(\bR\n" +
"eliminated\x12\x1c\n" +
"\tconnected\x18\x06 \x01(\bR\tconnected\x12\x12\n" +
"\x04rank\x18\a \x01(\rR\x04rank\"\xb4\x02\n" +
"\x04rank\x18\a \x01(\rR\x04rank\"\xf0\x02\n" +
"\vGameStarted\x12!\n" +
"\fopening_word\x18\x01 \x01(\tR\vopeningWord\x12)\n" +
"\x10current_syllable\x18\x02 \x01(\tR\x0fcurrentSyllable\x12\x17\n" +
@@ -2469,7 +2550,8 @@ const file_noitu_v1_game_proto_rawDesc = "" +
"\bturn_seq\x18\x05 \x01(\rR\aturnSeq\x12\"\n" +
"\rturn_limit_ms\x18\x06 \x01(\rR\vturnLimitMs\x12/\n" +
"\aplayers\x18\a \x03(\v2\x15.noitu.v1.PlayerScoreR\aplayers\x12$\n" +
"\x0eturn_player_id\x18\b \x01(\tR\fturnPlayerId\"\xc9\x02\n" +
"\x0eturn_player_id\x18\b \x01(\tR\fturnPlayerId\x12:\n" +
"\x10opening_meanings\x18\t \x03(\v2\x0f.noitu.v1.SenseR\x0fopeningMeanings\"\xc9\x02\n" +
"\n" +
"TurnUpdate\x12,\n" +
"\x06played\x18\x01 \x01(\v2\x14.noitu.v1.PlayedWordR\x06played\x12)\n" +
@@ -2572,7 +2654,7 @@ func file_noitu_v1_game_proto_rawDescGZIP() []byte {
}
var file_noitu_v1_game_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_noitu_v1_game_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_noitu_v1_game_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_noitu_v1_game_proto_goTypes = []any{
(Difficulty)(0), // 0: noitu.v1.Difficulty
(RejectReason)(0), // 1: noitu.v1.RejectReason
@@ -2592,19 +2674,20 @@ var file_noitu_v1_game_proto_goTypes = []any{
(*ClientMessage)(nil), // 15: noitu.v1.ClientMessage
(*Welcome)(nil), // 16: noitu.v1.Welcome
(*PlayedWord)(nil), // 17: noitu.v1.PlayedWord
(*PlayerSlot)(nil), // 18: noitu.v1.PlayerSlot
(*PlayerScore)(nil), // 19: noitu.v1.PlayerScore
(*GameStarted)(nil), // 20: noitu.v1.GameStarted
(*TurnUpdate)(nil), // 21: noitu.v1.TurnUpdate
(*MoveRejected)(nil), // 22: noitu.v1.MoveRejected
(*GameOver)(nil), // 23: noitu.v1.GameOver
(*PlayerEliminated)(nil), // 24: noitu.v1.PlayerEliminated
(*ServerError)(nil), // 25: noitu.v1.ServerError
(*Pong)(nil), // 26: noitu.v1.Pong
(*RoomState)(nil), // 27: noitu.v1.RoomState
(*ChatMessage)(nil), // 28: noitu.v1.ChatMessage
(*ChatHistory)(nil), // 29: noitu.v1.ChatHistory
(*ServerMessage)(nil), // 30: noitu.v1.ServerMessage
(*Sense)(nil), // 18: noitu.v1.Sense
(*PlayerSlot)(nil), // 19: noitu.v1.PlayerSlot
(*PlayerScore)(nil), // 20: noitu.v1.PlayerScore
(*GameStarted)(nil), // 21: noitu.v1.GameStarted
(*TurnUpdate)(nil), // 22: noitu.v1.TurnUpdate
(*MoveRejected)(nil), // 23: noitu.v1.MoveRejected
(*GameOver)(nil), // 24: noitu.v1.GameOver
(*PlayerEliminated)(nil), // 25: noitu.v1.PlayerEliminated
(*ServerError)(nil), // 26: noitu.v1.ServerError
(*Pong)(nil), // 27: noitu.v1.Pong
(*RoomState)(nil), // 28: noitu.v1.RoomState
(*ChatMessage)(nil), // 29: noitu.v1.ChatMessage
(*ChatHistory)(nil), // 30: noitu.v1.ChatHistory
(*ServerMessage)(nil), // 31: noitu.v1.ServerMessage
}
var file_noitu_v1_game_proto_depIdxs = []int32{
0, // 0: noitu.v1.StartBotGame.difficulty:type_name -> noitu.v1.Difficulty
@@ -2620,31 +2703,33 @@ var file_noitu_v1_game_proto_depIdxs = []int32{
11, // 10: noitu.v1.ClientMessage.kick_player:type_name -> noitu.v1.KickPlayer
12, // 11: noitu.v1.ClientMessage.leave_room:type_name -> noitu.v1.LeaveRoom
13, // 12: noitu.v1.ClientMessage.send_chat:type_name -> noitu.v1.SendChat
19, // 13: noitu.v1.GameStarted.players:type_name -> noitu.v1.PlayerScore
17, // 14: noitu.v1.TurnUpdate.played:type_name -> noitu.v1.PlayedWord
19, // 15: noitu.v1.TurnUpdate.players:type_name -> noitu.v1.PlayerScore
1, // 16: noitu.v1.MoveRejected.reason:type_name -> noitu.v1.RejectReason
2, // 17: noitu.v1.GameOver.reason:type_name -> noitu.v1.GameEndReason
19, // 18: noitu.v1.GameOver.standings:type_name -> noitu.v1.PlayerScore
2, // 19: noitu.v1.PlayerEliminated.reason:type_name -> noitu.v1.GameEndReason
18, // 20: noitu.v1.RoomState.players:type_name -> noitu.v1.PlayerSlot
28, // 21: noitu.v1.ChatHistory.messages:type_name -> noitu.v1.ChatMessage
16, // 22: noitu.v1.ServerMessage.welcome:type_name -> noitu.v1.Welcome
20, // 23: noitu.v1.ServerMessage.game_started:type_name -> noitu.v1.GameStarted
21, // 24: noitu.v1.ServerMessage.turn_update:type_name -> noitu.v1.TurnUpdate
22, // 25: noitu.v1.ServerMessage.move_rejected:type_name -> noitu.v1.MoveRejected
23, // 26: noitu.v1.ServerMessage.game_over:type_name -> noitu.v1.GameOver
25, // 27: noitu.v1.ServerMessage.error:type_name -> noitu.v1.ServerError
26, // 28: noitu.v1.ServerMessage.pong:type_name -> noitu.v1.Pong
27, // 29: noitu.v1.ServerMessage.room_state:type_name -> noitu.v1.RoomState
28, // 30: noitu.v1.ServerMessage.chat_message:type_name -> noitu.v1.ChatMessage
29, // 31: noitu.v1.ServerMessage.chat_history:type_name -> noitu.v1.ChatHistory
24, // 32: noitu.v1.ServerMessage.player_eliminated:type_name -> noitu.v1.PlayerEliminated
33, // [33:33] is the sub-list for method output_type
33, // [33:33] is the sub-list for method input_type
33, // [33:33] is the sub-list for extension type_name
33, // [33:33] is the sub-list for extension extendee
0, // [0:33] is the sub-list for field type_name
18, // 13: noitu.v1.PlayedWord.meanings:type_name -> noitu.v1.Sense
20, // 14: noitu.v1.GameStarted.players:type_name -> noitu.v1.PlayerScore
18, // 15: noitu.v1.GameStarted.opening_meanings:type_name -> noitu.v1.Sense
17, // 16: noitu.v1.TurnUpdate.played:type_name -> noitu.v1.PlayedWord
20, // 17: noitu.v1.TurnUpdate.players:type_name -> noitu.v1.PlayerScore
1, // 18: noitu.v1.MoveRejected.reason:type_name -> noitu.v1.RejectReason
2, // 19: noitu.v1.GameOver.reason:type_name -> noitu.v1.GameEndReason
20, // 20: noitu.v1.GameOver.standings:type_name -> noitu.v1.PlayerScore
2, // 21: noitu.v1.PlayerEliminated.reason:type_name -> noitu.v1.GameEndReason
19, // 22: noitu.v1.RoomState.players:type_name -> noitu.v1.PlayerSlot
29, // 23: noitu.v1.ChatHistory.messages:type_name -> noitu.v1.ChatMessage
16, // 24: noitu.v1.ServerMessage.welcome:type_name -> noitu.v1.Welcome
21, // 25: noitu.v1.ServerMessage.game_started:type_name -> noitu.v1.GameStarted
22, // 26: noitu.v1.ServerMessage.turn_update:type_name -> noitu.v1.TurnUpdate
23, // 27: noitu.v1.ServerMessage.move_rejected:type_name -> noitu.v1.MoveRejected
24, // 28: noitu.v1.ServerMessage.game_over:type_name -> noitu.v1.GameOver
26, // 29: noitu.v1.ServerMessage.error:type_name -> noitu.v1.ServerError
27, // 30: noitu.v1.ServerMessage.pong:type_name -> noitu.v1.Pong
28, // 31: noitu.v1.ServerMessage.room_state:type_name -> noitu.v1.RoomState
29, // 32: noitu.v1.ServerMessage.chat_message:type_name -> noitu.v1.ChatMessage
30, // 33: noitu.v1.ServerMessage.chat_history:type_name -> noitu.v1.ChatHistory
25, // 34: noitu.v1.ServerMessage.player_eliminated:type_name -> noitu.v1.PlayerEliminated
35, // [35:35] is the sub-list for method output_type
35, // [35:35] is the sub-list for method input_type
35, // [35:35] is the sub-list for extension type_name
35, // [35:35] is the sub-list for extension extendee
0, // [0:35] is the sub-list for field type_name
}
func init() { file_noitu_v1_game_proto_init() }
@@ -2666,7 +2751,7 @@ func file_noitu_v1_game_proto_init() {
(*ClientMessage_LeaveRoom)(nil),
(*ClientMessage_SendChat)(nil),
}
file_noitu_v1_game_proto_msgTypes[27].OneofWrappers = []any{
file_noitu_v1_game_proto_msgTypes[28].OneofWrappers = []any{
(*ServerMessage_Welcome)(nil),
(*ServerMessage_GameStarted)(nil),
(*ServerMessage_TurnUpdate)(nil),
@@ -2685,7 +2770,7 @@ func file_noitu_v1_game_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_noitu_v1_game_proto_rawDesc), len(file_noitu_v1_game_proto_rawDesc)),
NumEnums: 3,
NumMessages: 28,
NumMessages: 29,
NumExtensions: 0,
NumServices: 0,
},
+17 -2
View File
@@ -13,6 +13,7 @@ import (
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
"github.com/tiennm99dev/noitu/server/internal/bot"
"github.com/tiennm99dev/noitu/server/internal/dictionary"
"github.com/tiennm99dev/noitu/server/internal/game"
)
@@ -100,13 +101,27 @@ func Difficulty(d noituv1.Difficulty) (bot.Difficulty, bool) {
//
// byMe is the caller's business: the same move is sent to both players and
// only this flag differs, so the room serializes one message per player rather
// than broadcasting a single shared frame.
func PlayedWord(m game.Move, byMe bool) *noituv1.PlayedWord {
// than broadcasting a single shared frame. meanings is the word's senses from
// the dictionary, the same for every recipient; the room looks them up once
// per move and passes them in.
func PlayedWord(m game.Move, byMe bool, meanings []dictionary.Sense) *noituv1.PlayedWord {
return &noituv1.PlayedWord{
Word: m.Word,
Typed: m.Typed,
ByMe: byMe,
Points: uint32(m.Points),
Syllables: uint32(m.Syllables),
Meanings: Senses(meanings),
}
}
// Senses renders a word's dictionary senses for the wire. A word with none
// yields an empty list, which the client shows as "no meaning yet" rather than
// as a row without a panel.
func Senses(senses []dictionary.Sense) []*noituv1.Sense {
out := make([]*noituv1.Sense, 0, len(senses))
for _, s := range senses {
out = append(out, &noituv1.Sense{Pos: s.Pos, Gloss: s.Gloss})
}
return out
}
+2 -2
View File
@@ -172,14 +172,14 @@ func TestDifficultyMapping(t *testing.T) {
func TestPlayedWordKeepsTypedInput(t *testing.T) {
m := game.Move{Word: "hòa bình", Typed: "hoà bình", Syllables: 2, Points: 2}
mine := PlayedWord(m, true)
mine := PlayedWord(m, true, nil)
if mine.GetWord() != "hòa bình" || mine.GetTyped() != "hoà bình" {
t.Errorf("canonical/typed pair lost: word=%q typed=%q", mine.GetWord(), mine.GetTyped())
}
if !mine.GetByMe() || mine.GetSyllables() != 2 || mine.GetPoints() != 2 {
t.Errorf("unexpected rendering: %+v", mine)
}
if theirs := PlayedWord(m, false); theirs.GetByMe() {
if theirs := PlayedWord(m, false, nil); theirs.GetByMe() {
t.Error("by_me must be false for the opponent's copy of the same move")
}
}
+113
View File
@@ -0,0 +1,113 @@
package wsapi
import (
"testing"
"time"
"github.com/coder/websocket"
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
)
// definedChainDict is the forced chain with a meaning on the opening word and
// on the human's first reply, and none on the rest: both shapes must cross
// the wire, because the client shows a panel for every word.
func definedChainDict() *testDict {
return chainDict().
define("a b", "danh từ", "Từ mở đầu.").
define("b c", "động từ", "Nối tiếp.").
define("b c", "", "Nghĩa thứ hai.")
}
func assertSenses(t *testing.T, got []*noituv1.Sense, want ...[2]string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("got %d senses %v, want %d", len(got), got, len(want))
}
for i, w := range want {
if got[i].GetPos() != w[0] || got[i].GetGloss() != w[1] {
t.Errorf("sense %d = (%q, %q), want (%q, %q)", i, got[i].GetPos(), got[i].GetGloss(), w[0], w[1])
}
}
}
// TestBotGameCarriesMeanings checks the two messages that introduce a word to
// the client: the opening in GameStarted and each move in TurnUpdate, for a
// word with senses and for one without.
func TestBotGameCarriesMeanings(t *testing.T) {
_, url := newTestServer(t, definedChainDict(), Config{})
c := dial(t, url)
c.hello("Ăn")
c.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_StartBotGame{
StartBotGame: &noituv1.StartBotGame{Difficulty: noituv1.Difficulty_DIFFICULTY_EASY},
}})
started := c.await("game_started").GetGameStarted()
assertSenses(t, started.GetOpeningMeanings(), [2]string{"danh từ", "Từ mở đầu."})
c.submit("b c", started.GetTurnSeq())
own := c.await("turn_update").GetTurnUpdate()
if !own.GetPlayed().GetByMe() {
t.Fatal("first update should be the player's own move")
}
assertSenses(t, own.GetPlayed().GetMeanings(), [2]string{"động từ", "Nối tiếp."}, [2]string{"", "Nghĩa thứ hai."})
// The bot's only move, "c d", has no definition: an empty list, not a
// missing message.
reply := c.await("turn_update").GetTurnUpdate()
if reply.GetPlayed().GetWord() != "c d" {
t.Fatalf("bot played %q, want c d", reply.GetPlayed().GetWord())
}
if n := len(reply.GetPlayed().GetMeanings()); n != 0 {
t.Errorf("a word without a definition carried %d senses", n)
}
}
// TestResumeReplaysMeanings drops a player after a move and checks that the
// replayed opening and last move both carry their senses, with no code path
// of their own.
func TestResumeReplaysMeanings(t *testing.T) {
_, url := newTestServer(t, definedChainDict(), Config{TurnLimit: 10 * time.Second, GraceFor: 5 * time.Second})
host := dial(t, url)
welcome := host.hello("Chủ phòng")
host.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_CreateRoom{CreateRoom: &noituv1.CreateRoom{}}})
code := host.await("room_state").GetRoomState().GetRoomCode()
guest := dial(t, url)
guest.hello("Khách")
guest.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_JoinRoom{
JoinRoom: &noituv1.JoinRoom{RoomCode: code},
}})
readyAndStart(t, host, guest)
hostStart := host.await("game_started").GetGameStarted()
guest.await("game_started")
// Whoever was dealt the turn plays the only legal word.
lead := guest
if hostStart.GetMyTurn() {
lead = host
}
lead.submit("b c", hostStart.GetTurnSeq())
host.await("turn_update")
guest.await("turn_update")
_ = host.conn.Close(websocket.StatusGoingAway, "")
guest.await("room_state")
back := dial(t, url)
back.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Hello{Hello: &noituv1.Hello{
ProtocolVersion: ProtocolVersion,
Nickname: "Chủ phòng",
ResumeToken: welcome.GetResumeToken(),
}}})
back.await("welcome")
restored := back.await("game_started").GetGameStarted()
assertSenses(t, restored.GetOpeningMeanings(), [2]string{"danh từ", "Từ mở đầu."})
replayed := back.await("turn_update").GetTurnUpdate()
if replayed.GetPlayed().GetWord() != "b c" {
t.Fatalf("replayed move = %q, want b c", replayed.GetPlayed().GetWord())
}
assertSenses(t, replayed.GetPlayed().GetMeanings(), [2]string{"động từ", "Nối tiếp."}, [2]string{"", "Nghĩa thứ hai."})
}
+23 -6
View File
@@ -10,6 +10,7 @@ import (
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
"github.com/tiennm99dev/noitu/server/internal/bot"
"github.com/tiennm99dev/noitu/server/internal/dictionary"
"github.com/tiennm99dev/noitu/server/internal/game"
)
@@ -259,7 +260,9 @@ type room struct {
}
// Dictionary is everything the transport layer needs from the wordlist: the
// engine's own contract, plus a way to pick an opening.
// engine's own contract, a way to pick an opening, and the meanings a word
// travels to the client with. The engine never sees a meaning; only the room
// attaches them, where it renders a word for a recipient.
//
// An interface rather than *dictionary.Store so a test can play a whole game
// against a hand-built graph of a dozen words, where the expected outcome is
@@ -268,6 +271,8 @@ type room struct {
type Dictionary interface {
game.Dictionary
RandomOpeningWord(minOutDegree int) (string, error)
// Meanings returns a canonical word's senses in order, nil for none.
Meanings(word string) []dictionary.Sense
}
func newRoom(h *hub, code string, turnLimit, graceFor, idleFor time.Duration) *room {
@@ -749,6 +754,7 @@ func (r *room) sendGameStarted(s *seat, state game.State) {
s.sess.send(&noituv1.ServerMessage{Payload: &noituv1.ServerMessage_GameStarted{
GameStarted: &noituv1.GameStarted{
OpeningWord: r.opening,
OpeningMeanings: Senses(r.dict.Meanings(r.opening)),
CurrentSyllable: state.Current,
MyTurn: state.Turn == s.id,
DeadlineUnixMs: state.Deadline.UnixMilli(),
@@ -879,14 +885,25 @@ func (r *room) maybeScheduleBot() {
// new player to act.
func (r *room) broadcastTurn(move *game.Move) {
state := r.engine.Snapshot()
meanings := r.moveMeanings(move)
for _, s := range r.seats {
r.sendTurnUpdate(s, state, move)
r.sendTurnUpdate(s, state, move, meanings)
}
}
// moveMeanings looks up a played word's senses once per move; they are the
// same for every recipient. nil for no move.
func (r *room) moveMeanings(move *game.Move) []dictionary.Sense {
if move == nil {
return nil
}
return r.dict.Meanings(move.Word)
}
// sendTurnUpdate renders one position for one seat. by_me, my_turn and is_me
// are all per-recipient, which is why there is no single shared frame.
func (r *room) sendTurnUpdate(s *seat, state game.State, move *game.Move) {
// are all per-recipient, which is why there is no single shared frame; the
// move's meanings are not, and arrive looked up.
func (r *room) sendTurnUpdate(s *seat, state game.State, move *game.Move, meanings []dictionary.Sense) {
if s == nil || s.sess == nil {
return
}
@@ -900,7 +917,7 @@ func (r *room) sendTurnUpdate(s *seat, state game.State, move *game.Move) {
TurnPlayerId: string(state.Turn),
}
if move != nil {
update.Played = PlayedWord(*move, move.Player == s.id)
update.Played = PlayedWord(*move, move.Player == s.id, meanings)
}
s.sess.send(&noituv1.ServerMessage{Payload: &noituv1.ServerMessage_TurnUpdate{TurnUpdate: update}})
}
@@ -1191,7 +1208,7 @@ func (r *room) handleResume(m resumeInput) {
r.sendGameStarted(s, state)
if len(state.History) > 0 {
last := state.History[len(state.History)-1]
r.sendTurnUpdate(s, state, &last)
r.sendTurnUpdate(s, state, &last, r.moveMeanings(&last))
}
}
+5
View File
@@ -72,6 +72,7 @@ func serverVariants() map[string]*noituv1.ServerMessage {
}}},
"server_game_started": {Payload: &noituv1.ServerMessage_GameStarted{GameStarted: &noituv1.GameStarted{
OpeningWord: "hòa bình",
OpeningMeanings: []*noituv1.Sense{{Pos: "danh từ", Gloss: "Tình trạng không có chiến tranh."}},
CurrentSyllable: "bình",
MyTurn: true,
DeadlineUnixMs: 1756998020000,
@@ -88,6 +89,10 @@ func serverVariants() map[string]*noituv1.ServerMessage {
Points: 2,
Syllables: 2,
PlayerId: "p2",
Meanings: []*noituv1.Sense{
{Pos: "tính từ", Gloss: "Yên lành, không có gì xáo động."},
{Pos: "", Gloss: "Xem bình an."},
},
},
CurrentSyllable: "yên",
MyTurn: true,
+13 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/coder/websocket"
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
"github.com/tiennm99dev/noitu/server/internal/bot"
"github.com/tiennm99dev/noitu/server/internal/dictionary"
"github.com/tiennm99dev/noitu/server/internal/game"
"google.golang.org/protobuf/proto"
)
@@ -30,10 +31,13 @@ type testDict struct {
// words maps a canonical word to its first and last syllable.
words map[string][2]string
opening string
// senses holds the meanings of the few words a test gives one to; every
// other word has none, which is also a case the client must handle.
senses map[string][]dictionary.Sense
}
func newTestDict(opening string, words ...string) *testDict {
d := &testDict{words: map[string][2]string{}, opening: opening}
d := &testDict{words: map[string][2]string{}, opening: opening, senses: map[string][]dictionary.Sense{}}
for _, w := range append(words, opening) {
parts := strings.Fields(w)
d.words[w] = [2]string{parts[0], parts[len(parts)-1]}
@@ -41,6 +45,14 @@ func newTestDict(opening string, words ...string) *testDict {
return d
}
// define gives a word one sense, so a test can see it arrive on the wire.
func (d *testDict) define(word, pos, gloss string) *testDict {
d.senses[word] = append(d.senses[word], dictionary.Sense{Pos: pos, Gloss: gloss})
return d
}
func (d *testDict) Meanings(word string) []dictionary.Sense { return d.senses[word] }
func (d *testDict) Resolve(word string) (string, bool) {
_, ok := d.words[word]
return word, ok
+43
View File
@@ -411,6 +411,15 @@ export declare type PlayedWord = Message<"noitu.v1.PlayedWord"> & {
* @generated from field: string player_id = 6;
*/
playerId: string;
/**
* What the word means, at most five senses. Empty when the dictionary has
* no definition for the word; the client then says so rather than hiding
* the row's panel, so every word in the chain behaves the same.
*
* @generated from field: repeated noitu.v1.Sense meanings = 7;
*/
meanings: Sense[];
};
/**
@@ -419,6 +428,33 @@ export declare type PlayedWord = Message<"noitu.v1.PlayedWord"> & {
*/
export declare const PlayedWordSchema: GenMessage<PlayedWord>;
/**
* Sense is one definition of a word as Wiktionary gives it: the part of
* speech it sits under, in Vietnamese ("danh từ"), empty when the heading was
* not one the builder knows, and the definition stripped of markup at at most
* 200 characters. Plain text both: the client renders them as text, never as
* markup.
*
* @generated from message noitu.v1.Sense
*/
export declare type Sense = Message<"noitu.v1.Sense"> & {
/**
* @generated from field: string pos = 1;
*/
pos: string;
/**
* @generated from field: string gloss = 2;
*/
gloss: string;
};
/**
* Describes the message noitu.v1.Sense.
* Use `create(SenseSchema)` to create a new message.
*/
export declare const SenseSchema: GenMessage<Sense>;
/**
* PlayerSlot is one seat in the room, rendered for one recipient.
*
@@ -600,6 +636,13 @@ export declare type GameStarted = Message<"noitu.v1.GameStarted"> & {
* @generated from field: string turn_player_id = 8;
*/
turnPlayerId: string;
/**
* The opening word's senses, as PlayedWord.meanings for a played word.
*
* @generated from field: repeated noitu.v1.Sense opening_meanings = 9;
*/
openingMeanings: Sense[];
};
/**
+21 -14
View File
@@ -21,7 +21,7 @@ import { enumDesc, fileDesc, messageDesc, tsEnum } from "@bufbuild/protobuf/code
* Describes the file noitu/v1/game.proto.
*/
export const file_noitu_v1_game = /*@__PURE__*/
fileDesc("ChNub2l0dS92MS9nYW1lLnByb3RvEghub2l0dS52MSJJCgVIZWxsbxIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgNEhQKDHJlc3VtZV90b2tlbhgCIAEoCRIQCghuaWNrbmFtZRgDIAEoCSI4CgxTdGFydEJvdEdhbWUSKAoKZGlmZmljdWx0eRgBIAEoDjIULm5vaXR1LnYxLkRpZmZpY3VsdHkiDAoKQ3JlYXRlUm9vbSIdCghKb2luUm9vbRIRCglyb29tX2NvZGUYASABKAkiLAoKU3VibWl0V29yZBIMCgR3b3JkGAEgASgJEhAKCHR1cm5fc2VxGAIgASgNIggKBlJlc2lnbiIZCghTZXRSZWFkeRINCgVyZWFkeRgBIAEoCCILCglTdGFydEdhbWUiHwoKS2lja1BsYXllchIRCglwbGF5ZXJfaWQYASABKAkiCwoJTGVhdmVSb29tIhgKCFNlbmRDaGF0EgwKBHRleHQYASABKAkiHgoEUGluZxIWCg5jbGllbnRfdGltZV9tcxgBIAEoAyKQBAoNQ2xpZW50TWVzc2FnZRIgCgVoZWxsbxgBIAEoCzIPLm5vaXR1LnYxLkhlbGxvSAASMAoOc3RhcnRfYm90X2dhbWUYAiABKAsyFi5ub2l0dS52MS5TdGFydEJvdEdhbWVIABIrCgtjcmVhdGVfcm9vbRgDIAEoCzIULm5vaXR1LnYxLkNyZWF0ZVJvb21IABInCglqb2luX3Jvb20YBCABKAsyEi5ub2l0dS52MS5Kb2luUm9vbUgAEisKC3N1Ym1pdF93b3JkGAUgASgLMhQubm9pdHUudjEuU3VibWl0V29yZEgAEiIKBnJlc2lnbhgGIAEoCzIQLm5vaXR1LnYxLlJlc2lnbkgAEh4KBHBpbmcYByABKAsyDi5ub2l0dS52MS5QaW5nSAASJwoJc2V0X3JlYWR5GAkgASgLMhIubm9pdHUudjEuU2V0UmVhZHlIABIpCgpzdGFydF9nYW1lGAogASgLMhMubm9pdHUudjEuU3RhcnRHYW1lSAASKwoLa2lja19wbGF5ZXIYCyABKAsyFC5ub2l0dS52MS5LaWNrUGxheWVySAASKQoKbGVhdmVfcm9vbRgMIAEoCzITLm5vaXR1LnYxLkxlYXZlUm9vbUgAEicKCXNlbmRfY2hhdBgNIAEoCzISLm5vaXR1LnYxLlNlbmRDaGF0SABCCQoHcGF5bG9hZEoECAgQCSJoCgdXZWxjb21lEhIKCnNlc3Npb25faWQYASABKAkSFAoMcmVzdW1lX3Rva2VuGAIgASgJEhgKEHByb3RvY29sX3ZlcnNpb24YAyABKA0SGQoRYWNjZXB0ZWRfbmlja25hbWUYBCABKAkibgoKUGxheWVkV29yZBIMCgR3b3JkGAEgASgJEg0KBWJ5X21lGAIgASgIEg4KBnBvaW50cxgDIAEoDRIRCglzeWxsYWJsZXMYBCABKA0SDQoFdHlwZWQYBSABKAkSEQoJcGxheWVyX2lkGAYgASgJIn4KClBsYXllclNsb3QSEQoJcGxheWVyX2lkGAEgASgJEgwKBG5hbWUYAiABKAkSDQoFaXNfbWUYAyABKAgSEAoIaXNfb3duZXIYBCABKAgSDQoFcmVhZHkYBSABKAgSEQoJY29ubmVjdGVkGAYgASgIEgwKBHdpbnMYByABKA0igQEKC1BsYXllclNjb3JlEhEKCXBsYXllcl9pZBgBIAEoCRIMCgRuYW1lGAIgASgJEg0KBWlzX21lGAMgASgIEg0KBXNjb3JlGAQgASgNEhIKCmVsaW1pbmF0ZWQYBSABKAgSEQoJY29ubmVjdGVkGAYgASgIEgwKBHJhbmsYByABKA0i0QEKC0dhbWVTdGFydGVkEhQKDG9wZW5pbmdfd29yZBgBIAEoCRIYChBjdXJyZW50X3N5bGxhYmxlGAIgASgJEg8KB215X3R1cm4YAyABKAgSGAoQZGVhZGxpbmVfdW5peF9tcxgEIAEoAxIQCgh0dXJuX3NlcRgFIAEoDRIVCg10dXJuX2xpbWl0X21zGAYgASgNEiYKB3BsYXllcnMYByADKAsyFS5ub2l0dS52MS5QbGF5ZXJTY29yZRIWCg50dXJuX3BsYXllcl9pZBgIIAEoCSLrAQoKVHVyblVwZGF0ZRIkCgZwbGF5ZWQYASABKAsyFC5ub2l0dS52MS5QbGF5ZWRXb3JkEhgKEGN1cnJlbnRfc3lsbGFibGUYAiABKAkSDwoHbXlfdHVybhgDIAEoCBIYChBkZWFkbGluZV91bml4X21zGAQgASgDEhAKCHR1cm5fc2VxGAUgASgNEhQKDGNoYWluX2xlbmd0aBgIIAEoDRImCgdwbGF5ZXJzGAkgAygLMhUubm9pdHUudjEuUGxheWVyU2NvcmUSFgoOdHVybl9wbGF5ZXJfaWQYCiABKAlKBAgGEAdKBAgHEAgiVgoMTW92ZVJlamVjdGVkEiYKBnJlYXNvbhgBIAEoDjIWLm5vaXR1LnYxLlJlamVjdFJlYXNvbhIMCgR3b3JkGAIgASgJEhAKCHR1cm5fc2VxGAMgASgNIo4BCghHYW1lT3ZlchINCgVpX3dvbhgBIAEoCBInCgZyZWFzb24YAiABKA4yFy5ub2l0dS52MS5HYW1lRW5kUmVhc29uEhQKDGNoYWluX2xlbmd0aBgEIAEoDRIoCglzdGFuZGluZ3MYBiADKAsyFS5ub2l0dS52MS5QbGF5ZXJTY29yZUoECAMQBEoECAUQBiKAAQoQUGxheWVyRWxpbWluYXRlZBIRCglwbGF5ZXJfaWQYASABKAkSDAoEbmFtZRgCIAEoCRINCgVpc19tZRgDIAEoCBInCgZyZWFzb24YBCABKA4yFy5ub2l0dS52MS5HYW1lRW5kUmVhc29uEhMKC3N1Z2dlc3Rpb25zGAUgAygJIiwKC1NlcnZlckVycm9yEgwKBGNvZGUYASABKAkSDwoHbWVzc2FnZRgCIAEoCSI2CgRQb25nEhYKDmNsaWVudF90aW1lX21zGAEgASgDEhYKDnNlcnZlcl90aW1lX21zGAIgASgDIrgBCglSb29tU3RhdGUSEQoJcm9vbV9jb2RlGAEgASgJEhEKCWNhbl9zdGFydBgDIAEoCBIlCgdwbGF5ZXJzGAkgAygLMhQubm9pdHUudjEuUGxheWVyU2xvdBITCgttYXhfcGxheWVycxgKIAEoDRITCgttaW5fcGxheWVycxgLIAEoDRIQCghncmFjZV9tcxgMIAEoDUoECAIQA0oECAQQBUoECAUQBkoECAYQB0oECAcQCEoECAgQCSJlCgtDaGF0TWVzc2FnZRIPCgdmcm9tX21lGAEgASgIEg4KBmF1dGhvchgCIAEoCRIMCgR0ZXh0GAMgASgJEhQKDHNlbnRfdW5peF9tcxgEIAEoAxIRCglwbGF5ZXJfaWQYBSABKAkiNgoLQ2hhdEhpc3RvcnkSJwoIbWVzc2FnZXMYASADKAsyFS5ub2l0dS52MS5DaGF0TWVzc2FnZSKYBAoNU2VydmVyTWVzc2FnZRIkCgd3ZWxjb21lGAEgASgLMhEubm9pdHUudjEuV2VsY29tZUgAEi0KDGdhbWVfc3RhcnRlZBgEIAEoCzIVLm5vaXR1LnYxLkdhbWVTdGFydGVkSAASKwoLdHVybl91cGRhdGUYBSABKAsyFC5ub2l0dS52MS5UdXJuVXBkYXRlSAASLwoNbW92ZV9yZWplY3RlZBgGIAEoCzIWLm5vaXR1LnYxLk1vdmVSZWplY3RlZEgAEicKCWdhbWVfb3ZlchgHIAEoCzISLm5vaXR1LnYxLkdhbWVPdmVySAASJgoFZXJyb3IYCSABKAsyFS5ub2l0dS52MS5TZXJ2ZXJFcnJvckgAEh4KBHBvbmcYCiABKAsyDi5ub2l0dS52MS5Qb25nSAASKQoKcm9vbV9zdGF0ZRgMIAEoCzITLm5vaXR1LnYxLlJvb21TdGF0ZUgAEi0KDGNoYXRfbWVzc2FnZRgNIAEoCzIVLm5vaXR1LnYxLkNoYXRNZXNzYWdlSAASLQoMY2hhdF9oaXN0b3J5GA4gASgLMhUubm9pdHUudjEuQ2hhdEhpc3RvcnlIABI3ChFwbGF5ZXJfZWxpbWluYXRlZBgPIAEoCzIaLm5vaXR1LnYxLlBsYXllckVsaW1pbmF0ZWRIAEIJCgdwYXlsb2FkSgQIAhADSgQIAxAESgQICBAJSgQICxAMKmkKCkRpZmZpY3VsdHkSGgoWRElGRklDVUxUWV9VTlNQRUNJRklFRBAAEhMKD0RJRkZJQ1VMVFlfRUFTWRABEhUKEURJRkZJQ1VMVFlfTUVESVVNEAISEwoPRElGRklDVUxUWV9IQVJEEAMqjgIKDFJlamVjdFJlYXNvbhIdChlSRUpFQ1RfUkVBU09OX1VOU1BFQ0lGSUVEEAASIwofUkVKRUNUX1JFQVNPTl9UT09fRkVXX1NZTExBQkxFUxABEhwKGFJFSkVDVF9SRUFTT05fV1JPTkdfTElOSxACEiMKH1JFSkVDVF9SRUFTT05fTk9UX0lOX0RJQ1RJT05BUlkQAxIeChpSRUpFQ1RfUkVBU09OX0FMUkVBRFlfVVNFRBAEEh8KG1JFSkVDVF9SRUFTT05fTk9UX1lPVVJfVFVSThAFEhkKFVJFSkVDVF9SRUFTT05fVElNRU9VVBAGEhsKF1JFSkVDVF9SRUFTT05fR0FNRV9PVkVSEAcqsQEKDUdhbWVFbmRSZWFzb24SHwobR0FNRV9FTkRfUkVBU09OX1VOU1BFQ0lGSUVEEAASGwoXR0FNRV9FTkRfUkVBU09OX1RJTUVPVVQQARIhCh1HQU1FX0VORF9SRUFTT05fTk9fTEVHQUxfTU9WRRACEiEKHUdBTUVfRU5EX1JFQVNPTl9PUFBPTkVLine truncated
fileDesc("ChNub2l0dS92MS9nYW1lLnByb3RvEghub2l0dS52MSJJCgVIZWxsbxIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgNEhQKDHJlc3VtZV90b2tlbhgCIAEoCRIQCghuaWNrbmFtZRgDIAEoCSI4CgxTdGFydEJvdEdhbWUSKAoKZGlmZmljdWx0eRgBIAEoDjIULm5vaXR1LnYxLkRpZmZpY3VsdHkiDAoKQ3JlYXRlUm9vbSIdCghKb2luUm9vbRIRCglyb29tX2NvZGUYASABKAkiLAoKU3VibWl0V29yZBIMCgR3b3JkGAEgASgJEhAKCHR1cm5fc2VxGAIgASgNIggKBlJlc2lnbiIZCghTZXRSZWFkeRINCgVyZWFkeRgBIAEoCCILCglTdGFydEdhbWUiHwoKS2lja1BsYXllchIRCglwbGF5ZXJfaWQYASABKAkiCwoJTGVhdmVSb29tIhgKCFNlbmRDaGF0EgwKBHRleHQYASABKAkiHgoEUGluZxIWCg5jbGllbnRfdGltZV9tcxgBIAEoAyKQBAoNQ2xpZW50TWVzc2FnZRIgCgVoZWxsbxgBIAEoCzIPLm5vaXR1LnYxLkhlbGxvSAASMAoOc3RhcnRfYm90X2dhbWUYAiABKAsyFi5ub2l0dS52MS5TdGFydEJvdEdhbWVIABIrCgtjcmVhdGVfcm9vbRgDIAEoCzIULm5vaXR1LnYxLkNyZWF0ZVJvb21IABInCglqb2luX3Jvb20YBCABKAsyEi5ub2l0dS52MS5Kb2luUm9vbUgAEisKC3N1Ym1pdF93b3JkGAUgASgLMhQubm9pdHUudjEuU3VibWl0V29yZEgAEiIKBnJlc2lnbhgGIAEoCzIQLm5vaXR1LnYxLlJlc2lnbkgAEh4KBHBpbmcYByABKAsyDi5ub2l0dS52MS5QaW5nSAASJwoJc2V0X3JlYWR5GAkgASgLMhIubm9pdHUudjEuU2V0UmVhZHlIABIpCgpzdGFydF9nYW1lGAogASgLMhMubm9pdHUudjEuU3RhcnRHYW1lSAASKwoLa2lja19wbGF5ZXIYCyABKAsyFC5ub2l0dS52MS5LaWNrUGxheWVySAASKQoKbGVhdmVfcm9vbRgMIAEoCzITLm5vaXR1LnYxLkxlYXZlUm9vbUgAEicKCXNlbmRfY2hhdBgNIAEoCzISLm5vaXR1LnYxLlNlbmRDaGF0SABCCQoHcGF5bG9hZEoECAgQCSJoCgdXZWxjb21lEhIKCnNlc3Npb25faWQYASABKAkSFAoMcmVzdW1lX3Rva2VuGAIgASgJEhgKEHByb3RvY29sX3ZlcnNpb24YAyABKA0SGQoRYWNjZXB0ZWRfbmlja25hbWUYBCABKAkikQEKClBsYXllZFdvcmQSDAoEd29yZBgBIAEoCRINCgVieV9tZRgCIAEoCBIOCgZwb2ludHMYAyABKA0SEQoJc3lsbGFibGVzGAQgASgNEg0KBXR5cGVkGAUgASgJEhEKCXBsYXllcl9pZBgGIAEoCRIhCghtZWFuaW5ncxgHIAMoCzIPLm5vaXR1LnYxLlNlbnNlIiMKBVNlbnNlEgsKA3BvcxgBIAEoCRINCgVnbG9zcxgCIAEoCSJ+CgpQbGF5ZXJTbG90EhEKCXBsYXllcl9pZBgBIAEoCRIMCgRuYW1lGAIgASgJEg0KBWlzX21lGAMgASgIEhAKCGlzX293bmVyGAQgASgIEg0KBXJlYWR5GAUgASgIEhEKCWNvbm5lY3RlZBgGIAEoCBIMCgR3aW5zGAcgASgNIoEBCgtQbGF5ZXJTY29yZRIRCglwbGF5ZXJfaWQYASABKAkSDAoEbmFtZRgCIAEoCRINCgVpc19tZRgDIAEoCBINCgVzY29yZRgEIAEoDRISCgplbGltaW5hdGVkGAUgASgIEhEKCWNvbm5lY3RlZBgGIAEoCBIMCgRyYW5rGAcgASgNIvwBCgtHYW1lU3RhcnRlZBIUCgxvcGVuaW5nX3dvcmQYASABKAkSGAoQY3VycmVudF9zeWxsYWJsZRgCIAEoCRIPCgdteV90dXJuGAMgASgIEhgKEGRlYWRsaW5lX3VuaXhfbXMYBCABKAMSEAoIdHVybl9zZXEYBSABKA0SFQoNdHVybl9saW1pdF9tcxgGIAEoDRImCgdwbGF5ZXJzGAcgAygLMhUubm9pdHUudjEuUGxheWVyU2NvcmUSFgoOdHVybl9wbGF5ZXJfaWQYCCABKAkSKQoQb3BlbmluZ19tZWFuaW5ncxgJIAMoCzIPLm5vaXR1LnYxLlNlbnNlIusBCgpUdXJuVXBkYXRlEiQKBnBsYXllZBgBIAEoCzIULm5vaXR1LnYxLlBsYXllZFdvcmQSGAoQY3VycmVudF9zeWxsYWJsZRgCIAEoCRIPCgdteV90dXJuGAMgASgIEhgKEGRlYWRsaW5lX3VuaXhfbXMYBCABKAMSEAoIdHVybl9zZXEYBSABKA0SFAoMY2hhaW5fbGVuZ3RoGAggASgNEiYKB3BsYXllcnMYCSADKAsyFS5ub2l0dS52MS5QbGF5ZXJTY29yZRIWCg50dXJuX3BsYXllcl9pZBgKIAEoCUoECAYQB0oECAcQCCJWCgxNb3ZlUmVqZWN0ZWQSJgoGcmVhc29uGAEgASgOMhYubm9pdHUudjEuUmVqZWN0UmVhc29uEgwKBHdvcmQYAiABKAkSEAoIdHVybl9zZXEYAyABKA0ijgEKCEdhbWVPdmVyEg0KBWlfd29uGAEgASgIEicKBnJlYXNvbhgCIAEoDjIXLm5vaXR1LnYxLkdhbWVFbmRSZWFzb24SFAoMY2hhaW5fbGVuZ3RoGAQgASgNEigKCXN0YW5kaW5ncxgGIAMoCzIVLm5vaXR1LnYxLlBsYXllclNjb3JlSgQIAxAESgQIBRAGIoABChBQbGF5ZXJFbGltaW5hdGVkEhEKCXBsYXllcl9pZBgBIAEoCRIMCgRuYW1lGAIgASgJEg0KBWlzX21lGAMgASgIEicKBnJlYXNvbhgEIAEoDjIXLm5vaXR1LnYxLkdhbWVFbmRSZWFzb24SEwoLc3VnZ2VzdGlvbnMYBSADKAkiLAoLU2VydmVyRXJyb3ISDAoEY29kZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIjYKBFBvbmcSFgoOY2xpZW50X3RpbWVfbXMYASABKAMSFgoOc2VydmVyX3RpbWVfbXMYAiABKAMiuAEKCVJvb21TdGF0ZRIRCglyb29tX2NvZGUYASABKAkSEQoJY2FuX3N0YXJ0GAMgASgIEiUKB3BsYXllcnMYCSADKAsyFC5ub2l0dS52MS5QbGF5ZXJTbG90EhMKC21heF9wbGF5ZXJzGAogASgNEhMKC21pbl9wbGF5ZXJzGAsgASgNEhAKCGdyYWNlX21zGAwgASgNSgQIAhADSgQIBBAFSgQIBRAGSgQIBhAHSgQIBxAISgQICBAJImUKC0NoYXRNZXNzYWdlEg8KB2Zyb21fbWUYASABKAgSDgoGYXV0aG9yGAIgASgJEgwKBHRleHQYAyABKAkSFAoMc2VudF91bml4X21zGAQgASgDEhEKCXBsYXllcl9pZBgFIAEoCSI2CgtDaGF0SGlzdG9yeRInCghtZXNzYWdlcxgBIAMoCzIVLm5vaXR1LnYxLkNoYXRNZXNzYWdlIpgECg1TZXJ2ZXJNZXNzYWdlEiQKB3dlbGNvbWUYASABKAsyES5ub2l0dS52MS5XZWxjb21lSAASLQoMZ2FtZV9zdGFydGVkGAQgASgLMhUubm9pdHUudjEuR2FtZVN0YXJ0ZWRIABIrCgt0dXJuX3VwZGF0ZRgFIAEoCzIULm5vaXR1LnYxLlR1cm5VcGRhdGVIABIvCg1tb3ZlX3JlamVjdGVkGAYgASgLMhYubm9pdHUudjEuTW92ZVJlamVjdGVkSAASJwoJZ2FtZV9vdmVyGAcgASgLMhIubm9pdHUudjEuR2FtZU92ZXJIABImCgVlcnJvchgJIAEoCzIVLm5vaXR1LnYxLlNlcnZlckVycm9ySAASHgoEcG9uZxgKIAEoCzIOLm5vaXR1LnYxLlBvbmdIABIpCgpyb29tX3N0YXRlGAwgASgLMhMubm9pdHUudjEuUm9vbVN0YXRlSAASLQoMY2hhdF9tZXNzYWdlGA0gASgLMhUubm9pdHUudjEuQ2hhdE1lc3NhZ2VIABItCgxjaGF0X2hpc3RvcnkYDiABKAsyFS5ub2l0dS52MS5DaGF0SGlzdG9yeUgAEjcKEXBsYXllcl9lbGltaW5hdGVkGA8gASgLMhoubm9pdHUudjEuUGxheWVyRWxpbWluYXRlZEgAQgkKB3BheWxvYWRKBAgCEANKBAgDEARKBAgIEAlKBAgLEAwqaQoKRGlmZmljdWx0eRIaChZESUZGSUNVTFRZX1VOU1BFQ0lGSUVEEAASEwoPRElGRklDVUxUWV9FQVNZEAESFQoRRElGRklDVUxUWV9NRURJVU0QAhITCg9ESUZGSUNVTFRZX0hBUkQQAyqOAgoMUmVqZWN0UmVhc29uEh0KGVJFSkVDVF9SRUFTT05fVU5TUEVDSUZJRUQQABIjCh9SRUpFQ1RfUkVBU09OX1RPT19GRVdfU1lMTEFCTEVTEAESHAoYUkVKRUNUX1JFQVNPTl9XUk9OR19MSU5LEAISIwofUkVKRUNUX1JFQVNPTl9OT1RfSU5fRElDVElPTkFSWRADEh4KGlJFSkVDVF9SRUFTT05fQUxSRUFEWV9VU0VEEAQSHwobUkVKRUNUX1JFQVNPTl9OT1RfWU9VUl9UVVJOEAUSGQoVUkVKRUNUX1JFQVNPTl9USU1FT1VUEAYSGwoXUkVKRUNUX1JFQVNPTl9HQU1FX09WRVIQByqxAQoNR2FtZUVuZFJlYXNvbhIfChtHQU1Line truncated
/**
* Describes the message noitu.v1.Hello.
@@ -128,96 +128,103 @@ export const WelcomeSchema = /*@__PURE__*/
export const PlayedWordSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 14);
/**
* Describes the message noitu.v1.Sense.
* Use `create(SenseSchema)` to create a new message.
*/
export const SenseSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 15);
/**
* Describes the message noitu.v1.PlayerSlot.
* Use `create(PlayerSlotSchema)` to create a new message.
*/
export const PlayerSlotSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 15);
messageDesc(file_noitu_v1_game, 16);
/**
* Describes the message noitu.v1.PlayerScore.
* Use `create(PlayerScoreSchema)` to create a new message.
*/
export const PlayerScoreSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 16);
messageDesc(file_noitu_v1_game, 17);
/**
* Describes the message noitu.v1.GameStarted.
* Use `create(GameStartedSchema)` to create a new message.
*/
export const GameStartedSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 17);
messageDesc(file_noitu_v1_game, 18);
/**
* Describes the message noitu.v1.TurnUpdate.
* Use `create(TurnUpdateSchema)` to create a new message.
*/
export const TurnUpdateSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 18);
messageDesc(file_noitu_v1_game, 19);
/**
* Describes the message noitu.v1.MoveRejected.
* Use `create(MoveRejectedSchema)` to create a new message.
*/
export const MoveRejectedSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 19);
messageDesc(file_noitu_v1_game, 20);
/**
* Describes the message noitu.v1.GameOver.
* Use `create(GameOverSchema)` to create a new message.
*/
export const GameOverSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 20);
messageDesc(file_noitu_v1_game, 21);
/**
* Describes the message noitu.v1.PlayerEliminated.
* Use `create(PlayerEliminatedSchema)` to create a new message.
*/
export const PlayerEliminatedSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 21);
messageDesc(file_noitu_v1_game, 22);
/**
* Describes the message noitu.v1.ServerError.
* Use `create(ServerErrorSchema)` to create a new message.
*/
export const ServerErrorSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 22);
messageDesc(file_noitu_v1_game, 23);
/**
* Describes the message noitu.v1.Pong.
* Use `create(PongSchema)` to create a new message.
*/
export const PongSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 23);
messageDesc(file_noitu_v1_game, 24);
/**
* Describes the message noitu.v1.RoomState.
* Use `create(RoomStateSchema)` to create a new message.
*/
export const RoomStateSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 24);
messageDesc(file_noitu_v1_game, 25);
/**
* Describes the message noitu.v1.ChatMessage.
* Use `create(ChatMessageSchema)` to create a new message.
*/
export const ChatMessageSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 25);
messageDesc(file_noitu_v1_game, 26);
/**
* Describes the message noitu.v1.ChatHistory.
* Use `create(ChatHistorySchema)` to create a new message.
*/
export const ChatHistorySchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 26);
messageDesc(file_noitu_v1_game, 27);
/**
* Describes the message noitu.v1.ServerMessage.
* Use `create(ServerMessageSchema)` to create a new message.
*/
export const ServerMessageSchema = /*@__PURE__*/
messageDesc(file_noitu_v1_game, 27);
messageDesc(file_noitu_v1_game, 28);
/**
* Describes the enum noitu.v1.Difficulty.
+16
View File
@@ -112,6 +112,22 @@ describe('generated wire types', () => {
expect(turn.turnSeq).toBe(2);
});
// The label and the definition travel as two fields, so the client decides
// how a label is shown and never has to parse it back out of the text.
it('carries a word\'s senses on the opening and on each move', () => {
const start = decode('server_game_started');
expect(start.payload.value.openingMeanings).toEqual([
expect.objectContaining({ pos: 'danh từ', gloss: 'Tình trạng không có chiến tranh.' })
]);
const turn = decode('server_turn_update');
const senses = turn.payload.value.played.meanings;
expect(senses).toHaveLength(2);
expect(senses[0].pos).toBe('tính từ');
expect(senses[1].pos).toBe('');
expect(senses[1].gloss).toBe('Xem bình an.');
});
it('decodes an empty client arm without losing its tag', () => {
const msg = decode('client_create_room');
expect(msg.payload.case).toBe('createRoom');