Scouting: Add function to convert actions to stats

Signed-off-by: Emily Markova <emily.markova@gmail.com>
Change-Id: I92b418f528a06b1b4d267655d384805fe465c44c
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 4c4870c..935d721 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -60,6 +60,7 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_go_fbs",
         "//scouting/webserver/requests/messages:request_shift_schedule_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_actions_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_driver_ranking_go_fbs",
diff --git a/scouting/webserver/requests/messages/submit_actions.fbs b/scouting/webserver/requests/messages/submit_actions.fbs
index dfb980f..ebbaa5c 100644
--- a/scouting/webserver/requests/messages/submit_actions.fbs
+++ b/scouting/webserver/requests/messages/submit_actions.fbs
@@ -55,7 +55,10 @@
 }
 
 table SubmitActions {
-    actions_list:[Action] (id:0);
-}
-
-root_type SubmitActions;
\ No newline at end of file
+    team_number:string (id: 0);
+    match_number:int (id: 1);
+    set_number:int (id: 2);
+    comp_level:string (id: 3);
+    actions_list:[Action] (id:4);
+    collected_by:string (id: 5);
+}
\ No newline at end of file
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 467542a..062398a 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -454,6 +454,104 @@
 	w.Write(builder.FinishedBytes())
 }
 
+func ConvertActionsToStat(submitActions *submit_actions.SubmitActions) (db.Stats2023, error) {
+	overall_time := int64(0)
+	cycles := int64(0)
+	picked_up := false
+	lastPlacedTime := int64(0)
+	stat := db.Stats2023{TeamNumber: string(submitActions.TeamNumber()), MatchNumber: submitActions.MatchNumber(), SetNumber: submitActions.SetNumber(), CompLevel: string(submitActions.CompLevel()),
+		StartingQuadrant: 0, LowCubesAuto: 0, MiddleCubesAuto: 0, HighCubesAuto: 0, CubesDroppedAuto: 0,
+		LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0, ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 0, HighCubes: 0,
+		CubesDropped: 0, LowCones: 0, MiddleCones: 0, HighCones: 0, ConesDropped: 0, AvgCycle: 0, CollectedBy: string(submitActions.CollectedBy()),
+	}
+	// Loop over all actions.
+	for i := 0; i < submitActions.ActionsListLength(); i++ {
+		var action submit_actions.Action
+		if !submitActions.ActionsList(&action, i) {
+			return db.Stats2023{}, errors.New(fmt.Sprintf("Failed to parse submit_actions.Action"))
+		}
+		actionTable := new(flatbuffers.Table)
+		action_type := action.ActionTakenType()
+		if !action.ActionTaken(actionTable) {
+			return db.Stats2023{}, errors.New(fmt.Sprint("Failed to parse sub-action or sub-action was missing"))
+		}
+		if action_type == submit_actions.ActionTypeStartMatchAction {
+			var startMatchAction submit_actions.StartMatchAction
+			startMatchAction.Init(actionTable.Bytes, actionTable.Pos)
+			stat.StartingQuadrant = startMatchAction.Position()
+		} else if action_type == submit_actions.ActionTypePickupObjectAction {
+			var pick_up_action submit_actions.PickupObjectAction
+			pick_up_action.Init(actionTable.Bytes, actionTable.Pos)
+			if picked_up == true {
+				object := pick_up_action.ObjectType().String()
+				auto := pick_up_action.Auto()
+				if object == "kCube" && auto == false {
+					stat.CubesDropped += 1
+				} else if object == "kCube" && auto == true {
+					stat.CubesDroppedAuto += 1
+				} else if object == "kCone" && auto == false {
+					stat.ConesDropped += 1
+				} else if object == "kCube" && auto == true {
+					stat.ConesDroppedAuto += 1
+				}
+			} else {
+				picked_up = true
+			}
+		} else if action_type == submit_actions.ActionTypePlaceObjectAction {
+			var place_action submit_actions.PlaceObjectAction
+			place_action.Init(actionTable.Bytes, actionTable.Pos)
+			if !picked_up {
+				return db.Stats2023{}, errors.New(fmt.Sprintf("Got PlaceObjectAction without corresponding PickupObjectAction"))
+			}
+			object := place_action.ObjectType()
+			level := place_action.ScoreLevel()
+			auto := place_action.Auto()
+			if object == 0 && level == 0 && auto == true {
+				stat.LowCubesAuto += 1
+			} else if object == 0 && level == 0 && auto == false {
+				stat.LowCubes += 1
+			} else if object == 0 && level == 1 && auto == true {
+				stat.MiddleCubesAuto += 1
+			} else if object == 0 && level == 1 && auto == false {
+				stat.MiddleCubes += 1
+			} else if object == 0 && level == 2 && auto == true {
+				stat.HighCubesAuto += 1
+			} else if object == 0 && level == 2 && auto == false {
+				stat.HighCubes += 1
+			} else if object == 1 && level == 0 && auto == true {
+				stat.LowConesAuto += 1
+			} else if object == 1 && level == 0 && auto == false {
+				stat.LowCones += 1
+			} else if object == 1 && level == 1 && auto == true {
+				stat.MiddleConesAuto += 1
+			} else if object == 1 && level == 1 && auto == false {
+				stat.MiddleCones += 1
+			} else if object == 1 && level == 2 && auto == true {
+				stat.HighConesAuto += 1
+			} else if object == 1 && level == 2 && auto == false {
+				stat.HighCones += 1
+			} else {
+				return db.Stats2023{}, errors.New(fmt.Sprintf("Got unknown ObjectType/ScoreLevel/Auto combination"))
+			}
+			picked_up = false
+			if lastPlacedTime != int64(0) {
+				// If this is not the first time we place,
+				// start counting cycle time. We define cycle
+				// time as the time between placements.
+				overall_time += int64(action.Timestamp()) - lastPlacedTime
+				cycles += 1
+			}
+			lastPlacedTime = int64(action.Timestamp())
+		}
+	}
+	if cycles != 0 {
+		stat.AvgCycle = int32(overall_time / cycles)
+	} else {
+		stat.AvgCycle = 0
+	}
+	return stat, nil
+}
+
 // Handles a Request2023DataScouting request.
 type request2023DataScoutingHandler struct {
 	db Database
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index efd770b..26b79c5 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -23,6 +23,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
@@ -391,6 +392,103 @@
 	}
 }
 
