From d99949f615411f2f78fa2bd0ac8ea298d1ceb2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=83=BC=E3=82=8B?= Date: Wed, 4 Nov 2026 22:31:19 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=9E=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=81=AE=E4=B8=8B=E3=81=AB=E3=80=81=E7=9B=B4?= =?UTF-8?q?=E5=89=8D=E3=81=AE=E3=82=BD=E3=83=AB=E3=83=96=E3=81=AE=E6=89=8B?= =?UTF-8?q?=E6=95=B0=E3=81=A8=20TPS=20=E3=82=92=E5=87=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ソルブ後はすぐ「スクランブル中」の表示に変わり、手数が見えなかったので、手数と TPS を専用の行に分けた。 計測中は同じ行に今の手数を出す。TPS は +2 を含めない実測の時間で割る(ソルブ詳細と同じ formatTps)。 Co-Authored-By: Claude Opus 5.5 --- web/src/components/SplitsTable.tsx | 8 ++------ web/src/components/TimerDisplay.tsx | 24 +++++++++++++----------- web/src/index.css | 9 +++++++++ web/src/pages/SolveDetailPage.tsx | 5 ++--- web/src/solve/solve.test.ts | 7 ++++++- web/src/solve/stats.ts | 5 +++++ 6 files changed, 37 insertions(+), 21 deletions(-) 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 (
{time}
+
{stats}
{hint}
) 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