A Nix-friendly SQLite-enhanced fork of Flitter, a speedrunning split timer for Unix-style terminals
Révision | 8ef92b30e9d6b6f99d878d198a107b8d76885cc8 (tree) |
---|---|
l'heure | 2022-12-20 08:55:14 |
Auteur | Corbin <cds@corb...> |
Commiter | Corbin |
Add command for summarizing splits.
So that I don't have to run the entire timer just to look at a single
split file.
@@ -1,9 +1,6 @@ | ||
1 | 1 | (executables |
2 | - (names main) | |
2 | + (names main summary_main) | |
3 | + (public_names flitter flitter-summary) | |
3 | 4 | (libraries flitter)) |
4 | 5 | |
5 | -(install | |
6 | - (section bin) | |
7 | - (files (main.exe as flitter))) | |
8 | - | |
9 | 6 | ; vim:ft=scheme |
@@ -0,0 +1,10 @@ | ||
1 | +let usage = | |
2 | + "Usage:\n" ^ "flitter-summary <splits_path>\n" | |
3 | + ^ "Summarize the splits file pointed to by `splits_path`.\n" | |
4 | + | |
5 | +let () = | |
6 | + match Sys.argv with | |
7 | + | [| _; path |] -> | |
8 | + let summary = Loadsave.load_summary path in | |
9 | + Summary.print_summary summary | |
10 | + | _ -> print_string usage |
@@ -7,6 +7,6 @@ in pkgs.stdenv.mkDerivation { | ||
7 | 7 | # working with S-expressions |
8 | 8 | ocamlPackages.sexp |
9 | 9 | # maintaining OCaml code |
10 | - ocamlformat | |
10 | + ocamlformat ocamlPackages.utop | |
11 | 11 | ]; |
12 | 12 | } |
@@ -2,4 +2,4 @@ type t | ||
2 | 2 | |
3 | 3 | val make : unit -> t |
4 | 4 | val draw : t -> Timer_types.timer -> unit |
5 | -val close : t -> unit | |
\ No newline at end of file | ||
5 | +val close : t -> unit |
@@ -4,11 +4,9 @@ type t = int (* Milliseconds *) | ||
4 | 4 | |
5 | 5 | val t_of_sexp : Sexp.t -> t |
6 | 6 | val sexp_of_t : t -> Sexp.t |
7 | - | |
8 | 7 | val of_string : string -> t option |
9 | 8 | val to_string : t -> int -> string (* int is # of decimal places *) |
10 | 9 | val to_string_plus : t -> int -> string |
11 | 10 | val string_valid : string -> bool |
12 | - | |
13 | 11 | val between : float -> float -> t |
14 | 12 | val since : float -> t |
@@ -1,4 +1,4 @@ | ||
1 | 1 | type t |
2 | 2 | |
3 | 3 | val make : Timer_types.timer -> t Lwt.t |
4 | -val loop : t -> unit Lwt.t | |
\ No newline at end of file | ||
4 | +val loop : t -> unit Lwt.t |
@@ -1,4 +1,4 @@ | ||
1 | 1 | type keypress = float * string |
2 | 2 | type t = keypress Lwt_stream.t |
3 | 3 | |
4 | -val make_stream : unit -> t Lwt.t | |
\ No newline at end of file | ||
4 | +val make_stream : unit -> t Lwt.t |
@@ -12,6 +12,12 @@ type split = { | ||
12 | 12 | type gold = { title : string; duration : Duration.t } [@@deriving sexp] |
13 | 13 | type archived_run = { attempt : int; splits : split array } [@@deriving sexp] |
14 | 14 | |
15 | +let time_of_split segment { splits } = | |
16 | + Array.find_map splits ~f:(fun { title; time } -> | |
17 | + if String.equal segment title then Some time else None) | |
18 | + | |
19 | +let times_for_segment segment = List.filter_map ~f:(time_of_split segment) | |
20 | + | |
15 | 21 | type game = { |
16 | 22 | title : string; |
17 | 23 | category : string; |
@@ -55,6 +61,18 @@ let game_of_sexp sexp = | ||
55 | 61 | "Not all history runs have same number of splits as split_names" sexp |
56 | 62 | else game |
57 | 63 | |
64 | +let relativize run = | |
65 | + let rel = | |
66 | + Array.folding_map run.splits ~init:0 ~f:(fun offset sp -> | |
67 | + (sp.time, { sp with time = sp.time - offset })) | |
68 | + in | |
69 | + { run with splits = rel } | |
70 | + | |
71 | +let times_for_game_history { split_names; history } = | |
72 | + let relative_history = List.map ~f:relativize history in | |
73 | + Array.map split_names ~f:(fun title -> | |
74 | + (title, times_for_segment title relative_history)) | |
75 | + | |
58 | 76 | let load_golds parsed_game = |
59 | 77 | if Array.length parsed_game.golds = 0 then |
60 | 78 | Array.map parsed_game.split_names ~f:(fun name -> |
@@ -99,6 +117,20 @@ let load filepath = | ||
99 | 117 | splits_file = filepath; |
100 | 118 | } |
101 | 119 | |
120 | +let load_summary filepath = | |
121 | + let game = Sexp.load_sexp_conv_exn filepath game_of_sexp in | |
122 | + (* let pb = load_run_opt game.personal_best in | |
123 | + let golds = load_golds game in *) | |
124 | + let times = times_for_game_history game in | |
125 | + | |
126 | + { | |
127 | + Summary.title = game.title; | |
128 | + category = game.category; | |
129 | + attempts = game.attempts; | |
130 | + completed = game.completed; | |
131 | + splits = times; | |
132 | + } | |
133 | + | |
102 | 134 | let export_run (run : Timer_types.archived_run) = |
103 | 135 | { |
104 | 136 | attempt = run.attempt; |
@@ -1,2 +1,6 @@ | ||
1 | +(* Load and prepare to run. *) | |
1 | 2 | val load : string -> Timer_types.timer |
2 | -val save : Timer_types.timer -> unit | |
\ No newline at end of file | ||
3 | +val save : Timer_types.timer -> unit | |
4 | + | |
5 | +(* Load and summarize. *) | |
6 | +val load_summary : string -> Summary.t |
@@ -0,0 +1,52 @@ | ||
1 | +open Core | |
2 | + | |
3 | +type welford = { n : int; m1 : float; m2 : float } | |
4 | + | |
5 | +let do_welford = | |
6 | + List.fold | |
7 | + ~f:(fun { n; m1; m2 } d -> | |
8 | + let delta = Float.of_int d -. m1 in | |
9 | + let dn = delta /. Float.of_int (n + 1) in | |
10 | + let t = delta *. dn *. Float.of_int n in | |
11 | + { n = n + 1; m1 = m1 +. dn; m2 = m2 +. t }) | |
12 | + ~init:{ n = 0; m1 = 0.0; m2 = 0.0 } | |
13 | + | |
14 | +let mean { m1; _ } = m1 | |
15 | + | |
16 | +let variance { n; m2; _ } = | |
17 | + if n < 2 then None else Some (m2 /. Float.of_int (n - 1)) | |
18 | + | |
19 | +let stddev s = Option.value_map (variance s) ~default:0.0 ~f:Float.sqrt | |
20 | +let best = List.fold ~f:Int.min ~init:Int.max_value | |
21 | + | |
22 | +let print_summary_segment width segment times = | |
23 | + let w = do_welford times in | |
24 | + (* NB: 12 digits is a reasonable width for durations; it would take over a | |
25 | + week for a segment to surpass it! *) | |
26 | + Printf.printf "%*s: %12s %12s %12s (best/mean/stddev)\n" (width + 2) segment | |
27 | + (Duration.to_string (best times) 3) | |
28 | + (Duration.to_string (Float.to_int (mean w)) 3) | |
29 | + (Duration.to_string (Float.to_int (stddev w)) 3) | |
30 | + | |
31 | +type t = { | |
32 | + title : string; | |
33 | + category : string; | |
34 | + attempts : int; | |
35 | + completed : int; | |
36 | + splits : (string * Duration.t list) array; | |
37 | +} | |
38 | + | |
39 | +let print_completion completed attempts = | |
40 | + let percentage = completed * 100 / attempts in | |
41 | + Printf.printf "Completion rate: %d/%d (%d%%)\n" completed attempts percentage | |
42 | + | |
43 | +let print_summary (summary : t) = | |
44 | + let titlewidth = | |
45 | + Array.fold summary.splits ~init:0 ~f:(fun x (title, _) -> | |
46 | + Int.max x (String.length title)) | |
47 | + in | |
48 | + print_endline ("Title: " ^ summary.title); | |
49 | + print_endline ("Category: " ^ summary.category); | |
50 | + print_completion summary.completed summary.attempts; | |
51 | + Array.iter summary.splits ~f:(fun (title, times) -> | |
52 | + print_summary_segment titlewidth title times) |