Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions web/src/components/SplitsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -77,7 +73,7 @@ export function SplitsTable({
<td>{formatTime(ms, 3)}</td>
<td>{totalMs > 0 ? `${Math.round((ms / totalMs) * 100)}%` : '—'}</td>
<td>{s.move_count}</td>
<td>{tps(s.move_count, ms)}</td>
<td>{formatTps(s.move_count, ms)}</td>
{holds && <td>{rots.length ? `${rots.length}(${(rotMs / 1000).toFixed(2)})` : '—'}</td>}
<td className="alg">{tokens.length ? <NotationLine tokens={tokens} /> : <span className="muted">(skip)</span>}</td>
</tr>
Expand Down
24 changes: 13 additions & 11 deletions web/src/components/TimerDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand All @@ -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)
Expand All @@ -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 (
<section className={`timer timer--${phase} ${mod}`}>
<div className="timer-time">{time}</div>
<div className="timer-stats">{stats}</div>
<div className="timer-hint">{hint}</div>
</section>
)
Expand Down
9 changes: 9 additions & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions web/src/pages/SolveDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/** ソルブ詳細:段階タイム、スクランブル、解法、ペナルティ・メモ編集。 */
Expand Down Expand Up @@ -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 (
<div className="detail-page">
Expand All @@ -87,7 +86,7 @@ function SolveDetail({ solve, onChange }: { solve: Solve; onChange: (s: Solve) =
<div className="muted">
#{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
</div>
<div className="row">
<button className={solve.penalty === '+2' ? 'on' : ''} onClick={() => setPenalty('+2')}>
Expand Down
7 changes: 6 additions & 1 deletion web/src/solve/solve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { applyRotation } from './holds'
import { invert, simplify } from './notation'
import { ScrambleTracker } from './scramble-tracker'
import { SolveSession, inspectionPenalty, isSolved, type Exercise } from './session'
import { averageOf, effectiveTime, formatTime, rollingAverages } from './stats'
import { averageOf, effectiveTime, formatTime, formatTps, rollingAverages } from './stats'

const kpuzzle = await cube3x3x3.kpuzzle()
const solved = kpuzzle.defaultPattern()
Expand Down Expand Up @@ -95,6 +95,11 @@ describe('stats', () => {
expect(formatTime(null)).toBe('DNF')
expect(formatTime(undefined)).toBe('--')
})

it('formatTps', () => {
expect(formatTps(52, 12620)).toBe('4.12')
expect(formatTps(0, 0)).toBe('—')
})
})

describe('inspectionPenalty', () => {
Expand Down
5 changes: 5 additions & 0 deletions web/src/solve/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export function best(values: (number | null | undefined)[]): number | undefined
}

/** 表示用の文字列。12.345 / 1:02.345 / DNF / -- */
/** 1秒あたりの手数(TPS)。時間が 0 なら「—」。時間はペナルティを含めない実測の時間を渡す。 */
export function formatTps(moves: number, ms: number): string {
return ms > 0 ? (moves / (ms / 1000)).toFixed(2) : '—'
}

export function formatTime(ms: number | null | undefined, digits: 2 | 3 = 2): string {
if (ms === undefined) return '--'
if (ms === null) return 'DNF'
Expand Down
Loading