diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 404b282..12e1635 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: with: go-version: '1.22' - name: Caching go modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} diff --git a/TODO.md b/TODO.md index 3f5ef5a..1708876 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -* Implment sub, hsub, vsub +* Implement sub, hsub, vsub * Verify stats for games in tests diff --git a/data/2021/20210911-1.yaml b/data/2021/20210911-1.yaml index f79b465..a796581 100644 --- a/data/2021/20210911-1.yaml +++ b/data/2021/20210911-1.yaml @@ -19,7 +19,7 @@ visitorplays: - mj18,CBX,S8/G8.B-1;3-H - bj00,BFB,WP.1-2 - bj00,BFBBCB,W.B-1 - - kg2,BCFFB,HP.B-1;1-2;2-3 + - kg2,BCFFBH,HP.B-1;1-2;2-3 - ms11,CBSC,K - as7,X,6/L6 - inn,2,1 diff --git a/data/2021/20210911-3.yaml b/data/2021/20210911-3.yaml index 3ffb477..c067e49 100644 --- a/data/2021/20210911-3.yaml +++ b/data/2021/20210911-3.yaml @@ -17,7 +17,7 @@ visitorplays: - mf17,X,S9/G3.B-1 - aw6,BBX,S8/L8.B-1;1-2 - mj18,BX,FC5/G.B-1;1-2;2X3(5) - - bj00,CBFX,HP.B-1;1-2;2-3 + - bj00,CBFH,HP.B-1;1-2;2-3 - kg2,X,S9/G9.B-1;1-3;2-H;3-H - ms11,BX,FC5.B-1;1-2;3XH(52) - as7,CCX,S6/G.B-1;1-2;2-3 @@ -56,10 +56,10 @@ homeplays: - 26,X,43/G - inn,2,0 - 19,BX,53/G - - 20,X,HP.B-1 + - 20,H,HP.B-1 - 14,CCC,K - 92,X,E4/G4.B-1;1-2 - - 7,CX,HP.B-1;1-2;2-3 + - 7,CH,HP.B-1;1-2;2-3 - 88,CCX,8/F8 - inn,3,0 - 21,X,7/F7S diff --git a/data/2021/20210912-2.yaml b/data/2021/20210912-2.yaml index 62d92eb..5e38793 100644 --- a/data/2021/20210912-2.yaml +++ b/data/2021/20210912-2.yaml @@ -18,12 +18,12 @@ visitorplays: - aw6,X,D7/G56.B-2;1-3 - mj18,CBCX,S7/G56.BX2(714);2-H;3-H - ms11,BFX,63/G - - kg2,BX,HP + - kg2,BH,HP - bj00,CFBX,S9/F89S.B-1;1-3 - cm22,BB,SB2 - cm22,BBCFFFS,K - inn,2,2 - - rv10,X,HP + - rv10,H,HP - ss16,BX,S8/L8.B-1;1-2 - pitcher,13 - mf17,CX,S1/G3/B/SH.B-3(E1/TH);1-H;2-H,chaos! @@ -33,7 +33,7 @@ visitorplays: - ms11,BCX,D7/L87.B-2;2-H;3-H - kg2,BCX,63/G.2-3 - bj00,BBX,S8/G56.3-H;B-3(E7) - - cm22,X,HP.B-1 + - cm22,H,HP.B-1 - rv10,B,SB2 - rv10,BBCFFX,43/G - inn,3,7 diff --git a/data/2021/20210926-1.yaml b/data/2021/20210926-1.yaml index e1fc07e..335e738 100644 --- a/data/2021/20210926-1.yaml +++ b/data/2021/20210926-1.yaml @@ -54,7 +54,7 @@ homeplays: - 32,BCBX,7/F7.2-3;3-H - 21,B,WP.3-H - 21,BCSBS,K - - 1,BFB,HP.B-1 + - 1,BFBH,HP.B-1 - 28,BBBCSX,S1/L1.B-1;1-2 - 5,B,SB3;SB2 - 5,BBX,6/L6 diff --git a/data/2021/20211009-1.yaml b/data/2021/20211009-1.yaml index 69e4f81..1d67fc5 100644 --- a/data/2021/20211009-1.yaml +++ b/data/2021/20211009-1.yaml @@ -63,7 +63,7 @@ homeplays: - inn,2,0 - 13,FX,S7/G5.B-1 - 12,X,FC1/B.B-1;1-2,should have thrown 1 - - 22,CBX,HP.B-1;1-2;2-3 + - 22,CBH,HP.B-1;1-2;2-3 - 8,FX,4/P4 - 00,SCBBS,K - 20,SSS,K @@ -76,13 +76,13 @@ homeplays: - 23,BBBCB,W.B-1 - 27,BS,SB3.1-2 - 27,BSCX,8/F8/SF.2-3;3-H - - 11,X,HP + - 11,H,HP - 17,CB,SB2.1-2;3-H - 17,CBSX,S9/L9.B-1;2-H - 13,CX,E7/F7.B-1;1-2 - 12,B,WP.2-3;1-2 - 12,BCX,E5/G5/TH.B-1;2-3;3-H - - 22,X,HP.B-1;1-2 + - 22,H,HP.B-1;1-2 - 8,B,CSH(15).3-H(E1/TH);1-2;2-3 - 8,BBCCBC,K - inn,4,5 diff --git a/data/2021/20211016-1.yaml b/data/2021/20211016-1.yaml index c6f3753..817accd 100644 --- a/data/2021/20211016-1.yaml +++ b/data/2021/20211016-1.yaml @@ -32,7 +32,7 @@ visitorplays: - ac14,B,SB2 - ac14,BSX,S7/P6D.B-1;2X3(75);3-H,Angelina thrown out advancing to 3 - kn21,B,WP.1-2 - - kn21,BCBS,HP,hit by a strike? + - kn21,BCBSH,HP,hit by a strike? - mf17,FBFBFX,T7/F78.B-3;2-H;1-H - aw6,BBBF,FLE2 - aw6,BBBFFB,W.B-1 diff --git a/data/2021/20211016-3.yaml b/data/2021/20211016-3.yaml index dd185c9..cabd05a 100644 --- a/data/2021/20211016-3.yaml +++ b/data/2021/20211016-3.yaml @@ -57,7 +57,7 @@ homeplays: - 2,FSC,K - 10,BCBBX,3/P3 - inn,2,0 - - 13,FX,HP + - 13,FH,HP - 25,B,CS2(26) - 25,BX,43/G - 00,BBFBB,W.B-1 diff --git a/data/2021/20211017-1.yaml b/data/2021/20211017-1.yaml index 57b2071..25cf9fe 100644 --- a/data/2021/20211017-1.yaml +++ b/data/2021/20211017-1.yaml @@ -14,7 +14,7 @@ league: PGF visitorplays: - pitcher,11 - mf17,X,13/G - - aw6,X,HP.B-1 + - aw6,H,HP.B-1 - cm22,CCBX,FC6/G6.B-1;1X2(64) - kg2,BCB,PB.1-2 - kg2,BCBBX,53/G diff --git a/data/2021/20211030-1.yaml b/data/2021/20211030-1.yaml index 54fcbb5..7ab2947 100644 --- a/data/2021/20211030-1.yaml +++ b/data/2021/20211030-1.yaml @@ -29,7 +29,7 @@ visitorplays: - le13,FBX,13/G1,Lana bats out of order by umpire ruling - ss16,BX,DGR/F7 - kn21,B,WP.2-3 - - kn21,BSFBX,HP + - kn21,BSFBH,HP - rv10,CFX,8/F8/SF.3-H - as7,FFX,3/P3 - inn,4,2 @@ -37,7 +37,7 @@ visitorplays: - aw6,CX,S2/B.B-1 - cm22,CX,53/G.1-2 - kg2,CCX,S8/L8.B-1;2-H - - ms11,X,HP.B-1;1-2 + - ms11,H,HP.B-1;1-2 - bj00,X,D8/F89.B-2;1-H;2-H - mj18,CFFX,9/F9 - inn,5,5 diff --git a/data/2021/20211030-2.yaml b/data/2021/20211030-2.yaml index 74a94dc..3e481b6 100644 --- a/data/2021/20211030-2.yaml +++ b/data/2021/20211030-2.yaml @@ -25,7 +25,7 @@ visitorplays: - le13,BBB,WP.1-2 - le13,BBBB,W.B-1 - ss16,X,E6/P1.B-1;1-2;2-3 - - kn21,BFFBX,HP.B-1;1-2;2-3;3-H + - kn21,BFFBH,HP.B-1;1-2;2-3;3-H - rv10,FX,E4.B-1;1X2(4);2-3;3-H - inn,2,6 - as7,X,6/P6 @@ -45,7 +45,7 @@ visitorplays: - inn,4,10 - mf17,FBFBBX,5/P5 - aw6,X,7/L7 - - cm22,X,HP + - cm22,H,HP - kg2,BCBX,S7/G7.B-1;1-2 - ms11,X,FC6.B-1;1X2(64);2-3 - inn,5,10 diff --git a/data/2021/20211030-3.yaml b/data/2021/20211030-3.yaml index d78fee4..d220a1c 100644 --- a/data/2021/20211030-3.yaml +++ b/data/2021/20211030-3.yaml @@ -26,7 +26,7 @@ visitorplays: - le13,BBX,S7/L7.B-1 - ss16,BB,SB2 - ss16,BBBB,W.B-1 - - kn21,SX,HP.B-1;1-2;2-3 + - kn21,SH,HP.B-1;1-2;2-3 - rv10,FCX,S9/F9.B-2(E9);2-H;3-H - as7,BSBBX,43/P45.1-2;2-3 - pitcher,2 diff --git a/data/2021/20211031-1.yaml b/data/2021/20211031-1.yaml index a139fa0..b545e7c 100644 --- a/data/2021/20211031-1.yaml +++ b/data/2021/20211031-1.yaml @@ -30,9 +30,9 @@ visitorplays: - bj00,X,T9/L9.B-3;3-H - ms11,X,6/P6D - inn,5,2 - - le13,X,HP + - le13,H,HP - ss16,FBCFFFFBBB,W.B-1;1-2 - - mf17,X,HP.B-1;1-2;2-3 + - mf17,H,HP.B-1;1-2;2-3 - aw6,BBFBFB,W.B-1;1-2;2-3;3-H - cm22,BCCX,S3/G3.B-1;1-2;2-3;3-H - mj18,BFCBFBFX,S7/L7.B-1;1-2;2-3;3-H diff --git a/data/2021/20211031-2.yaml b/data/2021/20211031-2.yaml index 0801334..e9be945 100644 --- a/data/2021/20211031-2.yaml +++ b/data/2021/20211031-2.yaml @@ -63,8 +63,8 @@ homeplays: - 25,BX,S8/G64.B-1 - 44,C,SB2 - 44,CX,S8/P8S.B-2;2-3 - - 00,FX,HP.B-1 - - 9,X,HP.B-1;1-2;2-3;3-H + - 00,FH,HP.B-1 + - 9,H,HP.B-1;1-2;2-3;3-H - 42,BBFX,S8/L8.B-1;1-2;2-H;3-H - 3,BBX,S8/P4D.B-1;1-2;2-H - pitcher,bj00 diff --git a/data/2021/20211106-2.yaml b/data/2021/20211106-2.yaml index 61c5500..1118544 100644 --- a/data/2021/20211106-2.yaml +++ b/data/2021/20211106-2.yaml @@ -60,7 +60,7 @@ homeplays: - 1,BBFCFFBX,E5/G5.B-1 - 42,X,13/G1 - 28,X,S9/P9S.B-1;1-2 - - 7,X,HP.B-1;1-2;2-3 + - 7,H,HP.B-1;1-2;2-3 - 21,BCCX,D9/F9.B-2;1-3;2-H;3-H - 16,BX,E5/G5/TH.B-1;2-3;3-H - 17,X,D8/F8D.B-2;1-3;3-H @@ -86,7 +86,7 @@ homeplays: - pitcher,bj00 - 7,BX,7/F7 - 21,X,D8/L8D.B-2;1-H;2-H - - 16,BFFBBX,HP.B-1 + - 16,BFFBBH,HP.B-1 - 17,FX,53/G5 - inn,5,9 - 00,FX,13/G1 diff --git a/data/2021/20211119-1.yaml b/data/2021/20211119-1.yaml index ac8479d..8a111d0 100644 --- a/data/2021/20211119-1.yaml +++ b/data/2021/20211119-1.yaml @@ -34,8 +34,8 @@ visitorplays: - le13,BBBB,W.B-1 - rv10,X,S8/G56.B-1;1-2 - ss16,CBSX,1/P1/IF - - kn21,CBX,HP.B-1;1-2;2-3 - - as7,X,HP.B-1;1-2;2-3;3-H + - kn21,CBH,HP.B-1;1-2;2-3 + - as7,H,HP.B-1;1-2;2-3;3-H - mf17,BBBB,W.B-1;1-2;2-3;3-H - aw6,BX,FC5.B-1;1-2;2-3;3XH(52) - cm22,CBBCFC,K diff --git a/data/2021/20211120-1.yaml b/data/2021/20211120-1.yaml index fc2ef72..0117662 100644 --- a/data/2021/20211120-1.yaml +++ b/data/2021/20211120-1.yaml @@ -22,7 +22,7 @@ visitorplays: - le13,X,63/G6 - ss16,FBFFBFX,63/G6 - inn,1 - - kn21,SCFX,HP + - kn21,SCFH,HP - as7,CFX,6/P6 - bj00,BBBB,W.B-1;1-2 - mf17,BCCFBX,S4/G4.B-1;1-2;2-3 diff --git a/data/2021/20211121-1.yaml b/data/2021/20211121-1.yaml index 270fc4c..f7d0d05 100644 --- a/data/2021/20211121-1.yaml +++ b/data/2021/20211121-1.yaml @@ -68,7 +68,7 @@ homeplays: - aw6,CSS,K - inn,3 - cm22,BBBB,W.B-1 - - kg2,CX,HP.B-1;1-2 + - kg2,CH,HP.B-1;1-2 - mj18,BBCFX,FC4.B-1;1X2(46);2-3 - ms11,B,SB2 - ms11,BX,S7/L7.B-2(E2);2-3;3-H diff --git a/hack/data-export.sh b/hack/data-export.sh deleted file mode 100755 index 37ce80d..0000000 --- a/hack/data-export.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -set -x -rm -rf export -go run main.go data-export --re-matrix data/d3_softball_re_2022.csv \ - --us pride --games data -d export - -(cd export -ls -aws s3 cp --recursive . s3://slshen-public-us-west-2/pride-jf) diff --git a/pkg/boxscore/box.go b/pkg/boxscore/box.go index c15fb29..277b812 100644 --- a/pkg/boxscore/box.go +++ b/pkg/boxscore/box.go @@ -48,8 +48,8 @@ func NewBoxScore(g *game.Game, re stats.RunExpectancy) (*BoxScore, error) { boxscore := &BoxScore{ Game: g, Stats: gs, - HomeLineup: &Lineup{gs.TeamStats[g.Home.Name]}, - VisitorLineup: &Lineup{gs.TeamStats[g.Visitor.Name]}, + HomeLineup: &Lineup{gs.GetStats(g.Home)}, + VisitorLineup: &Lineup{gs.GetStats(g.Visitor)}, } if err := boxscore.run(); err != nil { return nil, err diff --git a/pkg/boxscore/lineup.go b/pkg/boxscore/lineup.go index 2cadb9b..52b4da3 100644 --- a/pkg/boxscore/lineup.go +++ b/pkg/boxscore/lineup.go @@ -23,6 +23,21 @@ func (lineup *Lineup) BattingTable() *dataframe.Data { dataframe.Rename("Walks", "BB"), ) idx := dat.GetIndex() + names := idx.GetColumn("#") + dat.RApply(func(row int) { + // Shorten "Babe Ruth" to "B Ruth" + name := names.GetString(row) + if strings.ContainsRune(name, ' ') { + parts := strings.Split(name, " ") + for i, part := range parts[0 : len(parts)-1] { + if len(part) > 2 { + // unless it's a very short name + parts[i] = part[0:1] + } + } + names.GetStrings()[row] = strings.Join(parts, " ") + } + }) idx.GetColumn("AB").Summary = dataframe.Sum idx.GetColumn("H").Summary = dataframe.Sum idx.GetColumn("K").Summary = dataframe.Sum diff --git a/pkg/game/game.go b/pkg/game/game.go index baa9907..d26e03c 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -201,7 +201,7 @@ func (g *Game) GetVisitorStates() []*State { func (g *Game) generateStates() (errs error) { var err error - g.visitorStates, err = g.runPlays(g.Visitor, g.Home, Top, + g.visitorStates, err = g.runEvents(g.Visitor, g.Home, Top, g.File.VisitorEvents) if err != nil { errs = multierror.Append(errs, err) @@ -209,7 +209,7 @@ func (g *Game) generateStates() (errs error) { if len(g.visitorStates) > 0 { g.Final.Visitor = g.visitorStates[len(g.visitorStates)-1].Score } - g.homeStates, err = g.runPlays(g.Home, g.Visitor, Bottom, + g.homeStates, err = g.runEvents(g.Home, g.Visitor, Bottom, g.File.HomeEvents) if err != nil { errs = multierror.Append(errs, err) @@ -246,7 +246,7 @@ func (g *Game) generateStates() (errs error) { return } -func (g *Game) runPlays(battingTeam, fieldingTeam *Team, half Half, events []*gamefile.Event) (states []*State, errs error) { +func (g *Game) runEvents(battingTeam, fieldingTeam *Team, half Half, events []*gamefile.Event) (states []*State, errs error) { if events == nil { return } @@ -271,18 +271,6 @@ func (g *Game) runPlays(battingTeam, fieldingTeam *Team, half Half, events []*ga errs = multierror.Append(errs, err) } if state != nil { - for _, after := range event.Afters { - if after.CourtesyRunner != nil { - // assume courtesy runner is for batter - cr := m.battingTeam.parsePlayerID(*after.CourtesyRunner) - for i := range state.Runners { - if state.Runners[i] == state.Batter { - state.Runners[i] = cr - break - } - } - } - } state.Comment = event.Comment states = append(states, state) lastState = state @@ -322,13 +310,6 @@ func (g *Game) GetDate() time.Time { return g.date } -func (g *Game) GetUsAndThem(us string) (*Team, *Team) { - if g.Home.IsUs(us) { - return g.Visitor, g.Home - } - return g.Visitor, g.Home -} - func (g *Game) GetTournament() string { if g.Tournament != "" { return g.Tournament diff --git a/pkg/game/gamemachine.go b/pkg/game/gamemachine.go index 9572a84..8dc89a5 100644 --- a/pkg/game/gamemachine.go +++ b/pkg/game/gamemachine.go @@ -30,12 +30,15 @@ func newGameMachine(battingTeam, fieldingTeam *Team) *gameMachine { func (m *gameMachine) newState(pos lexer.Position, lastState *State) *State { state := &State{ Pos: FileLocation{Filename: pos.Filename, Line: pos.Line}, + BattingTeam: m.battingTeam, + FieldingTeam: m.fieldingTeam, InningNumber: lastState.InningNumber, Outs: lastState.Outs, Half: lastState.Half, Score: lastState.Score, Pitcher: m.pitcher, Runners: [3]PlayerID{}, + Defense: lastState.Defense, LastState: lastState, } if state.Outs == 3 { @@ -53,6 +56,8 @@ func (m *gameMachine) handleAlternative(alt *gamefile.Alternative, lastState *St // we don't have the real real last state (which the last play // of the last inning), so we'll just fake one here realLastState = &State{ + BattingTeam: m.battingTeam, + FieldingTeam: m.fieldingTeam, InningNumber: lastState.InningNumber - 1, Outs: 3, Half: lastState.Half, @@ -86,6 +91,28 @@ func (m *gameMachine) handleActualPlay(play *gamefile.ActualPlay, lastState *Sta return nil, NewError("no batter for %s", play.GetPos(), play.GetCode()) } err := m.handlePlay(play, state) + for _, after := range play.Afters { + // handle subs for runners on base + var runnerEnter, runnerExit PlayerID + if after.CourtesyRunner != nil { + runnerEnter = m.battingTeam.parsePlayerID(*after.CourtesyRunner) + if after.CourtesyRunnerFor == nil { + runnerExit = state.Batter + } else { + runnerExit = m.battingTeam.parsePlayerID(*after.CourtesyRunnerFor) + } + } + if after.Sub != nil { + runnerEnter = m.battingTeam.parsePlayerID(after.Sub.Enter) + runnerExit = m.battingTeam.parsePlayerID(after.Sub.Exit) + } + for i := range state.Runners { + if state.Runners[i] == runnerExit { + state.Runners[i] = runnerEnter + break + } + } + } return state, err } @@ -112,8 +139,8 @@ func (m *gameMachine) handlePlay(play gamefile.Play, state *State) error { if state.Complete { // verify that we've struck out with 3 strikes, or walked with 4 balls // or that we put the ball in play without walking or striking out - _, balls, strikes := state.Pitches.Count() - if state.Play.IsBallInPlay() { + known, _, balls, strikes := state.Pitches.Count() + if known && state.Play.IsBallInPlay() { if strikes > 2 { return NewError("cannot put ball in play with %d strikes (%s)", state.Pos, strikes, play.GetCode()) } @@ -121,8 +148,8 @@ func (m *gameMachine) handlePlay(play gamefile.Play, state *State) error { return NewError("cannot put ball in play with %d balls (%s)", state.Pos, balls, play.GetCode()) } } - if state.Play.IsStrikeOut() { - if state.Pitches[len(state.Pitches)-1] == 'X' { + if known && state.Play.IsStrikeOut() { + if state.Pitches.Last() == 'X' { return NewError("strike out pitch sequence should not end in X", state.Pos) } if strikes != 3 { @@ -132,8 +159,8 @@ func (m *gameMachine) handlePlay(play gamefile.Play, state *State) error { return NewError("cannot strike out with more than 3 balls", state.Pos) } } - if state.Play.IsWalk() { - if state.Pitches[len(state.Pitches)-1] == 'X' { + if known && state.Play.IsWalk() { + if state.Pitches.Last() == 'X' { return NewError("walk pitch sequence should not end in X", state.Pos) } if strikes > 2 { @@ -143,7 +170,7 @@ func (m *gameMachine) handlePlay(play gamefile.Play, state *State) error { return NewError("must walk with 4 balls", state.Pos) } } - if !strings.HasSuffix(string(state.Pitches), "X") && + if known && state.Pitches.Last() != 'X' && state.Play.IsBallInPlay() { // fix up pitches state.Pitches += "X" @@ -167,6 +194,31 @@ func (m *gameMachine) handleSpecialEvent(event *gamefile.Event, state *State) (* if event.Pitcher != "" { m.pitcher = m.fieldingTeam.parsePlayerID(event.Pitcher) } + if event.PlayerName != nil { + playerID := m.battingTeam.parsePlayerID(event.PlayerName.Player) + player := m.battingTeam.GetPlayer(playerID) + player.Name = event.PlayerName.GetName() + } + if len(event.Defense) > 0 { + state = state.Copy() + for _, pp := range event.Defense { + state.Defense[pp.PositionNumber()-1] = m.fieldingTeam.parsePlayerID(pp.Player) + } + return state, nil + } + if event.DefenseSub != nil { + enter := m.fieldingTeam.parsePlayerID(event.DefenseSub.Enter) + exit := m.fieldingTeam.parsePlayerID(event.DefenseSub.Exit) + for _, pp := range event.Defense { + player := m.fieldingTeam.parsePlayerID(pp.Player) + if player == exit { + state = state.Copy() + state.Defense[pp.PositionNumber()-1] = enter + return state, nil + } + } + return nil, NewError("cannot sub %s for %s because %s is not in the field", event.Pos, enter, exit, exit) + } if event.Score != "" { if state.Outs != 3 { return nil, NewError("the inning with %d outs has not ended after %s", @@ -196,6 +248,8 @@ func (m *gameMachine) handleSpecialEvent(event *gamefile.Event, state *State) (* return nil, NewError("radj must be at the inning start", event.Pos) } lastState := &State{ + BattingTeam: m.battingTeam, + FieldingTeam: m.fieldingTeam, InningNumber: state.InningNumber + 1, Half: state.Half, Outs: 0, @@ -421,6 +475,12 @@ func (m *gameMachine) handlePlayCode(play gamefile.Play, state *State) error { state.Play = Play{ Type: HitByPitch, } + if actualPlay, ok := play.(*gamefile.ActualPlay); ok { + if !strings.HasSuffix(actualPlay.PitchSequence, "H") { + return NewError("HP pitch sequence %s should end with H", play.GetPos(), + actualPlay.PitchSequence) + } + } m.impliedAdvance(play, state, "B-1") state.Complete = true case pp.playIs("E$"): diff --git a/pkg/game/pitches.go b/pkg/game/pitches.go index 2eb42bd..ad0fc61 100644 --- a/pkg/game/pitches.go +++ b/pkg/game/pitches.go @@ -13,14 +13,33 @@ func (ps Pitches) CountUp(codes ...rune) (count int) { return } -func (ps Pitches) Last() string { +func (ps Pitches) Last() rune { + if ps.IsUnknown() { + return '?' + } if l := len(ps); l > 0 { - return string(ps[l-1]) + return rune(ps[l-1]) + } + return 0 +} + +func (ps Pitches) IsUnknown() bool { + if len(ps) == 0 { + return false } - return "" + for _, p := range ps { + if p != '?' && p != '.' { + return false + } + } + return true } -func (ps Pitches) Count() (string, int, int) { +func (ps Pitches) Count() (bool, string, int, int) { + if ps.IsUnknown() { + // unrecorded + return false, "?", 0, 0 + } balls := ps.CountUp('B') strikes := 0 for _, p := range ps { @@ -30,5 +49,5 @@ func (ps Pitches) Count() (string, int, int) { strikes++ } } - return fmt.Sprintf("%d-%d", balls, strikes), balls, strikes + return true, fmt.Sprintf("%d-%d", balls, strikes), balls, strikes } diff --git a/pkg/game/pitches_test.go b/pkg/game/pitches_test.go index b1570d9..8834873 100644 --- a/pkg/game/pitches_test.go +++ b/pkg/game/pitches_test.go @@ -23,10 +23,11 @@ func TestPitches(t *testing.T) { {"MCL", 0, 3, "0-3"}, } { ps := Pitches(tc.in) - count, balls, strikes := ps.Count() + known, count, balls, strikes := ps.Count() + assert.True(known) assert.Equal(tc.count, count) assert.Equal(tc.b, balls) assert.Equal(tc.s, strikes) } - assert.Equal("X", Pitches("CX").Last()) + assert.Equal('X', Pitches("CX").Last()) } diff --git a/pkg/game/state.go b/pkg/game/state.go index 278e23d..3ff9266 100644 --- a/pkg/game/state.go +++ b/pkg/game/state.go @@ -21,19 +21,19 @@ const ( type State struct { Pos FileLocation `yaml:",flow"` + BattingTeam *Team `yaml:"-"` + FieldingTeam *Team `yaml:"-"` InningNumber int Half Outs int Score int Pitcher PlayerID PlateAppearance - // Fielders []int `yaml:",flow,omitempty"` - - Runners [3]PlayerID `yaml:",omitempty,flow"` - // Runners []PlayerID `yaml:",flow"` - Comment string `yaml:",omitempty"` - LastState *State `yaml:"-"` - AlternativeFor *State `yaml:"-"` + Defense [9]PlayerID `yaml:",flow,omitempty"` + Runners [3]PlayerID `yaml:",omitempty,flow"` + Comment string `yaml:",omitempty"` + LastState *State `yaml:"-"` + AlternativeFor *State `yaml:"-"` } type PlateAppearance struct { @@ -85,6 +85,11 @@ func (state *State) IsAB() bool { state.Modifiers.Contains(SacrificeFly, SacrificeHit)) } +func (state *State) Copy() *State { + s := *state + return &s +} + func (pa *PlateAppearance) GetPlayAdvancesCode() string { s := &strings.Builder{} fmt.Fprintf(s, "%s", pa.PlayCode) @@ -99,7 +104,3 @@ var playerIDRegexp = regexp.MustCompile(`^[a-z]*\d+$`) func IsPlayerID(s string) bool { return playerIDRegexp.MatchString(s) } - -func (id PlayerID) IsUs() bool { - return len(id) > 0 && !(id[0] >= '0' && id[0] <= '9') -} diff --git a/pkg/game/team.go b/pkg/game/team.go index 206c238..9767771 100644 --- a/pkg/game/team.go +++ b/pkg/game/team.go @@ -17,6 +17,7 @@ type TeamID string type Team struct { ID TeamID Name string `yaml:"name"` + Us bool `yaml:"us"` Players map[PlayerID]*Player playerIDs map[string]PlayerID @@ -60,10 +61,6 @@ func GetTeam(dir, name, id string) (*Team, error) { return team, nil } -func (team *Team) IsUs(us string) bool { - return strings.HasPrefix(strings.ToLower((team.Name)), us) -} - func (team *Team) readFile(dir, id string) error { path := filepath.Join(dir, fmt.Sprintf("%s.yaml", id)) dat, err := os.ReadFile(path) @@ -76,7 +73,7 @@ func (team *Team) readFile(dir, id string) error { for playerID, player := range team.Players { player.PlayerID = playerID if player.Number == "" { - player.Number = getDefaultPlayerNumber(playerID) + player.Number = team.getDefaultPlayerNumber(playerID) } } return nil @@ -92,11 +89,12 @@ func (team *Team) GetPlayer(id PlayerID) *Player { player := &Player{ PlayerID: id, Name: string(id), - Number: getDefaultPlayerNumber(id), + Number: team.getDefaultPlayerNumber(id), } if player.Name == player.Number { player.Name = "" } + team.Players[id] = player return player } @@ -117,8 +115,12 @@ func (team *Team) parsePlayerID(s string) PlayerID { return PlayerID(s) } -func getDefaultPlayerNumber(player PlayerID) string { - return playerNumberRegexp.FindString(string(player)) +func (team *Team) getDefaultPlayerNumber(player PlayerID) string { + n := playerNumberRegexp.FindString(string(player)) + if n != "" { + return n + } + return fmt.Sprintf("00%d", len(team.Players)+1) } func (player *Player) NameOrNumber() string { @@ -130,3 +132,20 @@ func (player *Player) NameOrNumber() string { } return "?" } + +func (player *Player) UnmarshalYAML(value *yaml.Node) error { + switch value.Kind { + case yaml.ScalarNode: + player.Name = value.Value + return nil + case yaml.MappingNode: + var v any + if err := value.Decode(&v); err != nil { + return err + } + player.Name = v.(map[string]any)["name"].(string) + return nil + default: + return fmt.Errorf("cannot unmarshal player") + } +} diff --git a/pkg/game/testdata/g1.gm b/pkg/game/testdata/g1.gm new file mode 100644 index 0000000..aabe34d --- /dev/null +++ b/pkg/game/testdata/g1.gm @@ -0,0 +1,18 @@ +date: 4/16/2025 +visitor: Visalia Victors +homeid: happy-homes +--- +visitorplays +pitching aa1 +1 va BCBBB W +2 vb CFS K +3 vc X 13/G1 1-2 +4 vd X E6/G6 + alt 63/G6 +5 ve CFX 8/F8 +score 0 +homeplays +pitching vp +field bb2 at 2 cc3 at 3 dd4 at 4 ee5 at 5 ff6 at 6 +field gg7 at 7 hh8 at 8 ii9 at 9 +1 aa1 CCFBX S8/G8 diff --git a/pkg/game/testdata/happy-homes.yaml b/pkg/game/testdata/happy-homes.yaml new file mode 100644 index 0000000..af3c48e --- /dev/null +++ b/pkg/game/testdata/happy-homes.yaml @@ -0,0 +1,13 @@ +name: Happy Homes +players: + aa1: Alice A + bb2: Beatice B + cc3: Charlize C + dd4: Dorotohy D + ee4: Esther E + ff5: Francine F + gg6: Georgina G + hh7: Helena H + ii8: India I + jj9: Julia J + kk10: Kate K \ No newline at end of file diff --git a/pkg/gamefile/file.go b/pkg/gamefile/file.go index bb83e65..9d10347 100644 --- a/pkg/gamefile/file.go +++ b/pkg/gamefile/file.go @@ -38,29 +38,53 @@ type Property struct { } type Event struct { - Pos Position - Alternative *Alternative `parser:"'alt' @@ (NL|EOF)"` - Pitcher string `parser:"| ('pitcher'|'pitching') @Token (NL|EOF)"` - RAdjRunner Numbers `parser:"| 'radj' @Token"` - RAdjBase string `parser:" @Token (NL|EOF)"` - Score string `parser:"| 'score' @Token (NL|EOF)"` - Final string `parser:"| 'final' @Token (NL|EOF)"` - Sub *LineupChange `parser:"| @@"` - Play *ActualPlay `parser:"| @@"` - Afters []*LineupChange `parser:" @@*"` - Comment string `parser:" @Comment? (NL|EOF)"` - Empty bool `parser:"| @NL"` -} - -type LineupChange struct { - CourtesyRunner *string `parser:"'cr' @Token"` - Conference *bool `parser:"| @'conf'"` - SubEnter string `parser:"| 'sub' @Token"` - SubExit string `parser:"| 'for' @Token"` - HSubEnter string `parser:"| 'hsub' @Token"` - HSubExit string `parser:" 'for' @Token (NL|EOF)"` - VSubEnter string `parser:"| 'vsub' @Token"` - VSubExit string `parser:" 'for' @Token (NL|EOF)"` + Pos Position + Alternative *Alternative `parser:"'alt' @@ (NL|EOF)"` + Pitcher string `parser:"| ('pitcher'|'pitching') @Token"` + ReplacedPitcher *string `parser:" ( 'for' @Token )? (NL|EOF)"` + RAdjRunner Numbers `parser:"| 'radj' @Token"` + RAdjBase string `parser:" @Token (NL|EOF)"` + Score string `parser:"| 'score' @Token (NL|EOF)"` + Final string `parser:"| 'final' @Token (NL|EOF)"` + Play *ActualPlay `parser:"| @@"` + Comment string `parser:" @Comment? (NL|EOF)"` + Empty bool `parser:"| @NL"` + Defense []*PlayerPosition `parser:"| 'defense' @@* (NL|EOF)"` + Sub *Sub `parser:"| @@ (NL|EOF)"` + DefenseSub *DefenseSub `parser:"| @@ (NL|EOF)"` + PlayerName *PlayerName `parser:"| @@ (NL|EOF)"` +} + +type PlayerPosition struct { + Player string `parser:"@Token"` + Position string `parser:" 'at' @Token"` +} + +type Sub struct { + Enter string `parser:"'sub' @Token"` + Exit string `parser:" 'for' @Token"` +} + +type DefenseSub struct { + Enter string `parser:"'dsub' @Token"` + Exit string `parser:" 'for' @Token"` +} + +type AfterPlayChange struct { + CourtesyRunner *string `parser:"'cr' @Token"` + CourtesyRunnerFor *string `parser:" ('for' @Token)?"` + Conference *bool `parser:"| @'conf'"` + Sub *Sub `parser:"| @@"` + DefenseSub *DefenseSub `parser:"| @@"` +} + +type PlayerName struct { + Player string `parser:"'name' @Token"` + Names []string `parser:"@Token+"` +} + +func (pn PlayerName) GetName() string { + return strings.Join(pn.Names, " ") } type Play interface { @@ -71,12 +95,13 @@ type Play interface { type ActualPlay struct { Pos Position - ContinuedPlateAppearance bool `parser:"((@'...')"` - PlateAppearance Numbers `parser:" | (@PA"` - Batter string `parser:" @Token))"` - PitchSequence string `parser:" @Token"` - Code string `parser:" @Token"` - Advances []string `parser:" @Advance*"` + ContinuedPlateAppearance bool `parser:"((@'...')"` + PlateAppearance Numbers `parser:" | (@PA"` + Batter string `parser:" @Token))"` + PitchSequence string `parser:" @Token"` + Code string `parser:" @Token"` + Advances []string `parser:" @Advance*"` + Afters []*AfterPlayChange `parser:" @@*"` } var _ Play = (*ActualPlay)(nil) @@ -84,7 +109,8 @@ var _ Play = (*ActualPlay)(nil) type Alternative struct { Pos Position Code string `parser:"@Token"` - Advances []string `parser:"@Advance*"` + Advances []string `parser:" @Advance*"` + Credit []string `parser:" ('credit' @Token*)?"` Comment string `parser:" @Comment?"` } @@ -108,15 +134,23 @@ func (f *File) Parse(r io.Reader) error { return nil } -func (p *ActualPlay) normalize() { +var validPitches = ".?XHSFLMCB" + +func (p *ActualPlay) Validate() error { if p == nil { - return + return nil } for i, adv := range p.Advances { p.Advances[i] = strings.ToUpper(adv) } p.Code = strings.ToUpper(p.Code) p.PitchSequence = strings.ToUpper(p.PitchSequence) + for _, pitch := range p.PitchSequence { + if !strings.ContainsRune(validPitches, pitch) { + return fmt.Errorf("invalid pitch %c in %s", pitch, p.PitchSequence) + } + } + return nil } func (a *Alternative) normalize() { @@ -139,8 +173,15 @@ func (f *File) Validate() error { for _, te := range f.TeamEvents { for _, event := range te.Events { // make codes upper code - event.Play.normalize() + if err := event.Play.Validate(); err != nil { + return fmt.Errorf("%s: %w", event.Pos, err) + } event.Alternative.normalize() + for _, pp := range event.Defense { + if err := pp.Validate(); err != nil { + return fmt.Errorf("%s: %w", event.Pos, err) + } + } } switch te.HomeOrVisitor { case "homeplays": @@ -220,28 +261,56 @@ func (f *File) writeEvents(w io.Writer, name string, events []*Event) { fmt.Fprintf(w, " ... ") } fmt.Fprintf(w, "%s ", play.PitchSequence) - f.writeCodeAdvancesComment(w, play.Code, play.Advances, event.Afters, event.Comment) + f.writeCodeAdvances(w, play.Code, play.Advances) + f.writeAFters(w, play.Afters) + f.writeComment(w, event.Comment) + fmt.Fprintln(w) case event.Alternative != nil: alt := event.Alternative fmt.Fprintf(w, " alt ") - f.writeCodeAdvancesComment(w, alt.Code, alt.Advances, nil, alt.Comment) + f.writeCodeAdvances(w, alt.Code, alt.Advances) + if len(alt.Credit) > 0 { + fmt.Fprint(w, " credit") + for _, player := range alt.Credit { + fmt.Fprintf(w, " %s", player) + } + } + f.writeComment(w, alt.Comment) + fmt.Fprintln(w) case event.Pitcher != "": - fmt.Fprintf(w, "pitching %s\n", event.Pitcher) + fmt.Fprintf(w, "pitching %s", event.Pitcher) + if event.ReplacedPitcher != nil { + fmt.Fprintf(w, " for %s", *event.ReplacedPitcher) + } + fmt.Fprintln(w) case event.RAdjBase != "": fmt.Fprintf(w, "radj %s %s\n", event.RAdjRunner, event.RAdjBase) case event.Score != "": fmt.Fprintf(w, "score %s\n", event.Score) case event.Final != "": fmt.Fprintf(w, "final %s\n", event.Final) + case event.Sub != nil: + fmt.Fprintf(w, "sub %s for %s\n", event.Sub.Enter, event.Sub.Exit) + case event.DefenseSub != nil: + fmt.Fprintf(w, "dsub %s for %s\n", event.DefenseSub.Enter, event.DefenseSub.Exit) + case len(event.Defense) > 0: + fmt.Fprintf(w, "defense") + for _, pp := range event.Defense { + fmt.Fprintf(w, " %s at %s", pp.Player, pp.Position) + } + fmt.Fprintln(w) } } } -func (f *File) writeCodeAdvancesComment(w io.Writer, code string, advances []string, afters []*LineupChange, comment string) { +func (f *File) writeCodeAdvances(w io.Writer, code string, advances []string) { fmt.Fprintf(w, "%s", code) for _, adv := range advances { fmt.Fprintf(w, " %s", adv) } +} + +func (f *File) writeAFters(w io.Writer, afters []*AfterPlayChange) { for _, aft := range afters { if aft.Conference != nil { fmt.Fprint(w, " conf") @@ -249,11 +318,19 @@ func (f *File) writeCodeAdvancesComment(w io.Writer, code string, advances []str if aft.CourtesyRunner != nil { fmt.Fprintf(w, " cr %s", *aft.CourtesyRunner) } + if aft.Sub != nil { + fmt.Fprintf(w, " sub %s for %s", aft.Sub.Enter, aft.Sub.Exit) + } + if aft.DefenseSub != nil { + fmt.Fprintf(w, " dsub %s for %s", aft.DefenseSub.Enter, aft.DefenseSub.Exit) + } } +} + +func (f *File) writeComment(w io.Writer, comment string) { if comment != "" { fmt.Fprintf(w, " : %s", comment) } - fmt.Fprintln(w) } const GameDateFormat = "1/2/2006" @@ -297,3 +374,14 @@ func (a *Alternative) GetAdvances() []string { func (a *Alternative) GetComment() string { return a.Comment } + +func (pp PlayerPosition) Validate() error { + if len(pp.Position) != 1 && pp.Position[0] < '1' || pp.Position[0] > '9' { + return fmt.Errorf("player position should be 1-9") + } + return nil +} + +func (pp PlayerPosition) PositionNumber() int { + return int(pp.Position[0] - '0') +} diff --git a/pkg/gamefile/lexer.go b/pkg/gamefile/lexer.go index 9652076..7943092 100644 --- a/pkg/gamefile/lexer.go +++ b/pkg/gamefile/lexer.go @@ -26,8 +26,8 @@ var gameFileDef = lexer.MustStateful( "PA": { rule("Advance", `[Bb123][-Xx][123Hh]([^ \t\n\r]*)`, nil), rule("colon", `(:|--)[ \t]*`, lexer.Push("PAComment")), - rule("NL", `[\n\r]`, lexer.Pop()), rule("Token", `[^ \t\n\r]+`, nil), + rule("NL", `[\n\r]`, lexer.Pop()), rule("whitespace", `[ \t]+`, nil), rule("comment", `//.*[\n\r]`, nil), }, diff --git a/pkg/gamefile/parser_test.go b/pkg/gamefile/parser_test.go index 10c428f..22da203 100644 --- a/pkg/gamefile/parser_test.go +++ b/pkg/gamefile/parser_test.go @@ -34,13 +34,15 @@ func TestParser(t *testing.T) { assert.Equal("routine ground ball", event.Alternative.Comment) } event = events[8] - if assert.Len(event.Afters, 1) && assert.NotNil(event.Afters[0].Conference) { - assert.True(*event.Afters[0].Conference) + if assert.Len(event.Play.Afters, 1) && assert.NotNil(event.Play.Afters[0].Conference) { + assert.True(*event.Play.Afters[0].Conference) } event = events[3] - assert.Equal("9", *event.Afters[0].CourtesyRunner) - event = events[20] - assert.Equal("3", event.Sub.VSubEnter) - assert.Equal("2", event.Sub.VSubExit) + assert.Equal("9", *event.Play.Afters[0].CourtesyRunner) + event = events[19] + if assert.Len(event.Play.Afters, 1) { + assert.Equal("3", event.Play.Afters[0].Sub.Enter) + assert.Equal("2", event.Play.Afters[0].Sub.Exit) + } } } diff --git a/pkg/gamefile/testdata/test.gm b/pkg/gamefile/testdata/test.gm index 5a6e613..9188684 100644 --- a/pkg/gamefile/testdata/test.gm +++ b/pkg/gamefile/testdata/test.gm @@ -27,8 +27,7 @@ score 0 11 26 BFCS K 12 18 BBBB W B-1 13 11 X E7/F7 B-2 1-H -14 2 BSBFX 4/P4 -vsub 3 for 2 +14 2 BSBFX 4/P4 sub 3 for 2 score 1 pitcher 3 diff --git a/pkg/playbyplay/playbyplay.go b/pkg/playbyplay/playbyplay.go index 229f565..723c39b 100644 --- a/pkg/playbyplay/playbyplay.go +++ b/pkg/playbyplay/playbyplay.go @@ -53,7 +53,7 @@ func (gen *Generator) Generate(w io.Writer) error { line := &strings.Builder{} if batterPlay := batterPlayDescription(state); batterPlay != "" { batter := battingTeam.GetPlayer(state.Batter) - fmt.Fprintf(line, "%s %s %s", batter.NameOrNumber(), countDescription(state.Pitches), batterPlay) + fmt.Fprintf(line, "%s%s %s", batter.NameOrNumber(), countDescription(state.Pitches), batterPlay) } else if runnerPlay := runningPlayDescription(battingTeam, state, gen.lastState); runnerPlay != "" { fmt.Fprint(line, runnerPlay) } @@ -140,10 +140,13 @@ func (gen *Generator) Generate(w io.Writer) error { func countDescription(pitches game.Pitches) string { if pitches == "X" { - return "on the first pitch" + return " on the first pitch" } - count, _, _ := pitches.Count() - return fmt.Sprintf("with the count %s", count) + ok, count, _, _ := pitches.Count() + if ok { + return fmt.Sprintf(" with the count %s", count) + } + return "" } func positionName(fielder int) string { diff --git a/pkg/stats/alt_data.go b/pkg/stats/alt_data.go index 654acf9..28d39e0 100644 --- a/pkg/stats/alt_data.go +++ b/pkg/stats/alt_data.go @@ -62,7 +62,7 @@ func (alt *AltData) Record(gameID string, state *game.State) float64 { alt.play.AppendString(state.AlternativeFor.GetPlayAdvancesCode()) alt.alt.AppendString(state.GetPlayAdvancesCode()) price := originalChange - change - if state.Batter.IsUs() { + if state.BattingTeam.Us { price = -price } alt.cost.AppendFloat(price) diff --git a/pkg/stats/bat_situation.go b/pkg/stats/bat_situation.go index ec0c2cd..b1d5a88 100644 --- a/pkg/stats/bat_situation.go +++ b/pkg/stats/bat_situation.go @@ -2,6 +2,7 @@ package stats import ( "fmt" + "strings" "github.com/slshen/paperscore/pkg/dataframe" "github.com/slshen/paperscore/pkg/game" @@ -37,14 +38,14 @@ func NewBattingByCount() *BattingCountSituations { func (bc *BattingCountSituations) Read(g *game.Game) { states := g.GetStates() if bc.Us != "" { - if g.Home.IsUs(bc.Us) { + if strings.HasPrefix(g.Home.Name, bc.Us) { states = g.GetHomeStates() } else { states = g.GetVisitorStates() } } if bc.NotUs != "" { - if !g.Home.IsUs(bc.NotUs) { + if !strings.HasPrefix(g.Home.Name, bc.NotUs) { states = g.GetHomeStates() } else { states = g.GetVisitorStates() @@ -61,9 +62,11 @@ func (bc *BattingCountSituations) Read(g *game.Game) { func (bc *BattingCountSituations) recordDirect(state *game.State) { if state.Complete { - _, balls, strikes := state.Pitches[0 : len(state.Pitches)-1].Count() - sit := bc.sits[strikes*4+balls] - sit.Record(state) + known, _, balls, strikes := state.Pitches[0 : len(state.Pitches)-1].Count() + if known { + sit := bc.sits[strikes*4+balls] + sit.Record(state) + } } } @@ -71,7 +74,10 @@ func (bc *BattingCountSituations) recordPassingThrough(state *game.State) { if state.Complete { sits := map[string]*CountSituation{} for i := 0; i < len(state.Pitches); i++ { - count, balls, strikes := state.Pitches[0:i].Count() + known, count, balls, strikes := state.Pitches[0:i].Count() + if !known { + continue + } if balls < 4 && strikes < 3 && sits[count] == nil { sit := bc.sits[strikes*4+balls] sits[count] = sit diff --git a/pkg/stats/batting.go b/pkg/stats/batting.go index 60d7909..e960641 100644 --- a/pkg/stats/batting.go +++ b/pkg/stats/batting.go @@ -102,7 +102,7 @@ func (b *Batting) Record(state *game.State) (teamLOB int) { } if state.IsStrikeOut() { b.StrikeOuts++ - if state.Pitches.Last() == "C" { + if state.Pitches.Last() == 'C' { b.StrikeOutsLooking++ } } @@ -142,22 +142,29 @@ func (b *Batting) Record(state *game.State) (teamLOB int) { } } lastPitch := state.Pitches.Last() - if state.Play.Type == game.StrikeOut && (lastPitch == "L" || lastPitch == "M") { + if state.Play.Type == game.StrikeOut && (lastPitch == 'L' || lastPitch == 'M') { b.BuntOuts++ } - if lastPitch == "X" { + if lastPitch == 'X' { b.Strikes++ b.Swings++ } } if state.Complete || state.Incomplete { - b.PitchesSeen += len(state.Pitches) - b.Strikes += state.Pitches.CountUp('C', 'S', 'F', 'M', 'L', 'T') - b.Swings += state.Pitches.CountUp('S', 'F', 'M', 'T') - b.Misses += state.Pitches.CountUp('S', 'M', 'T') - b.CalledStrikes += state.Pitches.CountUp('C') - b.MissedBunts += state.Pitches.CountUp('M') - b.FoulBunts += state.Pitches.CountUp('L') + known, _, balls, strikes := state.Pitches.Count() + if known { + b.PitchesSeen += balls + strikes + b.Strikes += strikes + b.Swings += state.Pitches.CountUp('S', 'F', 'M', 'T') + b.Misses += state.Pitches.CountUp('S', 'M', 'T') + b.CalledStrikes += state.Pitches.CountUp('C') + b.MissedBunts += state.Pitches.CountUp('M') + b.FoulBunts += state.Pitches.CountUp('L') + if state.Pitches.Last() == 'X' { + b.PitchesSeen++ + b.Swings++ + } + } } return } diff --git a/pkg/stats/pitching.go b/pkg/stats/pitching.go index c6a47b0..247b0a0 100644 --- a/pkg/stats/pitching.go +++ b/pkg/stats/pitching.go @@ -51,17 +51,21 @@ func (p *Pitching) Record(state *game.State) { p.StolenBases++ } if state.Complete || state.Outs == 3 { - p.Pitches += len(state.Pitches) - p.Strikes += state.Pitches.CountUp('S', 'C', 'F', 'L', 'M') - p.Balls += state.Pitches.CountUp('B') - p.Swings += state.Pitches.CountUp('S', 'F', 'M') - p.Misses += state.Pitches.CountUp('S', 'M') - if state.Pitches.Last() == "X" { - if state.Play.Type == game.HitByPitch { + known, _, balls, strikes := state.Pitches.Count() + if known { + p.Pitches += balls + strikes + p.Strikes += strikes + p.Balls += balls + p.Swings += state.Pitches.CountUp('S', 'F', 'M') + p.Misses += state.Pitches.CountUp('S', 'M') + last := state.Pitches.Last() + if last == 'H' { p.Balls++ - } else { + } + if last == 'X' { p.Strikes++ p.Swings++ + p.Pitches++ } } if state.Play.IsHit() { @@ -94,7 +98,7 @@ func (p *Pitching) Record(state *game.State) { } if state.IsStrikeOut() { p.StrikeOuts++ - if state.Pitches.Last() == "C" { + if state.Pitches.Last() == 'C' { p.StrikeOutsLooking++ } } diff --git a/pkg/text/wrap.go b/pkg/text/wrap.go index bc223c1..a277ed0 100644 --- a/pkg/text/wrap.go +++ b/pkg/text/wrap.go @@ -6,6 +6,7 @@ import ( ) func Wrap(s string, width int) string { + //nolint:gosec return wordwrap.WrapString(s, uint(width)) } diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 0a51893..98d2d40 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -73,7 +73,7 @@ func New() *UI { 0, 1, false). AddItem(ui.messages, 6, 0, false). AddItem(tview.NewFlex(). - AddItem(tview.NewTextView().SetText("Quit:^Q Save:^S Choose:^L Swap:^R Box:^Z"), 0, 4, false). + AddItem(tview.NewTextView().SetText("Quit:^C Save:^S Choose:^L Swap:^R Box:^Z"), 0, 4, false). AddItem(ui.status, 0, 1, false), 1, 0, false) ui.root.AddAndSwitchToPage("main", flex, true) @@ -296,6 +296,7 @@ func (ui *UI) inputHandler(event *tcell.EventKey) *tcell.EventKey { case tcell.KeyCtrlQ: ui.app.Stop() case tcell.KeyCtrlC: + ui.app.Stop() return nil case tcell.KeyCtrlS: if ui.dialog == nil { @@ -477,7 +478,7 @@ func (ui *UI) showBox() { _ = box.Render(&s) flex := tview.NewFlex().SetDirection(tview.FlexRow) flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyCtrlQ { + if event.Key() == tcell.KeyCtrlC { ui.closeDialog() return nil } @@ -486,7 +487,7 @@ func (ui *UI) showBox() { view := tview.NewTextView().SetText(s.String()) view.SetBorder(true) flex.AddItem(view, 0, 1, true). - AddItem(tview.NewTextView().SetText("Close:^Q"), 1, 0, false) + AddItem(tview.NewTextView().SetText("Close:^C"), 1, 0, false) ui.showDialog(flex) }