diff --git a/web/src/components/SplitsTable.tsx b/web/src/components/SplitsTable.tsx
index 36309de..22c66fc 100644
--- a/web/src/components/SplitsTable.tsx
+++ b/web/src/components/SplitsTable.tsx
@@ -2,11 +2,7 @@ import type { SolveMove } from '../api'
import { useHoldFrame } from '../settings/hooks'
import { STAGE_LABELS, type Split } from '../solve/cfop'
import { isRegrip, notationTokens, type HoldChange, type Holds, type NotationToken } from '../solve/holds'
-import { formatTime } from '../solve/stats'
-
-function tps(moves: number, ms: number): string {
- return ms > 0 ? (moves / (ms / 1000)).toFixed(2) : '—'
-}
+import { formatTime, formatTps } from '../solve/stats'
/** 持ち替えの所要時間(秒)と、ワイドムーブかもしれないことの説明。 */
function rotationTitle(c: HoldChange): string {
@@ -77,7 +73,7 @@ export function SplitsTable({
{formatTime(ms, 3)} |
{totalMs > 0 ? `${Math.round((ms / totalMs) * 100)}%` : '—'} |
{s.move_count} |
- {tps(s.move_count, ms)} |
+ {formatTps(s.move_count, ms)} |
{holds && {rots.length ? `${rots.length}(${(rotMs / 1000).toFixed(2)})` : '—'} | }
{tokens.length ? : (skip)} |
diff --git a/web/src/components/TimerDisplay.tsx b/web/src/components/TimerDisplay.tsx
index d49f157..3c85338 100644
--- a/web/src/components/TimerDisplay.tsx
+++ b/web/src/components/TimerDisplay.tsx
@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
import { INSPECTION_DNF_MS, INSPECTION_MS, type SessionState } from '../solve/session'
-import { effectiveTime, formatTime } from '../solve/stats'
+import { effectiveTime, formatTime, formatTps } from '../solve/stats'
/**
- * 大きなタイマー表示。計測中は経過時間、それ以外は直前の記録。
+ * 大きなタイマー表示。計測中は経過時間、それ以外は直前の記録と、その手数・TPS。
* lastMoveCount は直前の記録の手数(回転ログを持たない中継の画面で渡す。省くと回転ログの長さ)。
*/
export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; lastMoveCount?: number }) {
@@ -25,6 +25,8 @@ export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; la
let time: string
let hint: string
let mod = ''
+ // 手数(直前の記録なら TPS も)。インスペクション中は空にする(行は残して高さを変えない)
+ let stats = ''
if (inspecting) {
// インスペクション:残り秒数。15秒を過ぎたら +2、17秒を過ぎたら DNF
const elapsed = Math.max(0, now - readyAt)
@@ -41,21 +43,21 @@ export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; la
hint = 'インスペクション中:最初の回転でスタート'
} else if (phase === 'running' && startedAt != null) {
time = formatTime(Math.max(0, now - startedAt), 2)
- hint = `${moveCount}手${state.inspectionPenalty ? `(インスペクション超過:${state.inspectionPenalty})` : ''}`
+ stats = `${moveCount}手`
+ hint = state.inspectionPenalty ? `インスペクション超過:${state.inspectionPenalty}` : ''
} else {
time = last ? formatTime(effectiveTime(last.total_ms, last.penalty), 3) : '0.000'
- hint =
- phase === 'ready'
- ? '準備OK:最初の回転でスタート'
- : phase === 'scrambling'
- ? 'スクランブル中'
- : last
- ? `${lastMoveCount ?? last.moves?.length ?? 0}手`
- : ''
+ hint = phase === 'ready' ? '準備OK:最初の回転でスタート' : phase === 'scrambling' ? 'スクランブル中' : ''
+ if (last) {
+ const moves = lastMoveCount ?? last.moves?.length ?? 0
+ // TPS は +2 を含めない実測の時間で割る
+ stats = `${moves}手 ・ ${formatTps(moves, last.total_ms)} TPS`
+ }
}
return (
)
diff --git a/web/src/index.css b/web/src/index.css
index 407e1e7..3fc9ddd 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -705,6 +705,15 @@ code {
color: var(--mute);
}
+.timer-stats {
+ min-height: 1.6em;
+ margin-top: 0.3em;
+ font-family: var(--font-num);
+ font-size: 1.25em;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
.timer-hint {
min-height: 1.6em;
color: var(--mute);
diff --git a/web/src/pages/SolveDetailPage.tsx b/web/src/pages/SolveDetailPage.tsx
index 5982200..67f0d6a 100644
--- a/web/src/pages/SolveDetailPage.tsx
+++ b/web/src/pages/SolveDetailPage.tsx
@@ -21,7 +21,7 @@ import { ReplayPlayer } from '../replay/ReplayPlayer'
import { isRegrip, notationTokens } from '../solve/holds'
import { useHoldFrame, useSettings } from '../settings/hooks'
import { computeSplits, COLOR_LABELS } from '../solve/cfop'
-import { effectiveTime, formatTime, type Penalty } from '../solve/stats'
+import { effectiveTime, formatTime, formatTps, type Penalty } from '../solve/stats'
import { formatDate, isFullSolve, MODE_LABELS, multiTitle } from '../format'
/** ソルブ詳細:段階タイム、スクランブル、解法、ペナルティ・メモ編集。 */
@@ -72,7 +72,6 @@ function SolveDetail({ solve, onChange }: { solve: Solve; onChange: (s: Solve) =
}
const moves = solve.moves ?? []
- const tps = solve.total_ms > 0 ? (moves.length / (solve.total_ms / 1000)).toFixed(2) : '—'
return (
@@ -87,7 +86,7 @@ function SolveDetail({ solve, onChange }: { solve: Solve; onChange: (s: Solve) =
#{solve.id} ・ {formatDate(solve.solved_at)} ・ {MODE_LABELS[solve.mode]}
{solve.case_id && ` ${solve.case_id}`}
- {solve.step && ` ${solve.step}`} ・ {moves.length}手 ・ {tps} TPS
+ {solve.step && ` ${solve.step}`} ・ {moves.length}手 ・ {formatTps(moves.length, solve.total_ms)} TPS