ソースを参照

feat: simulator improvements with tests

- consistent sorting for scores

- change format of logged games

- env var for debug
Alex White 7 ヶ月 前
コミット
a6d0dacdd4
7 ファイル変更296 行追加93 行削除
  1. 14 14
      fallout.go
  2. 9 5
      game.go
  3. 1 1
      game_test.go
  4. 17 7
      games.txt
  5. 101 55
      simulator.go
  6. 153 10
      simulator_test.go
  7. 1 1
      word_test.go

+ 14 - 14
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()
@@ -17,24 +23,12 @@ func main() {
 	}
 
 	simulator := NewSimulator(*game)
-	err = simulator.SimulateAllPossibleGames()
+	_, err = simulator.SimulateAllPossibleGames()
 	if err != nil {
 		fmt.Printf("Error simulating all possible games: %s\n", err)
 		os.Exit(1)
 	}
 
-	//err = game.scoreWordsByCommonLetterLocations()
-	//if err != nil {
-	//	fmt.Printf("Error scoring Words: %s\n", err)
-	//	os.Exit(1)
-	//}
-	//
-	//_, err = game.printSortedScores()
-	//if err != nil {
-	//	fmt.Printf("Error printing sorted scores: %s\n", err)
-	//	os.Exit(1)
-	//}
-
 	for i := 0; i < 3; i++ {
 		guess, score := getGuessAndScoreFromStdin()
 		fmt.Printf("Guess: %s, Score: %d\n", guess, score)
@@ -58,7 +52,7 @@ func main() {
 			fmt.Printf("Error getting best guess: %s\n", err)
 			os.Exit(1)
 		}
-		fmt.Sprintf("Best Guess: %s", bestGuess)
+		fmt.Printf("Best Guess: %s", bestGuess)
 
 		if len(game.Words) == 1 {
 			for word := range game.Words {
@@ -85,3 +79,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)
 			}
 		})
 	}

+ 17 - 7
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,6 +53,8 @@ gates
 favor
 hands
 range
+.
+mazes
 
 
 lending
@@ -59,7 +64,7 @@ enclave
 sparing
 decline
 recruit
-dealing *
+dealing
 feeling
 require
 ceiling
@@ -71,6 +76,9 @@ servant
 defeats
 leading
 decrees
+.
+dealing
+
 
 guides
 posted
@@ -78,7 +86,7 @@ former
 become
 armory
 border
-member *
+member
 wishes
 priced
 fended
@@ -89,4 +97,6 @@ cellar
 murder
 nomads
 series
-proper
+proper
+.
+member

+ 101 - 55
simulator.go

@@ -20,114 +20,160 @@ func NewSimulator(g Game) *Simulation {
 	}
 }
 
