4 Komitmen 32d7c673de ... f678f19173

Pembuat SHA1 Pesan Tanggal
  Alex White f678f19173 fix: consider total rounds, improve answer detection 5 bulan lalu
  Alex White 5f352bbd1d fix: better detection of game end 5 bulan lalu
  Alex White a6d0dacdd4 feat: simulator improvements with tests 5 bulan lalu
  Alex White a700eb9ba1 feat: simulator prototype 5 bulan lalu
7 mengubah file dengan 471 tambahan dan 24 penghapusan
  1. 33 13
      fallout.go
  2. 9 5
      game.go
  3. 1 1
      game_test.go
  4. 52 4
      games.txt
  5. 192 0
      simulator.go
  6. 183 0
      simulator_test.go
  7. 1 1
      word_test.go

+ 33 - 13
fallout.go

@@ -6,8 +6,14 @@ import (
 	"os"
 )
 
+var debug = 0
+
 func main() {
 
+	if os.Getenv("DEBUG") == "true" {
+		debug = 1
+	}
+
 	game := NewGame()
 
 	err := game.getWordsFromStdin()
@@ -16,15 +22,10 @@ func main() {
 		os.Exit(1)
 	}
 
-	err = game.scoreWordsByCommonLetterLocations()
-	if err != nil {
-		fmt.Printf("Error scoring Words: %s\n", err)
-		os.Exit(1)
-	}
-
-	_, err = game.printSortedScores()
+	simulator := NewSimulator(*game)
+	_, err = simulator.SimulateAllPossibleGames()
 	if err != nil {
-		fmt.Printf("Error printing sorted scores: %s\n", err)
+		fmt.Printf("Error simulating all possible games: %s\n", err)
 		os.Exit(1)
 	}
 
@@ -34,6 +35,18 @@ func main() {
 
 		game.FilterWords(guess, score)
 
+		if len(game.Words) == 0 {
+			fmt.Printf("\n\nFATAL ERROR: something went wrong!  No words are left, this shouldn't happen!!!!!!!\n\n")
+			os.Exit(1)
+
+		} else if len(game.Words) == 1 {
+			fmt.Printf("Game is down to one word: %+v\n", game.Words)
+			for word := range game.Words {
+				fmt.Printf("\n\nThe answer is: %s\n\n", word)
+				os.Exit(0)
+			}
+		}
+
 		err = game.scoreWordsByCommonLetterLocations()
 		if err != nil {
 			fmt.Printf("Error scoring Words: %s\n", err)
@@ -46,12 +59,13 @@ func main() {
 			os.Exit(1)
 		}
 
-		if len(game.Words) == 1 {
-			for word := range game.Words {
-				fmt.Printf("The word is: %s\n", word)
-				os.Exit(0)
-			}
+		bestGuess, err := game.getBestGuess()
+		if err != nil {
+			fmt.Printf("Error getting best guess: %s\n", err)
+			os.Exit(1)
 		}
+		fmt.Printf("Best Guess: %s", bestGuess)
+
 	}
 }
 
@@ -71,3 +85,9 @@ func getGuessAndScoreFromStdin() (string, int) {
 
 	return guess, score
 }
+
+func debugPrint(format string, args ...interface{}) {
+	if debug == 1 {
+		fmt.Printf(format, args...)
+	}
+}

+ 9 - 5
game.go

@@ -62,9 +62,15 @@ func (g Game) getSortedScores() []string {
 		sortedWordScores = append(sortedWordScores, word.Word)
 	}
 
+	debugPrint("Sorted Word Scores: %v\n", sortedWordScores)
+
 	// sort words by score
 	sort.Slice(sortedWordScores, func(i, j int) bool {
-		return g.Words[sortedWordScores[i]].Score > g.Words[sortedWordScores[j]].Score
+		if g.Words[sortedWordScores[i]].Score != g.Words[sortedWordScores[j]].Score {
+			return g.Words[sortedWordScores[i]].Score > g.Words[sortedWordScores[j]].Score
+		}
+		// sort by word if scores are equal - this is for consistent answers for unit testing
+		return g.Words[sortedWordScores[i]].Word < g.Words[sortedWordScores[j]].Word
 	})
 
 	return sortedWordScores
@@ -97,8 +103,8 @@ func (g Game) getWordsFromStdin() error {
 		word, _ := reader.ReadString('\n')
 		word = word[:len(word)-1]
 		if word == "." {
-			fmt.Println("got Words!")
-			break
+			fmt.Print("\nOK!\n\n")
+			return nil
 		}
 
 		if g.Length == 0 {
@@ -111,6 +117,4 @@ func (g Game) getWordsFromStdin() error {
 		}
 		g.Words[word] = &Word{Word: word}
 	}
-
-	return nil
 }

+ 1 - 1
game_test.go

@@ -216,7 +216,7 @@ func TestGame_getBestGuess(t *testing.T) {
 				return
 			}
 			if got != tt.want {
-				t.Errorf("getBestGuess() got = %v, want %v", got, tt.want)
+				t.Errorf("getBestGuess() got = %v, wantedLossCount %v", got, tt.want)
 			}
 		})
 	}

