From d137a36f5e9e1e8f6d4786a254ba85e5e3f8f69b Mon Sep 17 00:00:00 2001 From: Jason Ragsdale Date: Sat, 3 May 2025 11:48:53 -0500 Subject: [PATCH 1/3] Updated FindNoun to fix issue 356 --- internal/rooms/rooms.go | 171 +++++++++++------------------------ internal/rooms/rooms_test.go | 28 ++++++ 2 files changed, 81 insertions(+), 118 deletions(-) diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index 3e255b05..9fbef628 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -1576,151 +1576,86 @@ func (r *Room) FindContainerByName(containerNameSearch string) string { return closeMatch } +// FindNoun resolves a noun or alias within the room's noun map. func (r *Room) FindNoun(noun string) (foundNoun string, nounDescription string) { - if len(r.Nouns) == 0 { return ``, `` } - roomNouns := map[string]string{} - - for originalNoun, originalDesc := range r.Nouns { - roomNouns[originalNoun] = originalDesc - - if strings.Contains(originalNoun, ` `) { - for _, n := range strings.Split(originalNoun, ` `) { - - if _, ok := r.Nouns[n]; ok { - continue - } - - if _, ok := roomNouns[n]; ok { - continue + // Build flat map of noun -> desc, and add single-word aliases for multi-word nouns + roomNouns := make(map[string]string, len(r.Nouns)) + for key, desc := range r.Nouns { + roomNouns[key] = desc + if strings.Contains(key, ` `) { + for word := range strings.SplitSeq(key, " ") { + if _, exists := r.Nouns[word]; !exists && roomNouns[word] == "" { + roomNouns[word] = `:` + key } - - roomNouns[n] = `:` + originalNoun - } } - } - testNouns := util.SplitButRespectQuotes(noun) - ct := len(testNouns) - for i := 0; i < ct; i++ { - - if splitCount := strings.Split(testNouns[i], ` `); len(splitCount) > 1 { - - for _, n2 := range splitCount { - - if len(n2) < 2 { - continue - } - - testNouns = append(testNouns, n2) - } - + // Build candidate list: full noun first, then each word + candidates := []string{noun} + for _, part := range util.SplitButRespectQuotes(noun) { + if len(part) > 1 { + candidates = append(candidates, part) } } - // If it created more than one word, put the original back on as a full string to test - if len(testNouns) > 1 { - testNouns = append(testNouns, noun) - } - - for _, newNoun := range testNouns { - - if desc, ok := roomNouns[newNoun]; ok { - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] + // Try direct matches and aliases + for _, cand := range candidates { + if desc, ok := roomNouns[cand]; ok { + key := cand + for strings.HasPrefix(desc, `:`) { + key = desc[1:] + desc = roomNouns[key] } - return newNoun, desc - } - - if len(newNoun) < 2 { - continue + return key, desc } + } - // If ended in `s`, strip it and add a new word to the search list - if newNoun[len(newNoun)-1:] == `s` { - - testNoun := newNoun[:len(newNoun)-1] - if desc, ok := roomNouns[testNoun]; ok { - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] - } - return testNoun, desc - } - - } else { - - testNoun := newNoun + `s` - if desc, ok := roomNouns[testNoun]; ok { // `s`` at end - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] + // Fallback pluralization/singularization + for _, cand := range candidates { + // Handle "ies" -> "y" + if strings.HasSuffix(cand, `ies`) { + singular := cand[:len(cand)-3] + `y` + if desc, ok := roomNouns[singular]; ok { + key := singular + for strings.HasPrefix(desc, `:`) { + key = desc[1:] + desc = roomNouns[key] } - return testNoun, desc + return key, desc } - } - // Switch ending of `y` to `ies` - if newNoun[len(newNoun)-1:] == `y` { - - testNoun := newNoun[:len(newNoun)-1] + `ies` - if desc, ok := roomNouns[testNoun]; ok { // `ies` instead of `y` at end - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] + // Handle "es" -> remove + if strings.HasSuffix(cand, `es`) { + singular := cand[:len(cand)-2] + if desc, ok := roomNouns[singular]; ok { + key := singular + for strings.HasPrefix(desc, `:`) { + key = desc[1:] + desc = roomNouns[key] } - return testNoun, desc + return key, desc } - } - if len(newNoun) < 3 { - continue - } - - // Strip 'es' such as 'torches' - if newNoun[len(newNoun)-2:] == `es` { - - testNoun := newNoun[:len(newNoun)-2] - if desc, ok := roomNouns[testNoun]; ok { - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] + // Handle trailing 's' + if strings.HasSuffix(cand, `s`) { + singular := cand[:len(cand)-1] + if desc, ok := roomNouns[singular]; ok { + key := singular + for strings.HasPrefix(desc, `:`) { + key = desc[1:] + desc = roomNouns[key] } - return testNoun, desc + return key, desc } - - } else { - - testNoun := newNoun + `es` - if desc, ok := roomNouns[testNoun]; ok { // `es` at end - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] - } - return testNoun, desc - } - } - - if len(newNoun) < 4 { - continue - } - - // Strip 'es' such as 'torches' - if noun[len(newNoun)-3:] == `ies` { - - testNoun := newNoun[:len(newNoun)-3] + `y` - if desc, ok := roomNouns[testNoun]; ok { // `y` instead of `ies` at end - if desc[0:1] == `:` { - return desc[1:], roomNouns[desc[1:]] - } - return testNoun, desc - } - - } - + // Default: no match } return ``, `` diff --git a/internal/rooms/rooms_test.go b/internal/rooms/rooms_test.go index 76b22f85..4ab9f93f 100644 --- a/internal/rooms/rooms_test.go +++ b/internal/rooms/rooms_test.go @@ -108,6 +108,9 @@ func TestFindNoun(t *testing.T) { "mystery": "just a riddle", "secret": ":mystery", // chain alias -> :mystery "projector screen": "a wide, matte-white screen", // multi-word matching + "island": "a tropical paradise", + "rocks": ":island", + "rocky island": ":island", }, } @@ -209,6 +212,31 @@ func TestFindNoun(t *testing.T) { wantFoundNoun: "", wantDesc: "", }, + { + name: "Multi-word input, first word valid (island)", + inputNoun: "island", + wantFoundNoun: "island", + wantDesc: "a tropical paradise", + }, + // Issue #356: Fix Noun aliases + { + name: "Multi-word input, first word valid (rocks)", + inputNoun: "rocks", + wantFoundNoun: "island", + wantDesc: "a tropical paradise", + }, + { + name: "Multi-word input, first word valid (rocky island)", + inputNoun: "rocky island", + wantFoundNoun: "island", + wantDesc: "a tropical paradise", + }, + { + name: "Multi-word input, first word valid (rocky)", + inputNoun: "rocky", + wantFoundNoun: "island", + wantDesc: "a tropical paradise", + }, } // Run each sub-test. From cd9fedd3db7820cef822340ca4af8c05e60d7963 Mon Sep 17 00:00:00 2001 From: Jason Ragsdale Date: Sat, 3 May 2025 16:19:59 -0500 Subject: [PATCH 2/3] Fix exists check --- internal/rooms/rooms.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index 9fbef628..ff75f669 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -1588,8 +1588,10 @@ func (r *Room) FindNoun(noun string) (foundNoun string, nounDescription string) roomNouns[key] = desc if strings.Contains(key, ` `) { for word := range strings.SplitSeq(key, " ") { - if _, exists := r.Nouns[word]; !exists && roomNouns[word] == "" { - roomNouns[word] = `:` + key + if _, exists := roomNouns[word]; !exists { + if _, existsInNouns := r.Nouns[word]; !existsInNouns { + roomNouns[word] = `:` + key + } } } } From ca2055fa1b51b60deecdc53d2921da6848fe8dc4 Mon Sep 17 00:00:00 2001 From: Jason Ragsdale Date: Mon, 12 May 2025 19:17:57 -0500 Subject: [PATCH 3/3] Updated FindNoun to disallow circular references to alises --- internal/rooms/rooms.go | 166 ++++++++++++++++++++++------------- internal/rooms/rooms_test.go | 14 +++ 2 files changed, 121 insertions(+), 59 deletions(-) diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index ff75f669..0bb21633 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -1576,91 +1576,139 @@ func (r *Room) FindContainerByName(containerNameSearch string) string { return closeMatch } -// FindNoun resolves a noun or alias within the room's noun map. func (r *Room) FindNoun(noun string) (foundNoun string, nounDescription string) { if len(r.Nouns) == 0 { - return ``, `` - } - - // Build flat map of noun -> desc, and add single-word aliases for multi-word nouns - roomNouns := make(map[string]string, len(r.Nouns)) - for key, desc := range r.Nouns { - roomNouns[key] = desc - if strings.Contains(key, ` `) { - for word := range strings.SplitSeq(key, " ") { - if _, exists := roomNouns[word]; !exists { - if _, existsInNouns := r.Nouns[word]; !existsInNouns { - roomNouns[word] = `:` + key - } + return "", "" + } + + // Flatten the room nouns and create single-word aliases for multi-word nouns + roomNouns := map[string]string{} + for originalNoun, originalDesc := range r.Nouns { + roomNouns[originalNoun] = originalDesc + if strings.Contains(originalNoun, " ") { + for _, part := range strings.Split(originalNoun, " ") { + if _, exists := r.Nouns[part]; exists { + continue } + if _, exists := roomNouns[part]; exists { + continue + } + roomNouns[part] = ":" + originalNoun } } } - // Build candidate list: full noun first, then each word - candidates := []string{noun} - for _, part := range util.SplitButRespectQuotes(noun) { - if len(part) > 1 { - candidates = append(candidates, part) + // Build candidate noun list + testNouns := util.SplitButRespectQuotes(noun) + for i := 0; i < len(testNouns); i++ { + if strings.Contains(testNouns[i], " ") { + for _, part := range strings.Split(testNouns[i], " ") { + testNouns = append(testNouns, strings.ToLower(strings.TrimSpace(part))) + } } } + if len(testNouns) > 1 { + testNouns = append(testNouns, strings.ToLower(strings.TrimSpace(noun))) + } - // Try direct matches and aliases - for _, cand := range candidates { - if desc, ok := roomNouns[cand]; ok { - key := cand - for strings.HasPrefix(desc, `:`) { - key = desc[1:] - desc = roomNouns[key] + // Try each candidate: exact, singular/plural, alias-aware + for _, cand := range testNouns { + newNoun := strings.ToLower(strings.TrimSpace(cand)) + + // Direct match or single-level alias + if desc, ok := roomNouns[newNoun]; ok { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if targetDesc, ok2 := roomNouns[target]; ok2 && !strings.HasPrefix(targetDesc, ":") { + return target, targetDesc + } + // alias->alias or missing target => ignore + } else { + return newNoun, desc } - return key, desc } - } - // Fallback pluralization/singularization - for _, cand := range candidates { - // Handle "ies" -> "y" - if strings.HasSuffix(cand, `ies`) { - singular := cand[:len(cand)-3] + `y` - if desc, ok := roomNouns[singular]; ok { - key := singular - for strings.HasPrefix(desc, `:`) { - key = desc[1:] - desc = roomNouns[key] + // Strip "es" + if strings.HasSuffix(newNoun, "es") { + tn := strings.TrimSuffix(newNoun, "es") + if desc, ok := roomNouns[tn]; ok { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if targetDesc, ok2 := roomNouns[target]; ok2 && !strings.HasPrefix(targetDesc, ":") { + return target, targetDesc + } + } else { + return tn, desc + } + } + } else { + // Add "es" + tn := newNoun + "es" + if desc, ok := roomNouns[tn]; ok { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if targetDesc, ok2 := roomNouns[target]; ok2 && !strings.HasPrefix(targetDesc, ":") { + return target, targetDesc + } + } else { + return tn, desc } - return key, desc } } - // Handle "es" -> remove - if strings.HasSuffix(cand, `es`) { - singular := cand[:len(cand)-2] - if desc, ok := roomNouns[singular]; ok { - key := singular - for strings.HasPrefix(desc, `:`) { - key = desc[1:] - desc = roomNouns[key] + // "ies" -> "y" + if strings.HasSuffix(newNoun, "ies") { + tn := strings.TrimSuffix(newNoun, "ies") + "y" + if desc, ok := roomNouns[tn]; ok { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if targetDesc, ok2 := roomNouns[target]; ok2 && !strings.HasPrefix(targetDesc, ":") { + return target, targetDesc + } + } else { + return tn, desc } - return key, desc } } + } - // Handle trailing 's' - if strings.HasSuffix(cand, `s`) { - singular := cand[:len(cand)-1] - if desc, ok := roomNouns[singular]; ok { - key := singular - for strings.HasPrefix(desc, `:`) { - key = desc[1:] - desc = roomNouns[key] + // Multi-word noun match + for full, desc := range roomNouns { + if strings.Contains(full, " ") { + for _, part := range testNouns { + if strings.Contains(full, part) { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if td, ok := roomNouns[target]; ok && !strings.HasPrefix(td, ":") { + return target, td + } + } else { + return full, desc + } + } + } + } + } + + // Single-word match for multi-word nouns + for full, desc := range roomNouns { + if !strings.Contains(full, " ") { + for _, part := range testNouns { + if part == full { + if strings.HasPrefix(desc, ":") { + target := desc[1:] + if td, ok := roomNouns[target]; ok && !strings.HasPrefix(td, ":") { + return target, td + } + } else { + return full, desc + } } - return key, desc } } - // Default: no match } - return ``, `` + return "", "" } func (r *Room) FindExitByName(exitNameSearch string) (exitName string, exitRoomId int) { diff --git a/internal/rooms/rooms_test.go b/internal/rooms/rooms_test.go index 4ab9f93f..ed7142a1 100644 --- a/internal/rooms/rooms_test.go +++ b/internal/rooms/rooms_test.go @@ -111,6 +111,8 @@ func TestFindNoun(t *testing.T) { "island": "a tropical paradise", "rocks": ":island", "rocky island": ":island", + "feet": ":hands", + "hands": ":feet", }, } @@ -237,6 +239,18 @@ func TestFindNoun(t *testing.T) { wantFoundNoun: "island", wantDesc: "a tropical paradise", }, + { + name: "circular references (hands)", + inputNoun: "hands", + wantFoundNoun: "", + wantDesc: "", + }, + { + name: "circular references (feet)", + inputNoun: "feet", + wantFoundNoun: "", + wantDesc: "", + }, } // Run each sub-test.