-func (s Simulation) SimulateAllPossibleGames() error {
+func (s Simulation) SimulateAllPossibleGames() (string, error) {
 
 	bestGuessLossCount := 99
 	bestGuessTotalRounds := 99
 
 	wordLossCounts := make(map[string]int)
-	totalRounds := make(map[string]int)
+	maxRoundCounts := make(map[string]int)
 
 	for initialWord := range s.Game.Words {
 		fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
-		lossCount, totalRoundCount := s.SimulateOneGame(initialWord)
+
+		simulatedGame := NewGame()
+		simulatedGame.Words = make(map[string]*Word)
+		for word := range s.Game.Words {
+			simulatedGame.Words[word] = &Word{Word: word}
+		}
+
+		lossCount, maxRounds, _ := s.SimulateOneInitialWord(simulatedGame, initialWord)
 		wordLossCounts[initialWord] = lossCount
-		totalRounds[initialWord] = totalRoundCount
+		maxRoundCounts[initialWord] = maxRounds
 		if lossCount < bestGuessLossCount {
 			bestGuessLossCount = lossCount
-			bestGuessTotalRounds = totalRoundCount
-		} else if lossCount == bestGuessLossCount && totalRoundCount < bestGuessTotalRounds {
-			bestGuessTotalRounds = totalRoundCount
+			bestGuessTotalRounds = maxRounds
+		} else if lossCount == bestGuessLossCount && maxRounds < bestGuessTotalRounds {
+			bestGuessTotalRounds = maxRounds
 		}
-	}
 
+	}
 	fmt.Println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
 
 	err := s.Game.scoreWordsByCommonLetterLocations()
 	if err != nil {
-		return fmt.Errorf("Error scoring Words: %s", err)
+		return "", fmt.Errorf("Error scoring Words: %s", err)
 	}
+
+	var bestGuess string
 	for _, word := range s.Game.getSortedScores() {
-		if wordLossCounts[word] == bestGuessLossCount && totalRounds[word] == bestGuessTotalRounds {
-			fmt.Printf("\nBest Guess: %s  Failed Branches: %d  Total Rounds: %d\n\n", word, bestGuessLossCount, totalRounds[word])
+		if wordLossCounts[word] == bestGuessLossCount && maxRoundCounts[word] == bestGuessTotalRounds {
+			fmt.Printf("\nBest Guess: %s  Failed Branches: %d  Max Rounds: %d\n\n", word, bestGuessLossCount, maxRoundCounts[word])
+			bestGuess = word
 			break
 		}
 	}
 
-	return nil
+	return bestGuess, nil
 }
 
-func (s Simulation) SimulateOneGame(initialWord string) (int, int) {
+func (s Simulation) SimulateOneInitialWord(game *Game, initialWord string) (lossCount, maxRounds, totalRounds int) {
 
-	lossCount := 0
-	totalRounds := 0
-	maxRounds := 4
+	debugPrint("Initial Word: %s\n", initialWord)
 
-ANSWER_LOOP:
 	for answer := range s.Game.Words {
 
+		debugPrint(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+
+		debugPrint("Target Word: %s\n", answer)
+
 		simulatedGame := NewGame()
 		simulatedGame.Words = make(map[string]*Word)
-		for word := range s.Game.Words {
+		for word := range game.Words {
 			simulatedGame.Words[word] = &Word{Word: word}
 		}
 
-		guess := initialWord
+		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)
 
-		for guessRounds := 1; guessRounds <= maxRounds; guessRounds++ {
+	return lossCount, maxRounds, totalRounds
+}
 
-			totalRounds++
+func (s Simulation) SimulateOneGame(simulatedGame *Game, initialWord, answer string) (bool, int) {
 
-			if guess == answer {
-				fmt.Printf("Rounds: %d  Initial Word: %s  Target Word: %s\n", guessRounds, initialWord, answer)
-				if guessRounds > maxRounds {
-					lossCount++
-				}
-				continue ANSWER_LOOP
-			}
+	guess := initialWord
 
-			score := s.getScore(guess, answer)
-			//fmt.Printf("Score: %d\n", score)
+	// 10 rounds max just to prevent infinite loops
+	var round int
+	for round = 1; round <= 10; round++ {
 
-			simulatedGame.FilterWords(guess, score)
-			//fmt.Printf("Words remaining: %+v\n", simulatedGame.Words)
+		if round == 1 {
+			debugPrint("First guess, initial Word: %s\n", initialWord)
 
-			err := simulatedGame.scoreWordsByCommonLetterLocations()
+		} else {
+
+			//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)
+			//}
+
+			var err error
+			guess, err = simulatedGame.getBestGuess()
 			if err != nil {
-				fmt.Printf("Error scoring Words: %s\n", err)
+				fmt.Printf("Error calculating best guess: %s\n", err)
 				os.Exit(1)
 			}
+			debugPrint("Round %d, guess: %s\n", round, guess)
+
+		}
 
-			if len(simulatedGame.Words) == 1 {
-				//fmt.Printf("The word is: %s\n", guess)
-				for word := range simulatedGame.Words {
+		score := s.getScore(guess, answer)
+		debugPrint("guess '%s' matches %d locations in answer '%s'\n", guess, score, answer)
 
-					if word != answer {
-						fmt.Printf("Incorrectly guessed word: %s => This should never happen!\n", word)
-						os.Exit(1)
-					}
+		simulatedGame.FilterWords(guess, score)
+		debugPrint("Words remaining after filtering: %+v\n", simulatedGame.Words)
 
-					if guessRounds > maxRounds {
-						lossCount++
-					}
+		if guess == answer {
+			break
+		}
 
-					fmt.Printf("Rounds: %d  Initial Word: %s  Target Word: %s\n", guessRounds, initialWord, answer)
-					continue ANSWER_LOOP
+		if len(simulatedGame.Words) == 1 {
+			debugPrint("The word is: %s\n", guess)
+			for word := range simulatedGame.Words {
+
+				if word != answer {
+					fmt.Printf("Incorrectly guessed word: %s => This should never happen!\n", word)
+					os.Exit(1)
 				}
-			} else {
-				//fmt.Printf("Number of words remaining: %d\n", len(simulatedGame.Words))
-			}
 
-			guess, err = simulatedGame.getBestGuess()
-			if err != nil {
-				fmt.Printf("Error calculating best guess: %s\n", err)
-				os.Exit(1)
+				break
 			}
 		}
+
+		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: Matched in %d rounds =>  Initial Word: %s  Target Word: %s\n", round, initialWord, answer)
+		return false, round
 	}
-	fmt.Printf("\nInitial Word: %s  Loss Count: %d  Total Rounds: %d\n", initialWord, lossCount, totalRounds)
 
-	return lossCount, totalRounds
+	fmt.Printf("Win:  Matched in %d rounds =>  Initial Word: %s  Target Word: %s\n", round, initialWord, answer)
+	return true, round
 }
 
 func (s Simulation) getScore(guess string, answer string) int {

+ 153 - 10
simulator_test.go

@@ -1,22 +1,33 @@
 package main
 
 import (
-	"fmt"
 	"github.com/stretchr/testify/assert"
 	"testing"
 )
 
-func TestSimulation_Run(t *testing.T) {
+func TestSimulation_SimulateOneInitialWord(t *testing.T) {
 	type fields struct {
-		Game *Game
+		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
-		wantErr assert.ErrorAssertionFunc
+		name              string
+		fields            fields
+		args              args
+		wantedLossCount   int
+		wantedMaxRounds   int
+		wantedTotalRounds int
 	}{
 		{
-			name: "simple words",
+			name: "simple game with no losses",
 			fields: fields{
 				Game: &Game{
 					Words: map[string]*Word{
@@ -26,15 +37,147 @@ func TestSimulation_Run(t *testing.T) {
 					},
 				},
 			},
-			wantErr: assert.NoError,
+			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,
+				Game:            tt.fields.Game,
+				BestGuess:       tt.fields.BestGuess,
+				BestGuessRounds: tt.fields.BestGuessRounds,
+				SuccessCount:    tt.fields.SuccessCount,
+				FailCount:       tt.fields.FailCount,
+				TotalRounds:     tt.fields.TotalRounds,
 			}
-			tt.wantErr(t, s.SimulateAllPossibleGames(), fmt.Sprintf("SimulateAllPossibleGames()"))
+			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)
 			}
 		})
 	}