Skip to content

Commit

Permalink
Merge branch 'main' of github.com:RopeScore/rulesets into apk-ss-inte…
Browse files Browse the repository at this point in the history
…gration-2
  • Loading branch information
andrejpk committed Sep 25, 2024
2 parents 2d8af6a + 2ad69b7 commit d2df5d5
Show file tree
Hide file tree
Showing 18 changed files with 389 additions and 191 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ jobs:

publish-gpr:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
attestations: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
59 changes: 50 additions & 9 deletions lib/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { calculateTally, clampNumber, formatFactor, isObject, parseCompetitionEventDefinition, roundTo, roundToCurry, roundToMultiple } from './helpers.js'
import { simpleCalculateTallyFactory, clampNumber, filterMarkStream, formatFactor, isObject, parseCompetitionEventDefinition, roundTo, roundToCurry, roundToMultiple } from './helpers.js'
import assert from 'node:assert'
import test from 'node:test'
import type { JudgeMeta, Mark } from './models/types.js'
import type { GenericMark, JudgeMeta, Mark } from './models/types.js'

void test('helpers', async t => {
await t.test('isObject', async t => {
Expand Down Expand Up @@ -70,18 +70,59 @@ void test('helpers', async t => {
assert.strictEqual(formatFactor(1.1), '+10 %')
})

await t.test('calculateTally', async t => {
await t.test('filterMarkStream', async t => {
const tests: Array<[name: string, input: Array<Mark<string>>, output: Array<GenericMark<string>>]> = [
[
'No special marks',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }],
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }],
],
[
'Start at clear mark',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'clear' }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }],
[{ sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }],
],
[
'Handle single undo',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'undo', target: 2 }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }],
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 4, timestamp: 4, schema: 'b' }, { sequence: 5, timestamp: 5, schema: 'b' }],
],
[
'Handle two undos right after each other',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'undo', target: 2 }, { sequence: 5, timestamp: 5, schema: 'undo', target: 3 }, { sequence: 6, timestamp: 6, schema: 'b' }],
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 6, timestamp: 6, schema: 'b' }],
],
[
'Cannot undo an undo - silent ignore',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'undo', target: 2 }, { sequence: 5, timestamp: 5, schema: 'undo', target: 4 }, { sequence: 6, timestamp: 6, schema: 'b' }],
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 6, timestamp: 6, schema: 'b' }],
],
[
'Cannot undo a clear mark - silent ignore',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'clear' }, { sequence: 5, timestamp: 5, schema: 'undo', target: 4 }, { sequence: 6, timestamp: 6, schema: 'b' }],
[{ sequence: 6, timestamp: 6, schema: 'b' }],
],
[
'Cannot undo before a clear mark - silent ignore',
[{ sequence: 1, timestamp: 1, schema: 'a' }, { sequence: 2, timestamp: 2, schema: 'a' }, { sequence: 3, timestamp: 3, schema: 'b' }, { sequence: 4, timestamp: 4, schema: 'clear' }, { sequence: 5, timestamp: 5, schema: 'undo', target: 2 }, { sequence: 6, timestamp: 6, schema: 'b' }],
[{ sequence: 6, timestamp: 6, schema: 'b' }],
],
]
for (const [title, input, output] of tests) {
await t.test(title, () => {
assert.deepStrictEqual(filterMarkStream(input), output)
})
}
})

