Skip to content

Commit

Permalink
feat: hint description (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
igor-sirotin authored Jul 7, 2024
1 parent 3de3b3d commit 76e2658
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 69 deletions.
24 changes: 12 additions & 12 deletions internal/view/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ package view
Issue: https://github.com/golang/go/issues/19412
proposal: spec: add sum types / discriminated unions
[LanguageChange] [v2] [Proposal] [NeedsInvestigation]

╭───────┬──────────────────────┬─────────╮
│ Alice │ Bob │ Charlie │ DidukhSirotin │ Recommended: 3
├───────┼──────────────────────┼─────────┤ Acceptable: x - too big votes variety
13 │ 8 │ 13 3What to do: Listen to Alice and Didukh arguments
╰───────┴──────────────────────┴─────────
╭───╮
╭───╮ ╭───╮ │ 3 │ ╭───╮ ╭───╮ ╭────╮ ╭────╮ ╭────╮
│ 1 │ │ 2 │ ╰───╯ │ 5 │ │ 8 │ │ 13 │ │ 21 │ │ 34 │
╰───╯ ╰───╯ ╰───╯ ╰───╯ ╰────╯ ╰────╯ ╰────╯
^
╭───────┬────────────────────┬───────┬──────╮
│ Alice │ Bob (You) │ Charlie │ DavidErin │ Recommended: 8
├───────┼────────────────────┼─────────────┤ Acceptable:
8 8 5 5 │ 8 │ > Not bad.
╰───────┴────────────────────┴─────────────╯
╭───╮
╭───╮ ╭───╮ ╭───╮ ╭───╮ │ 8 │ ╭────╮ ╭────╮ ╭────╮
│ 1 │ │ 2 │ │ 3 │ │ 5 │ ╰───╯ │ 13 │ │ 21 │ │ 34 │
╰───╯ ╰───╯ ╰───╯ ╰───╯ ╰────╯ ╰────╯ ╰────╯
^

Use [←] and [→] arrows to select a card and press [Enter]
[Tab] To switch to issues list view
Expand Down Expand Up @@ -242,4 +242,4 @@ For example for a Fibbonacci deck
- https://github.com/six78/2-story-points-cli/issues/1
- https://github.com/six78/2-story-points-cli/issues/34
... 7 more issues
```
```
29 changes: 10 additions & 19 deletions internal/view/components/hintview/model.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package hintview

import (
"fmt"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

"github.com/six78/2-story-points-cli/internal/config"
"github.com/six78/2-story-points-cli/internal/view/components/voteview"
"github.com/six78/2-story-points-cli/internal/view/messages"
"github.com/six78/2-story-points-cli/pkg/protocol"
Expand All @@ -16,8 +13,7 @@ var (
headerStyle = lipgloss.NewStyle() // .Foreground(lipgloss.Color("#FAFAFA"))
acceptableStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
unacceptableStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
textStyle = lipgloss.NewStyle() // .Foreground(lipgloss.Color("#FAFAFA"))
MentionStyle = textStyle.Copy().Italic(true).Foreground(config.UserColor)
textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
)

type Model struct {
Expand Down Expand Up @@ -56,23 +52,18 @@ func (m Model) View() string {
return ""
}

verdictStyle := unacceptableStyle
verdictText := "x"
if m.hint.Acceptable {
verdictStyle = acceptableStyle
verdictText = "✓"
}

rejectionReason := ""
if !m.hint.Acceptable {
rejectionReason = fmt.Sprintf(" (%s)", textStyle.Render(m.hint.RejectReason))
}

return lipgloss.JoinVertical(lipgloss.Top,
"",
headerStyle.Render("Recommended:")+""+voteview.Render(m.hint.Value),
headerStyle.Render("Acceptable:")+" "+verdictStyle.Render(verdictText)+rejectionReason,
headerStyle.Render("What to do:")+" "+textStyle.Render(m.hint.Advice),
headerStyle.Render("Acceptable:")+" "+renderAcceptanceIcon(m.hint.Acceptable),
headerStyle.Render(">")+" "+textStyle.Render(m.hint.Description),
"",
)
}

func renderAcceptanceIcon(acceptable bool) string {
if acceptable {
return acceptableStyle.Render("✓")
}
return unacceptableStyle.Render("x")
}
15 changes: 7 additions & 8 deletions internal/view/components/hintview/model_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package hintview

import (
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -70,9 +69,9 @@ func TestUpdateAcceptableVote(t *testing.T) {
issue := protocol.Issue{
ID: protocol.IssueID(gofakeit.UUID()),
Hint: &protocol.Hint{
Acceptable: tc.acceptable,
Value: protocol.VoteValue(gofakeit.LetterN(5)),
RejectReason: gofakeit.LetterN(10),
Acceptable: tc.acceptable,
Value: protocol.VoteValue(gofakeit.LetterN(5)),
Description: gofakeit.LetterN(10),
},
}
model, cmd = model.Update(messages.GameStateMessage{
Expand All @@ -86,16 +85,16 @@ func TestUpdateAcceptableVote(t *testing.T) {
require.NotNil(t, model.hint)
require.Equal(t, *issue.Hint, *model.hint)

expectedVerdict := "✓"
expectedAcceptableIcon := "✓"
if !tc.acceptable {
expectedVerdict = "x" + fmt.Sprintf(" (%s)", issue.Hint.RejectReason)
expectedAcceptableIcon = "x"
}

expectedLines := []string{
"",
"Recommended: " + string(issue.Hint.Value),
"Acceptable: " + expectedVerdict,
"What to do:",
"Acceptable: " + expectedAcceptableIcon,
"> " + issue.Hint.Description,
"",
}

Expand Down
46 changes: 36 additions & 10 deletions pkg/game/hints.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ type hintMeasurements struct {
}

const (
// thresholds
maxAcceptableMaximumDeviation = 1
maxAcceptableMeanDeviation = 0.5
// Rejection reasons
varietyOfVotesIsTooHigh = "Variety of votes is too high"
maximumDeviationIsTooHigh = "Maximum deviation is too high"
varietyOfVotesIsTooHigh = "No strong consensus among the players"
maximumDeviationIsTooHigh = "Maximum deviation threshold exceeded"
// Advices
descriptionBingo = "BINGO! 🎉💃"
descriptionGoodJob = "Good job 😎"
descriptionNotBad = "Not bad 🤞"
descriptionYouCanDoBetter = "You can do better 💪"
// internal consts
float64Epsilon = 1e-9
)

var (
Expand All @@ -50,18 +58,30 @@ func GetResultHint(deck protocol.Deck, issueVotes protocol.IssueVotes) (*protoco
// Build the hint based on the measures
hint := &protocol.Hint{
Value: medianValue,
Advice: "",
Acceptable: true,
}

if resultMeasures.maxDeviation > maxAcceptableMaximumDeviation {
hint.Acceptable = false
hint.RejectReason = maximumDeviationIsTooHigh
hint.Description = maximumDeviationIsTooHigh
}

if resultMeasures.meanDeviation >= maxAcceptableMeanDeviation {
if resultMeasures.meanDeviation > maxAcceptableMeanDeviation {
hint.Acceptable = false
hint.RejectReason = varietyOfVotesIsTooHigh
hint.Description = varietyOfVotesIsTooHigh
}

if hint.Acceptable {
switch {
case resultMeasures.meanDeviation == 0:
hint.Description = descriptionBingo
case resultMeasures.meanDeviation < maxAcceptableMeanDeviation/2:
hint.Description = descriptionGoodJob
case resultMeasures.meanDeviation < maxAcceptableMeanDeviation:
hint.Description = descriptionNotBad
case compareFloats(resultMeasures.meanDeviation, maxAcceptableMeanDeviation):
hint.Description = descriptionYouCanDoBetter
}
}

return hint, nil
Expand Down Expand Up @@ -93,19 +113,17 @@ func getMeasures(values []int) hintMeasurements {
// Maximum deviation
r.maxDeviation = 0
for _, v := range values {
deviation := math.Abs(float64(r.median) - float64(v))
r.maxDeviation = math.Max(r.maxDeviation, deviation)
r.maxDeviation = math.Max(r.maxDeviation, deviation(v, r.median))
}

// Average deviation
sum := 0
for _, v := range values {
sum += int(math.Abs(float64(r.median) - float64(v)))
sum += int(deviation(v, r.median))
}
r.meanDeviation = float64(sum) / float64(len(values))

return r

}

func median(values []int) int {
Expand All @@ -119,3 +137,11 @@ func median(values []int) int {
center := len(values) / 2
return values[center]
}

func deviation(value int, median int) float64 {
return math.Abs(float64(median) - float64(value))
}

func compareFloats(a, b float64) bool {
return math.Abs(a-b) < float64Epsilon
}
30 changes: 17 additions & 13 deletions pkg/game/hints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ func TestMedian(t *testing.T) {

func TestHint(t *testing.T) {
deck := protocol.Deck{"1", "2", "3", "5", "8", "13", "21"}
t.Log("deck:", deck)

type Case struct {
values []protocol.VoteValue
Expand All @@ -46,67 +45,72 @@ func TestHint(t *testing.T) {
// But the intention was also to see how the algorithm behaves for different scenarios.

testCases := []Case{
{
values: []protocol.VoteValue{"3", "3", "3", "3", "3"},
measurements: hintMeasurements{median: 2, meanDeviation: 0, maxDeviation: 0},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionBingo},
},
{
values: []protocol.VoteValue{"3", "3", "3", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.2, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: true, Value: "3"},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionGoodJob},
},
{
values: []protocol.VoteValue{"3", "3", "3", "3", "8"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.4, maxDeviation: 2},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: maximumDeviationIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", Description: maximumDeviationIsTooHigh},
},
{
values: []protocol.VoteValue{"3", "3", "3", "3", "13"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.6, maxDeviation: 3},
// Test: varietyOfVotesIsTooHigh takes precedence over maximumDeviationIsTooHigh
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", Description: varietyOfVotesIsTooHigh},
},
{
values: []protocol.VoteValue{"3", "3", "3", "3", "21"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.8, maxDeviation: 4},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", Description: varietyOfVotesIsTooHigh},
},
{
values: []protocol.VoteValue{"3", "3", "3", "5", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.4, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: true, Value: "3"},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionNotBad},
},
{
values: []protocol.VoteValue{"3", "3", "3", "5", "8"},
measurements: hintMeasurements{median: 2, meanDeviation: 0.6, maxDeviation: 2},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", Description: varietyOfVotesIsTooHigh},
},
{
values: []protocol.VoteValue{"2", "3", "3", "3", "3", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 2 / 7.0, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: true, Value: "3"},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionNotBad},
},
{
values: []protocol.VoteValue{"2", "3", "3", "3", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 2 / 6.0, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: true, Value: "3"},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionNotBad},
},
{
values: []protocol.VoteValue{"2", "3", "3", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 2 / 5.0, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: true, Value: "3"},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionNotBad},
},
{
values: []protocol.VoteValue{"2", "3", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 2 / 4.0, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: true, Value: "3", Description: descriptionYouCanDoBetter},
},
{
values: []protocol.VoteValue{"2", "3", "5"},
measurements: hintMeasurements{median: 2, meanDeviation: 2 / 3.0, maxDeviation: 1},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "3", Description: varietyOfVotesIsTooHigh},
},
{
// This also tests round up median when even number of votes
values: []protocol.VoteValue{"2", "3", "5", "8"},
measurements: hintMeasurements{median: 3, meanDeviation: 1, maxDeviation: 2},
expectedHint: protocol.Hint{Acceptable: false, Value: "5", RejectReason: varietyOfVotesIsTooHigh},
expectedHint: protocol.Hint{Acceptable: false, Value: "5", Description: varietyOfVotesIsTooHigh},
},
}

Expand Down
11 changes: 4 additions & 7 deletions pkg/protocol/hint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ type Hint struct {
// a suggestion to discuss and re-vote.
Acceptable bool

// RejectReason contains an explanation of why the vote is not acceptable.
// When Acceptable is true, RejectReason is empty.
RejectReason string

// Value is the recommended value for the issue.
// It's guaranteed to be one of the values from the deck.
Value VoteValue

// Advice is a text advice for the team about current vote.
// It might contain players mentions in form "@<id>", where <id> a particular player ID.
Advice string
// Description contains text message for the team.
// When Acceptable is false, Description explaining the reject reasoning.
// When Acceptable is true, Description contains some congratulatory message.
Description string
}

0 comments on commit 76e2658

Please sign in to comment.