Add UNO game, hangman threading, DM room reuse, and gameplay fixes

- Add full UNO game plugin (1v1 vs bot via DM, community pot, bot personality)
- Move hangman into threads with direct reply guessing (no !hangman prefix needed)
- Fix DM room duplication by checking m.direct account data with in-memory cache
- Auto-draw when player has no playable cards instead of requiring manual draw
- Remove forced UNO call phase — players are penalized naturally if they forget
- Fix mutex held during network I/O in hangman start
- Fix infinite loop when both sides have no playable cards and deck is empty
- Fix Wild Draw Four penalty being skipped when UNO prompt triggered
- Fix hangman displayPhrase word boundary clarity (triple-wide gaps for spaces)
- Add UNO env vars, db tables, and README updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-11 23:52:38 -07:00
parent 992b62c51f
commit 7c9dd28021
7 changed files with 1466 additions and 63 deletions

View File

@@ -55,6 +55,12 @@ type Plugin interface {
Init() error
}
// dmCache maps user IDs to their DM room IDs to avoid creating duplicate rooms.
var (
dmCache = make(map[id.UserID]id.RoomID)
dmCacheMu sync.Mutex
)
// Base provides common helpers for plugin implementations.
type Base struct {
Client *mautrix.Client
@@ -548,8 +554,29 @@ func (b *Base) SendReact(roomID id.RoomID, eventID id.EventID, emoji string) err
return err
}
// SendDM sends a direct message to a user. Creates a DM room if needed.
func (b *Base) SendDM(userID id.UserID, text string) error {
// GetDMRoom returns the DM room for a user, creating one if needed.
func (b *Base) GetDMRoom(userID id.UserID) (id.RoomID, error) {
dmCacheMu.Lock()
if roomID, ok := dmCache[userID]; ok {
dmCacheMu.Unlock()
return roomID, nil
}
dmCacheMu.Unlock()
// Check account data for existing DM rooms
var dmRooms map[id.UserID][]id.RoomID
err := b.Client.GetAccountData(context.Background(), "m.direct", &dmRooms)
if err == nil {
if rooms, ok := dmRooms[userID]; ok && len(rooms) > 0 {
roomID := rooms[len(rooms)-1] // use most recent
dmCacheMu.Lock()
dmCache[userID] = roomID
dmCacheMu.Unlock()
return roomID, nil
}
}
// No existing DM room — create one
resp, err := b.Client.CreateRoom(context.Background(), &mautrix.ReqCreateRoom{
Preset: "trusted_private_chat",
Invite: []id.UserID{userID},
@@ -566,9 +593,30 @@ func (b *Base) SendDM(userID id.UserID, text string) error {
},
})
if err != nil {
return fmt.Errorf("create DM room: %w", err)
return "", fmt.Errorf("create DM room: %w", err)
}
return b.SendMessage(resp.RoomID, text)
dmCacheMu.Lock()
dmCache[userID] = resp.RoomID
dmCacheMu.Unlock()
return resp.RoomID, nil
}
// IsDMRoom checks if the given room is a known DM room for the given user.
func IsDMRoom(roomID id.RoomID, userID id.UserID) bool {
dmCacheMu.Lock()
defer dmCacheMu.Unlock()
cached, ok := dmCache[userID]
return ok && cached == roomID
}
// SendDM sends a direct message to a user. Reuses existing DM room if available.
func (b *Base) SendDM(userID id.UserID, text string) error {
roomID, err := b.GetDMRoom(userID)
if err != nil {
return err
}
return b.SendMessage(roomID, text)
}
// UploadContent uploads data to the Matrix content repository and returns the MXC URI.