Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hint description #64

Merged
merged 1 commit into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading