diff --git a/wordle/terminal_manager.go b/wordle/terminal_manager.go index c719c39..dc32c07 100644 --- a/wordle/terminal_manager.go +++ b/wordle/terminal_manager.go @@ -36,7 +36,7 @@ func NewTerminalManager(ctx context.Context, game *WordGame) *TerminalManager { stateBox := tview.NewTextView() stateBox.SetDynamicColors(true) stateBox.SetBorder(true) - stateBox.SetTitle(fmt.Sprintf("P2P Wordle")) + stateBox.SetTitle("P2P Wordle") // text views are io.Writers, but they don't automatically refresh. // this sets a change handler to force the app to redraw when we get @@ -107,17 +107,21 @@ func NewTerminalManager(ctx context.Context, game *WordGame) *TerminalManager { } } -func (ui *TerminalManager) Run() error { +func (ui *TerminalManager) Run(initialized *bool) error { go ui.handleEvents() defer ui.end() + go func() { + // mark as initialized after ~200 milliseconds + time.Sleep(200 * time.Millisecond) + *initialized = true + }() err := ui.app.Run() if err != nil { return err } - ui.displayStateString(ui.Game.ComposeStateUI()) - return nil + return nil } // end signals the event loop to exit gracefully @@ -125,59 +129,44 @@ func (ui *TerminalManager) end() { ui.doneCh <- struct{}{} } -func (ui *TerminalManager) displayStateString(s string) { - fmt.Fprintf(ui.stateBox, "%s\n", s) -} - -func (ui *TerminalManager) displayStateStatus() { - s := ui.Game.ComposeStateUI() - ui.displayStateString(s) -} - -func (ui *TerminalManager) displayDebugString(s string) { +func (ui *TerminalManager) RefreshWordleState() { stateB := ui.stateBox.(*tview.TextView) stateB.Clear() - fmt.Fprintf(ui.debugBox, "%s\n", s) + s := ui.Game.ComposeStateUI() + fmt.Fprintf(ui.stateBox, "%s\n", s) } -func (ui *TerminalManager) AddDebugItem(s string) { - ui.displayDebugString(s) +func (ui *TerminalManager) AddDebugMsg(s string) { + fmt.Fprintf(ui.debugBox, "%s\n", s) } func (ui *TerminalManager) handleEvents() { - peerRefreshTicker := time.NewTicker(time.Second) - defer peerRefreshTicker.Stop() - + ui.RefreshWordleState() for { - ui.displayStateStatus() select { case input := <-ui.inputCh: switch ui.Game.StateIdx { case 0: - ui.AddDebugItem(fmt.Sprintf("Your next proposed word: %s", input)) + ui.AddDebugMsg(fmt.Sprintf("Your next proposed word: %s", input)) case 1: - ui.AddDebugItem(fmt.Sprintf("Last guess: %s (freezes are expected if no peers connected)", input)) + ui.AddDebugMsg(fmt.Sprintf("Last guess: %s (freezes are expected if no peers connected)", input)) default: continue } // when the user types in a line, publish it to the chat room and print to the message window err := ui.Game.NewStdinInput(input) if err != nil { - ui.AddDebugItem(fmt.Sprintf("publish error: %s", err)) + ui.AddDebugMsg(fmt.Sprintf("publish error: %s", err)) } - case others := <-ui.OthersGuessC: - ui.AddDebugItem(fmt.Sprintln("new gueess from someone", others)) - // when we receive a message from the chat room, print it to the message window - case <-ui.ctx.Done(): fmt.Println("context done") return case <-ui.doneCh: fmt.Println("channel done") - return } + ui.RefreshWordleState() } } diff --git a/wordle/user_interface.go b/wordle/user_interface.go index a7ae2b9..4ce2633 100644 --- a/wordle/user_interface.go +++ b/wordle/user_interface.go @@ -2,7 +2,7 @@ package wordle import ( "context" - "fmt" + "time" "github.com/p2p-games/wordle/model" ) @@ -21,7 +21,8 @@ type WordleUI struct { CannonicalHeader *model.Header - tm *TerminalManager + uiInitialized bool + tm *TerminalManager } func NewWordleUI(ctx context.Context, wordleServ *Service, peerId string) *WordleUI { @@ -33,6 +34,10 @@ func NewWordleUI(ctx context.Context, wordleServ *Service, peerId string) *Wordl } wordleServ.SetLog(func(s string) { + // wait untill UI is initialilzed + for !ui.uiInitialized { + time.Sleep(200 * time.Millisecond) + } ui.AddDebugItem(s) }) @@ -48,43 +53,69 @@ func (w *WordleUI) Run() { panic("non able to load any header from the datastore, not even genesis??!") } + // generate a guess channel + userGuessC := make(chan guess) + // generate a new game - w.CurrentGame = NewWordGame(w.ctx, w.PeerId, w.CannonicalHeader.PeerID, w.CannonicalHeader.Proposal, w.WordleServ) + w.CurrentGame = NewWordGame(w.PeerId, w.CannonicalHeader.PeerID, w.CannonicalHeader.Proposal, userGuessC) - // generate a terminal manager - w.tm = NewTerminalManager(w.ctx, w.CurrentGame) - err = w.tm.Run() - if err != nil { - panic(err) - } // get the channel for incoming headers incomingHeaders, err := w.WordleServ.Guesses(w.ctx) if err != nil { panic("unable to retrieve the channel of headers from the user interface") } - - for { - select { - case recHeader := <-incomingHeaders: // incoming New Message from surrounding peers - w.AddDebugItem(fmt.Sprintf("guess received from %s", recHeader.PeerID)) - // verify weather the header is correct or not - if model.Verify(recHeader.Guess, w.CannonicalHeader.Proposal) { - w.CannonicalHeader = recHeader - // generate a new one game - w.CurrentGame = NewWordGame(w.ctx, w.PeerId, w.CannonicalHeader.PeerID, recHeader.Proposal, w.WordleServ) - - // refresh the terminal manager - w.tm.Game = w.CurrentGame - } else { - // Actually, there isn't anything else to do - continue + go func() { + for { + select { + case userGuess := <-userGuessC: + w.tm.AddDebugMsg("about to send new guess") + // notify the service of a new User Guess + err = w.WordleServ.Guess(w.ctx, userGuess.Guess, userGuess.Proposal) + if err != nil { + w.tm.AddDebugMsg("error sending guess" + userGuess.Guess + " - " + err.Error()) + } + + // check if the guess if the guess was right for debug-msg purpose + bools, err := model.VerifyString(userGuess.Guess, w.CannonicalHeader.Proposal) + if err != nil { + w.tm.AddDebugMsg("error verifying the guess" + err.Error()) + } + + if IsGuessSuccess(bools) { + w.tm.AddDebugMsg("succ guess sent") + } else { + w.tm.AddDebugMsg("wrong guess sent") + } + + case recHeader := <-incomingHeaders: // incoming New Message from surrounding peers + //w.AddDebugItem(fmt.Sprintf("guess received from %s", recHeader.PeerID)) + // verify weather the header is correct or not + if model.Verify(recHeader.Guess, w.CannonicalHeader.Proposal) { + w.CannonicalHeader = recHeader + // generate a new one game + + w.CurrentGame = NewWordGame(w.PeerId, w.CannonicalHeader.PeerID, recHeader.Proposal, userGuessC) + // refresh the terminal manager + w.tm.Game = w.CurrentGame + } + case <-w.ctx.Done(): // context shutdow + return } - case <-w.ctx.Done(): // context shutdow - return + + // render the new state of the UI + w.tm.RefreshWordleState() } + }() + + // generate a terminal manager + w.tm = NewTerminalManager(w.ctx, w.CurrentGame) + err = w.tm.Run(&w.uiInitialized) + if err != nil { + panic(err) } + } func (w *WordleUI) AddDebugItem(s string) { - w.tm.AddDebugItem(s) + w.tm.AddDebugMsg(s) } diff --git a/wordle/utils.go b/wordle/utils.go index 9e0a265..0a0d44d 100644 --- a/wordle/utils.go +++ b/wordle/utils.go @@ -85,7 +85,7 @@ func ComposeWordleVisualWord(word string, target *model.Word) string { switch charStatus { case 0: // not in the word - compWord += composeCharWithColor(c[i], "") + compWord += composeCharWithColor(c[i], "white") case 1: // in the word but on wrong possition compWord += composeCharWithColor(c[i], Yellow) case 2: // bingo @@ -93,7 +93,7 @@ func ComposeWordleVisualWord(word string, target *model.Word) string { } } - return compWord + return compWord + "[white]" } // compose the character over the color and reset the terminal color diff --git a/wordle/word_game.go b/wordle/word_game.go index 2a899b6..78d0f61 100644 --- a/wordle/word_game.go +++ b/wordle/word_game.go @@ -1,10 +1,8 @@ package wordle import ( - "context" "fmt" "strings" - "sync/atomic" "github.com/p2p-games/wordle/model" "github.com/pkg/errors" @@ -13,7 +11,6 @@ import ( const maxAttempts int = 5 type WordGame struct { - ctx context.Context PeerId string // to verify if the guess is correct @@ -26,7 +23,7 @@ type WordGame struct { AttemptedWords []string isCorrect map[string][]bool - serv *Service + userGuessC chan guess } type guess struct { @@ -35,28 +32,27 @@ type guess struct { } // generate new game session -func NewWordGame(ctx context.Context, peerId string, proposerId string, target *model.Word, serv *Service) *WordGame { +func NewWordGame(peerId string, proposerId string, target *model.Word, guessC chan guess) *WordGame { salts := GetSaltsFromWord(target) wg := &WordGame{ - ctx: ctx, PeerId: peerId, Target: target, Salts: salts, StateIdx: int32(0), // start requesting the word AttemptedWords: make([]string, 0), isCorrect: make(map[string][]bool), - serv: serv, + userGuessC: guessC, } if proposerId == peerId { // go straight to the 2 state (I already won) - atomic.StoreInt32(&wg.StateIdx, int32(2)) + wg.StateIdx = 2 } return wg } func (w *WordGame) ComposeStateUI() string { var s string - switch atomic.LoadInt32(&w.StateIdx) { + switch w.StateIdx { case int32(0): s = "Introduce your word proposal as next word to guess:\n" case int32(1): @@ -64,13 +60,13 @@ func (w *WordGame) ComposeStateUI() string { for _, guessedWord := range w.AttemptedWords { if guessedWord != "" { // check wheather the word was correct or not - correct := "x" + correct := "[red]x[white]" comp, err := model.VerifyString(guessedWord, w.Target) if err != nil { continue } if IsGuessSuccess(comp) { - correct = "v" + correct = "[green]v[white]" } // compose the color strings with color chars @@ -79,13 +75,30 @@ func (w *WordGame) ComposeStateUI() string { } s += fmt.Sprintf("\nAttempts left %d\n", maxAttempts-len(w.AttemptedWords)) case int32(2): - s = "\n\tCongrats, you guessed the word!\nWait untill someone guesses your word to play again\n" + s = "\n\nCongrats, you guessed the word!\nWait untill someone guesses your word to play again\n" + s += "list of guessed words:\n" + for _, guessedWord := range w.AttemptedWords { + if guessedWord != "" { + // check wheather the word was correct or not + correct := "[red]x[white]" + comp, err := model.VerifyString(guessedWord, w.Target) + if err != nil { + continue + } + if IsGuessSuccess(comp) { + correct = "[green]v[white]" + } + // compose the color strings with color chars + + s += fmt.Sprintf("\t[%s] %s\n", correct, ComposeWordleVisualWord(guessedWord, w.Target)) + } + } case int32(3): s = "\n\tNo more attempts left for this word!\nWait untill someone guesses it to play again\n" default: s = "unrecognized state to generate the UI\n" } - return s + return s + "\ntype '/quit to exit game' \n" } func (w *WordGame) NewStdinInput(input string) error { @@ -97,7 +110,7 @@ func (w *WordGame) NewStdinInput(input string) error { } */ // check in which state do we are - switch atomic.LoadInt32(&w.StateIdx) { + switch w.StateIdx { case int32(0): err := w.addNextTarget(input, w.Salts) if err != nil { @@ -118,13 +131,13 @@ func (w *WordGame) NewStdinInput(input string) error { func (w *WordGame) addNextTarget(nextWord string, salts []string) error { // check if we are in state 0 - if atomic.LoadInt32(&w.StateIdx) != int32(0) { + if w.StateIdx != int32(0) { return errors.New("unable to add next target, not in state 0") } w.NextWord = nextWord // go to state 1 - atomic.StoreInt32(&w.StateIdx, int32(1)) + w.StateIdx = 1 return nil } @@ -144,7 +157,7 @@ func (w *WordGame) WasGuessed() bool { func (w *WordGame) addNewGuess(guessedWord string) error { // check if we are in state 1 - if atomic.LoadInt32(&w.StateIdx) != int32(1) { + if w.StateIdx != int32(1) { return errors.New("unable to add next target, not in state 1") } @@ -164,24 +177,19 @@ func (w *WordGame) addNewGuess(guessedWord string) error { } if IsGuessSuccess(comp) { - atomic.StoreInt32(&w.StateIdx, int32(2)) // Congrats, wait untill someone guesses your word + w.StateIdx = 2 // Congrats, wait untill someone guesses your word } // check if we did all the attempts if len(w.AttemptedWords) == maxAttempts && !IsGuessSuccess(comp) { - atomic.StoreInt32(&w.StateIdx, int32(3)) // Wait untill you can play again + w.StateIdx = 3 // Wait untill you can play again } - // send the msg over the channel to notify the service - currentGuess := guess{ - guessedWord, - w.NextWord, + g := guess{ + Guess: guessedWord, + Proposal: w.NextWord, } - fmt.Println("sending guess") - err = w.serv.Guess(w.ctx, currentGuess.Guess, currentGuess.Proposal) - if err != nil { - return err - } - fmt.Println("after guessing") + + w.userGuessC <- g return nil } diff --git a/wordle/word_game_test.go b/wordle/word_game_test.go index 28b1818..51428e0 100644 --- a/wordle/word_game_test.go +++ b/wordle/word_game_test.go @@ -11,7 +11,7 @@ import ( func TestStateTransition(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - + defer cancel() require := require.New(t) salts := []string{"a", "b", "c", "d", "e"} target := &model.Word{ @@ -43,7 +43,8 @@ func TestStateTransition(t *testing.T) { go func() { for { select { - case _ = <-guessMsgC: + case guess := <-guessMsgC: + t.Log("guess msg received, guess:" + guess.Guess + " | proposal:" + guess.Proposal) case <-ctx.Done(): return }