+// Validates that we can request the 2023 stats.
+func TestConvertActionsToStat(t *testing.T) {
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_actions.SubmitActionsT{
+		TeamNumber:  "4244",
+		MatchNumber: 3,
+		SetNumber:   1,
+		CompLevel:   "quals",
+		CollectedBy: "katie",
+		ActionsList: []*submit_actions.ActionT{
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypeStartMatchAction,
+					Value: &submit_actions.StartMatchActionT{
+						Position: 1,
+					},
+				},
+				Timestamp: 0,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						Auto:       true,
+					},
+				},
+				Timestamp: 400,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						Auto:       true,
+					},
+				},
+				Timestamp: 800,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePlaceObjectAction,
+					Value: &submit_actions.PlaceObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCube,
+						ScoreLevel: submit_actions.ScoreLevelkLow,
+						Auto:       true,
+					},
+				},
+				Timestamp: 2000,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePickupObjectAction,
+					Value: &submit_actions.PickupObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCone,
+						Auto:       false,
+					},
+				},
+				Timestamp: 2800,
+			},
+			{
+				ActionTaken: &submit_actions.ActionTypeT{
+					Type: submit_actions.ActionTypePlaceObjectAction,
+					Value: &submit_actions.PlaceObjectActionT{
+						ObjectType: submit_actions.ObjectTypekCone,
+						ScoreLevel: submit_actions.ScoreLevelkHigh,
+						Auto:       false,
+					},
+				},
+				Timestamp: 3100,
+			},
+		},
+	}).Pack(builder))
+
+	submitActions := submit_actions.GetRootAsSubmitActions(builder.FinishedBytes(), 0)
+	response, err := ConvertActionsToStat(submitActions)
+
+	if err != nil {
+		t.Fatal("Failed to convert actions to stats: ", err)
+	}
+
+	expected := db.Stats2023{
+		TeamNumber: "4244", MatchNumber: 3, SetNumber: 1,
+		CompLevel: "quals", StartingQuadrant: 1, LowCubesAuto: 1,
+		MiddleCubesAuto: 0, HighCubesAuto: 0, CubesDroppedAuto: 1,
+		LowConesAuto: 0, MiddleConesAuto: 0, HighConesAuto: 0,
+		ConesDroppedAuto: 0, LowCubes: 0, MiddleCubes: 0,
+		HighCubes: 0, CubesDropped: 0, LowCones: 0,
+		MiddleCones: 0, HighCones: 1, ConesDropped: 0,
+		AvgCycle: 1100, CollectedBy: "katie",
+	}
+
+	if expected != response {
+		t.Fatal("Expected ", expected, ", but got ", response)
+	}
+}
+
 func TestSubmitNotes(t *testing.T) {
 	database := MockDatabase{}
 	scoutingServer := server.NewScoutingServer()