await t.test('simpleCalculateTallyFactory', async t => {
const meta: JudgeMeta = {
judgeId: '1',
judgeTypeId: 'S',
entryId: '1',
participantId: '1',
competitionEvent: 'e.ijru.sp.sr.srss.1.30@1.0.0',
}
await t.test('Should return tally for a TallyScoresheet', () => {
const tally = { entPlus: 2, entMinus: 1 }
assert.deepStrictEqual(calculateTally({ meta, tally }), tally)
})

await t.test('Should return tally for MarkScoresheet', () => {
const marks: Array<Mark<string>> = [
Expand All @@ -93,7 +134,7 @@ void test('helpers', async t => {
formPlus: 2,
formCheck: 1,
}
assert.deepStrictEqual(calculateTally({ meta, marks }), tally)
assert.deepStrictEqual(simpleCalculateTallyFactory(meta.judgeTypeId)({ meta, marks }), { meta, tally })
})

await t.test('Should return tally for MarkScoresheet with undo marks', () => {
Expand All @@ -107,7 +148,7 @@ void test('helpers', async t => {
formPlus: 1,
formCheck: 1,
}
assert.deepStrictEqual(calculateTally({ meta, marks }), tally)
assert.deepStrictEqual(simpleCalculateTallyFactory(meta.judgeTypeId)({ meta, marks }), { meta, tally })
})
})

Expand Down
84 changes: 54 additions & 30 deletions lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RSRWrongJudgeTypeError } from './errors.js'
import type { GenericMark, Mark } from './models/types.js'
import { type JudgeFieldDefinition, type ScoreTally, isClearMark, isUndoMark, type Meta, type EntryResult, type TallyScoresheet, type MarkScoresheet } from './models/types.js'
import { type CompetitionEventDefinition } from './preconfigured/types.js'

Expand Down Expand Up @@ -76,6 +78,47 @@ export function formatFactor (value: number): string {
else return `-${roundTo((1 - value) * 100, 0)} %`
}

export function filterMarkStream <Schema extends string> (rawMarks: Readonly<Array<Readonly<Mark<Schema>>>>): Array<GenericMark<Schema>> {
const clearMarkIdx = rawMarks.findLastIndex(mark => mark.schema === 'clear')
const marks = rawMarks.slice(clearMarkIdx + 1)
for (let idx = 0; idx < marks.length; idx++) {
const mark = marks[idx]
if (isUndoMark(mark)) {
let targetIdx = -1
// We're doing an optimised check here since the undone mark need to be
// before the undo mark, and will most likely be just before teh undo mark
// therefore findIndex or findLastIndex would be wasteful
for (let tIdx = idx - 1; tIdx >= 0; tIdx--) {
if (marks[tIdx].sequence === mark.target) {
targetIdx = tIdx
break
}
}
if (targetIdx >= 0 && !isUndoMark(marks[targetIdx]) && !isClearMark(marks[targetIdx])) {
marks.splice(targetIdx, 1)
idx--
}
marks.splice(idx, 1)
idx--
}
}
return marks
}

export function filterTally <Schema extends string> (_tally: ScoreTally, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>): ScoreTally<Schema> {
if (fieldDefinitions == null) return _tally as ScoreTally<Schema>
const tally: ScoreTally<Schema> = {}

for (const field of fieldDefinitions) {
const v = _tally[field.schema]
if (typeof v !== 'number') continue

tally[field.schema] = clampNumber(v, field)
}

return tally
}

/**
* Takes a scoresheet and returns a tally
*
Expand All @@ -85,41 +128,22 @@ export function formatFactor (value: number): string {
* Each value of the tally will also be clamped to the specified max, min and
* step size for that field schema.
*/
export function calculateTally <Schema extends string> (scoresheet: Pick<TallyScoresheet<Schema>, 'tally'> | Pick<MarkScoresheet<Schema>, 'marks'>, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>): ScoreTally<Schema> {
let tally: ScoreTally<Schema> = isTallyScoresheet<Schema>(scoresheet) ? { ...(scoresheet.tally ?? {}) } : {}
const allowedSchemas = fieldDefinitions?.map(f => f.schema)

if (isMarkScoresheet(scoresheet)) {
for (const mark of scoresheet.marks) {
if (isUndoMark(mark)) {
const target = scoresheet.marks.find(m => m.sequence === mark.target)
if (target == null || isUndoMark(target) || isClearMark(target)) continue
tally[target.schema as Schema] = (tally[target.schema as Schema] ?? 0) - (target.value ?? 1)
} else if (isClearMark(mark)) {
tally = {}
} else {
tally[mark.schema as Schema] = (tally[mark.schema as Schema] ?? 0) + (mark.value ?? 1)
}
}
}
export function simpleCalculateTallyFactory <Schema extends string> (judgeTypeId: string, fieldDefinitions?: Readonly<Array<JudgeFieldDefinition<Schema>>>) {
return function simpleCalculateTally (scoresheet: MarkScoresheet<Schema>) {
if (!matchMeta(scoresheet.meta, { judgeTypeId })) throw new RSRWrongJudgeTypeError(scoresheet.meta.judgeTypeId, judgeTypeId)
let tally: ScoreTally<Schema> = isTallyScoresheet<Schema>(scoresheet) ? { ...(scoresheet.tally ?? {}) } : {}

if (fieldDefinitions != null) {
for (const field of fieldDefinitions) {
if (typeof tally[field.schema] !== 'number') continue
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
tally[field.schema] = clampNumber(tally[field.schema] as number, field)
for (const mark of filterMarkStream(scoresheet.marks)) {
tally[mark.schema as Schema] = (tally[mark.schema as Schema] ?? 0) + (mark.value ?? 1)
}
}

if (allowedSchemas != null) {
const extra = Object.keys(tally).filter(schema => !allowedSchemas.includes(schema as Schema))
tally = filterTally(tally, fieldDefinitions)

// @ts-expect-error Yes I know schema doesn't exist in the target object, I'm deleting the schemas that shouldn't be there
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
for (const schema of extra) delete tally[schema]
return {
meta: scoresheet.meta,
tally,
}
}

return tally
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export {
filterParticipatingInAll,
parseCompetitionEventDefinition,
calculateTally,
filterMarkStream,
isMarkScoresheet,
isTallyScoresheet,
} from './helpers.js'
Expand Down
50 changes: 30 additions & 20 deletions lib/models/competition-events/ijru.freestyle@2.0.0.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RSRWrongJudgeTypeError } from '../../errors'
import { calculateTally, formatFactor, matchMeta, roundTo, roundToCurry } from '../../helpers'
import { type ScoreTally, type JudgeTypeGetter, type JudgeFieldDefinition, type TableDefinition, type CompetitionEventModel } from '../types'
import { filterTally, formatFactor, matchMeta, roundTo, roundToCurry, simpleCalculateTallyFactory } from '../../helpers'
import { type JudgeTypeGetter, type JudgeFieldDefinition, type TableDefinition, type CompetitionEventModel } from '../types'
import { ijruAverage } from './ijru.freestyle@3.0.0'

type Option = 'noMusicality' | 'discipline' | 'interactions'
Expand All @@ -24,10 +24,9 @@ export function L (l: number): number {
// ======
// JUDGES
// ======
export const routinePresentationJudge: JudgeTypeGetter<string, Option> = options => {
export const routinePresentationJudge: JudgeTypeGetter<Option> = options => {
const noMusicality = options.noMusicality === true

type Schema = `entertainment${'Plus' | 'Check' | 'Minus'}` | `musicality${'Plus' | 'Check' | 'Minus'}`
const fieldDefinitions = [
{
schema: 'entertainmentPlus',
Expand Down Expand Up @@ -75,10 +74,13 @@ export const routinePresentationJudge: JudgeTypeGetter<string, Option> = options
return {
id,
name: 'Routine Presentation',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory(id, fieldDefinitions),
calculateJudgeResult: scsh => {
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<Schema> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

const enTop = (tally.entertainmentPlus ?? 0) - (tally.entertainmentMinus ?? 0)
const enBottom = (tally.entertainmentPlus ?? 0) + (tally.entertainmentCheck ?? 0) + (tally.entertainmentMinus ?? 0)
const enAvg = enTop / (enBottom || 1)
Expand All @@ -104,7 +106,7 @@ export const routinePresentationJudge: JudgeTypeGetter<string, Option> = options
}
}

export const athletePresentationJudge: JudgeTypeGetter<string, Option> = options => {
export const athletePresentationJudge: JudgeTypeGetter<Option> = options => {
const fieldDefinitions = [
{
name: 'Form and Execution +',
Expand Down Expand Up @@ -135,10 +137,13 @@ export const athletePresentationJudge: JudgeTypeGetter<string, Option> = options
return {
id,
name: 'Athlete Presentation',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory<string>(id, fieldDefinitions),
calculateJudgeResult: scsh => {
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<(typeof fieldDefinitions)[number]['schema']> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

const top = (tally.formExecutionPlus ?? 0) - (tally.formExecutionMinus ?? 0)
const bottom = (tally.formExecutionPlus ?? 0) + (tally.formExecutionCheck ?? 0) + (tally.formExecutionMinus ?? 0)
const avg = top / (bottom || 1)
Expand All @@ -155,7 +160,7 @@ export const athletePresentationJudge: JudgeTypeGetter<string, Option> = options
}
}

export const requiredElementsJudge: JudgeTypeGetter<string, Option> = options => {
export const requiredElementsJudge: JudgeTypeGetter<Option> = options => {
const isDD = options.discipline === 'dd'
const hasInteractions = options.interactions === true
const fieldDefinitions = [
Expand Down Expand Up @@ -230,10 +235,12 @@ export const requiredElementsJudge: JudgeTypeGetter<string, Option> = options =>
return {
id,
name: 'Required Elements',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory(id, fieldDefinitions),
calculateJudgeResult: scsh => {
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

let score = rqFields.map(f => tally[f.schema] ?? 0).reduce((a, b) => a + b)
score = score > max ? max : score
Expand All @@ -255,7 +262,7 @@ export const requiredElementsJudge: JudgeTypeGetter<string, Option> = options =>
}
}

export const difficultyJudge: JudgeTypeGetter<string, Option> = options => {
export const difficultyJudge: JudgeTypeGetter<Option> = options => {
const fieldDefinitions = [
{
name: 'Level 0.5',
Expand All @@ -276,10 +283,13 @@ export const difficultyJudge: JudgeTypeGetter<string, Option> = options => {
return {
id,
name: 'Difficulty',
fieldDefinitions,
calculateScoresheet: scsh => {
markDefinitions: fieldDefinitions,
tallyDefinitions: fieldDefinitions,
calculateTally: simpleCalculateTallyFactory<string>(id, fieldDefinitions),
calculateJudgeResult: scsh => {
if (!matchMeta(scsh.meta, { judgeTypeId: id })) throw new RSRWrongJudgeTypeError(scsh.meta.judgeTypeId, id)
const tally: ScoreTally<(typeof fieldDefinitions)[number]['schema']> = calculateTally(scsh, fieldDefinitions)
const tally = filterTally(scsh.tally, fieldDefinitions)

const D = fieldDefinitions.map(f => (tally[f.schema] ?? 0) * L(levels[f.schema])).reduce((a, b) => a + b)
return {
meta: scsh.meta,
Expand Down Expand Up @@ -394,4 +404,4 @@ export default {

previewTable: options => freestylePreviewTableHeaders,
resultTable: options => freestyleResultTableHeaders,
} satisfies CompetitionEventModel<string, Option>
} satisfies CompetitionEventModel<Option>
Loading

0 comments on commit d2df5d5

Please sign in to comment.