+ 52 - 4
games.txt

@@ -10,10 +10,12 @@ pulls
 brass
 howls
 lined
-lines *
+lines
 likes
 caves
 lives
+.
+lines
 
 hazards
 hurting
@@ -26,16 +28,17 @@ cantina
 caravan
 gangers
 falling
-waiting *
+waiting
 screens
 happens
 largest
 lawless
 dangers
 variety
+.
+waiting
 
-
-mazes *
+mazes
 cares
 nails
 caves
@@ -50,5 +53,50 @@ gates
 favor
 hands
 range
+.
+mazes
+
+
+lending
+sealing
+element
+enclave
+sparing
+decline
+recruit
+dealing
+feeling
+require
+ceiling
+lecture
+sealant
+setting
+believe
+servant
+defeats
+leading
+decrees
+.
+dealing
 
 
+guides
+posted
+former
+become
+armory
+border
+member
+wishes
+priced
+fended
+depart
+mutter
+serves
+cellar
+murder
+nomads
+series
+proper
+.
+member

+ 192 - 0
simulator.go

@@ -0,0 +1,192 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+type Simulation struct {
+	Game            *Game
+	BestGuess       string
+	BestGuessRounds int
+	SuccessCount    int
+	FailCount       int
+	TotalRounds     int
+}
+
+func NewSimulator(g Game) *Simulation {
+	return &Simulation{
+		Game: &g,
+	}
+}
+
+func (s Simulation) SimulateAllPossibleGames() (string, error) {
+
+	bestGuessLossCount := 99
+	bestGuessMaxRounds := 99
+	bestGuessTotalRounds := 99
+
+	wordLossCounts := make(map[string]int)
+	totRoundCounts := make(map[string]int)
+	maxRoundCounts := make(map[string]int)
+
+	for initialWord := range s.Game.Words {
+		fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+
+		simulatedGame := NewGame()
+		simulatedGame.Words = make(map[string]*Word)
+		for word := range s.Game.Words {
+			simulatedGame.Words[word] = &Word{Word: word}
+		}
+
+		lossCount, maxRounds, totalRounds := s.SimulateOneInitialWord(simulatedGame, initialWord)
+		wordLossCounts[initialWord] = lossCount
+		totRoundCounts[initialWord] = totalRounds
+		maxRoundCounts[initialWord] = maxRounds
+		if lossCount < bestGuessLossCount {
+			bestGuessLossCount = lossCount
+			bestGuessTotalRounds = totalRounds
+			bestGuessMaxRounds = maxRounds
+		} else if lossCount == bestGuessLossCount && maxRounds < bestGuessMaxRounds {
+			bestGuessTotalRounds = totalRounds
+			bestGuessMaxRounds = maxRounds
+		} else if lossCount == bestGuessLossCount && maxRounds == bestGuessMaxRounds && totalRounds < bestGuessTotalRounds {
+			bestGuessTotalRounds = totalRounds
+		}
+
+	}
+	fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+	fmt.Print("Simulation completed\n\n")
+
+	err := s.Game.scoreWordsByCommonLetterLocations()
+	if err != nil {
+		return "", fmt.Errorf("Error scoring Words: %s", err)
+	}
+
+	var bestGuess string
+	for _, word := range s.Game.getSortedScores() {
+		if wordLossCounts[word] == bestGuessLossCount && totRoundCounts[word] == bestGuessTotalRounds && maxRoundCounts[word] == bestGuessMaxRounds {
+			fmt.Printf("Best Guess: %s  Failed Branches: %d  Total Rounds: %d  Max Rounds: %d\n\n", word, bestGuessLossCount, bestGuessTotalRounds, bestGuessMaxRounds)
+			bestGuess = word
+			break
+		}
+	}
+
+	return bestGuess, nil
+}
+
+func (s Simulation) SimulateOneInitialWord(game *Game, initialWord string) (lossCount, maxRounds, totalRounds int) {
+
+	debugPrint("Initial Word: %s\n", initialWord)
+
+	for answer := range s.Game.Words {
+
+		debugPrint(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n")
+
+		debugPrint("Target Word: %s\n", answer)
+
+		simulatedGame := NewGame()
+		simulatedGame.Words = make(map[string]*Word)
+		for word := range game.Words {
+			simulatedGame.Words[word] = &Word{Word: word}
+		}
+
+		won, rounds := s.SimulateOneGame(simulatedGame, initialWord, answer)
+		if !won {
+			lossCount++
+		}
+		totalRounds += rounds
+		if rounds > maxRounds {
+			maxRounds = rounds
+		}
+
+	}
+	fmt.Printf("Summary: Initial Word: %s  Loss Count: %d  Max Rounds: %d  Total Rounds: %d\n", initialWord, lossCount, maxRounds, totalRounds)
+
+	return lossCount, maxRounds, totalRounds
+}
+
+func (s Simulation) SimulateOneGame(simulatedGame *Game, initialWord, answer string) (bool, int) {
+
+	guess := initialWord
+
+	// 10 rounds max just to prevent infinite loops
+	var round int
+	for round = 1; round <= 10; round++ {
+
+		if len(simulatedGame.Words) == 1 {
+			debugPrint("Only one guess remaining after filtering: %s\n", guess)
+			break
+		}
+
+		if round == 1 {
+			debugPrint("First guess, initial Word: %s\n", initialWord)
+
+		} else {
+
+			//debugPrint(">>>>>>>>>>>>>>>>>>>>>>>>>>>>\n")
+			//fmt.Printf("Starting Sub Simulation: %+v\n", simulatedGame.Words)
+			//simulatedSubGame := NewGame()
+			//simulatedSubGame.Words = make(map[string]*Word)
+			//for word := range simulatedGame.Words {
+			//	simulatedSubGame.Words[word] = &Word{Word: word}
+			//}
+			//subSimulator := NewSimulator(*simulatedSubGame)
+			//
+			//var err error
+			//guess, err = subSimulator.SimulateAllPossibleGames()
+			//if err != nil {
+			//	fmt.Printf("Error simulating all possible games: %s\n", err)
+			//	os.Exit(1)
+			//}
+			//fmt.Printf("RESULT OF SUB Guess: %s\n", guess)
+
+			var err error
+			guess, err = simulatedGame.getBestGuess()
+			if err != nil {
+				fmt.Printf("Error calculating best guess: %s\n", err)
+				os.Exit(1)
+			}
+			debugPrint("Round %d, guess: %s\n", round, guess)
+
+		}
+
+		if guess == answer {
+			break
+		}
+
+		score := s.getScore(guess, answer)
+		debugPrint("guess '%s' matches %d locations in answer '%s'\n", guess, score, answer)
+
+		simulatedGame.FilterWords(guess, score)
+		debugPrint("Words remaining after filtering: %+v\n", simulatedGame.Words)
+
+		err := simulatedGame.scoreWordsByCommonLetterLocations()
+		if err != nil {
+			fmt.Printf("Error scoring Words: %s\n", err)
+			os.Exit(1)
+		}
+
+		debugPrint("End round %d, not solved, %d words remaining\n", round, len(simulatedGame.Words))
+	}
+
+	if round > 4 {
+		fmt.Printf(" Loss in %d rounds =>  %s .. %s\n", round, initialWord, answer)
+		return false, round
+	}
+
+	fmt.Printf("  Win in %d rounds =>  %s .. %s\n", round, initialWord, answer)
+	return true, round
+}
+
+func (s Simulation) getScore(guess string, answer string) int {
+	score := 0
+
+	for idx, letter := range answer {
+		if string(letter) == string(guess[idx]) {
+			score++
+		}
+	}
+
+	return score
+}

+ 183 - 0
simulator_test.go

@@ -0,0 +1,183 @@
+package main
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestSimulation_SimulateOneInitialWord(t *testing.T) {
+	type fields struct {
+		Game            *Game
+		BestGuess       string
+		BestGuessRounds int
+		SuccessCount    int
+		FailCount       int
+		TotalRounds     int
+	}
+	type args struct {
+		game        *Game
+		initialWord string
+	}
+	tests := []struct {
+		name              string
+		fields            fields
+		args              args
+		wantedLossCount   int
+		wantedMaxRounds   int
+		wantedTotalRounds int
+	}{
+		{
+			name: "simple game with no losses",
+			fields: fields{
+				Game: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"abb": {Word: "abb"},
+					},
+				},
+			},
+			args: args{
+				game: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"abb": {Word: "abb"},
+					},
+				},
+				initialWord: "aaa",
+			},
+			wantedLossCount:   0,
+			wantedMaxRounds:   2,
+			wantedTotalRounds: 5,
+		},
+		{
+			name: "simple failed game",
+			fields: fields{
+				Game: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"aac": {Word: "aac"},
+						"aad": {Word: "aad"},
+						"aae": {Word: "aae"},
+					},
+				},
+			},
+			args: args{
+				game: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"aac": {Word: "aac"},
+						"aad": {Word: "aad"},
+						"aae": {Word: "aae"},
+					},
+				},
+				initialWord: "aaa",
+			},
+			wantedLossCount:   1,
+			wantedMaxRounds:   5,
+			wantedTotalRounds: 15,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := Simulation{
+				Game:            tt.fields.Game,
+				BestGuess:       tt.fields.BestGuess,
+				BestGuessRounds: tt.fields.BestGuessRounds,
+				SuccessCount:    tt.fields.SuccessCount,
+				FailCount:       tt.fields.FailCount,
+				TotalRounds:     tt.fields.TotalRounds,
+			}
+			lossCount, maxRounds, totalRounds := s.SimulateOneInitialWord(tt.args.game, tt.args.initialWord)
+			assert.Equalf(t, tt.wantedLossCount, lossCount, "loss count for %v => %v", tt.args.game, tt.args.initialWord)
+			assert.Equalf(t, tt.wantedMaxRounds, maxRounds, "max rounds for %v => %v)", tt.args.game, tt.args.initialWord)
+			assert.Equalf(t, tt.wantedTotalRounds, totalRounds, "total rounds for %v => %v)", tt.args.game, tt.args.initialWord)
+		})
+	}
+}
+
+func TestSimulation_SimulateOneGame(t *testing.T) {
+	type fields struct {
+		Game            *Game
+		BestGuess       string
+		BestGuessRounds int
+		SuccessCount    int
+		FailCount       int
+		TotalRounds     int
+	}
+	type args struct {
+		simulatedGame *Game
+		initialWord   string
+		answer        string
+	}
+	tests := []struct {
+		name      string
+		fields    fields
+		args      args
+		won       bool
+		numRounds int
+	}{
+
+		{
+			name: "initial word is answer",
+			args: args{
+				simulatedGame: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"abb": {Word: "abb"},
+					},
+				},
+				initialWord: "aaa",
+				answer:      "aaa",
+			},
+			won:       true,
+			numRounds: 1,
+		},
+		{
+			name: "one guess to get answer",
+			args: args{
+				simulatedGame: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"abb": {Word: "abb"},
+					},
+				},
+				initialWord: "aaa",
+				answer:      "aab",
+			},
+			won:       true,
+			numRounds: 2,
+		},
+		{
+			name: "one guess to get answer",
+			args: args{
+				simulatedGame: &Game{
+					Words: map[string]*Word{
+						"aaa": {Word: "aaa"},
+						"aab": {Word: "aab"},
+						"aac": {Word: "aac"},
+						"aad": {Word: "aad"},
+						"aae": {Word: "aae"},
+					},
+				},
+				initialWord: "aaa",
+				answer:      "aae",
+			},
+			won:       false,
+			numRounds: 5,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := Simulation{}
+			got, got1 := s.SimulateOneGame(tt.args.simulatedGame, tt.args.initialWord, tt.args.answer)
+			assert.Equalf(t, tt.won, got, "SimulateOneGame(%v, %v, %v)", tt.args.simulatedGame, tt.args.initialWord, tt.args.answer)
+			assert.Equalf(t, tt.numRounds, got1, "SimulateOneGame(%v, %v, %v)", tt.args.simulatedGame, tt.args.initialWord, tt.args.answer)
+		})
+	}
+}

+ 1 - 1
word_test.go

@@ -43,7 +43,7 @@ func TestWord_MatchesGuess(t *testing.T) {
 				Score: tt.fields.Score,
 			}
 			if got := w.MatchesGuess(tt.args.guess, tt.args.numMatchingChars); got != tt.want {
-				t.Errorf("MatchesGuess() = %v, want %v", got, tt.want)
+				t.Errorf("MatchesGuess() = %v, wantedLossCount %v", got, tt.want)
 			}
 		})
 	}