| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- declare const console: {
- log: (...args: unknown[]) => void
- }
- import { buildGameDefinitionFromCourse } from '../miniprogram/game/content/courseToGameDefinition'
- import { getGameModeDefaults } from '../miniprogram/game/core/gameModeDefaults'
- import { GameRuntime } from '../miniprogram/game/core/gameRuntime'
- import { ScoreORule } from '../miniprogram/game/rules/scoreORule'
- import { resolveSystemSettingsState } from '../miniprogram/game/core/systemSettingsState'
- import { adaptBackendLaunchResultToEnvelope } from '../miniprogram/utils/backendLaunchAdapter'
- import { type GameDefinition } from '../miniprogram/game/core/gameDefinition'
- import { type BackendLaunchResult } from '../miniprogram/utils/backendApi'
- import { type OrienteeringCourseData } from '../miniprogram/utils/orienteeringCourse'
- type StorageMap = Record<string, unknown>
- function assert(condition: boolean, message: string): void {
- if (!condition) {
- throw new Error(message)
- }
- }
- function createWxStorage(storage: StorageMap): void {
- ;(globalThis as { wx?: unknown }).wx = {
- getStorageSync(key: string): unknown {
- return storage[key]
- },
- setStorageSync(key: string, value: unknown): void {
- storage[key] = value
- },
- }
- }
- function buildCourse(): OrienteeringCourseData {
- return {
- title: 'Smoke Test Course',
- layers: {
- starts: [
- { label: 'Start', point: { lon: 120.0, lat: 30.0 }, headingDeg: 90 },
- ],
- controls: [
- { label: '1', sequence: 1, point: { lon: 120.0001, lat: 30.0 } },
- { label: '2', sequence: 2, point: { lon: 120.0002, lat: 30.0 } },
- ],
- finishes: [
- { label: 'Finish', point: { lon: 120.0003, lat: 30.0 } },
- ],
- legs: [],
- },
- }
- }
- function getControl(definition: GameDefinition, id: string) {
- return definition.controls.find((control) => control.id === id) || null
- }
- function testControlInheritance(): void {
- const definition = buildGameDefinitionFromCourse(
- buildCourse(),
- 5,
- 'score-o',
- undefined,
- undefined,
- undefined,
- undefined,
- 'enter-confirm',
- 5,
- false,
- false,
- undefined,
- undefined,
- { 'control-2': 80 },
- {
- title: '默认说明',
- body: '所有点默认正文',
- },
- {
- 'control-2': {
- body: '2号点单点覆盖正文',
- },
- },
- 30,
- )
- const control1 = getControl(definition, 'control-1')
- const control2 = getControl(definition, 'control-2')
- assert(!!control1 && !!control2, '应生成普通检查点')
- assert(control1!.score === 30, 'controlDefaults 默认分值应继承到普通点')
- assert(control2!.score === 80, '单点 score override 应覆盖默认分值')
- assert(!!control1!.displayContent && control1!.displayContent.title === '默认说明', '默认内容标题应继承到普通点')
- assert(!!control2!.displayContent && control2!.displayContent.body === '2号点单点覆盖正文', '单点内容 override 应覆盖默认正文')
- }
- function testScoreOFreePunchAndFinishGate(): void {
- const definition = buildGameDefinitionFromCourse(
- buildCourse(),
- 5,
- 'score-o',
- 2 * 60 * 60 * 1000,
- 10 * 60 * 1000,
- 1,
- false,
- 'enter-confirm',
- 5,
- false,
- )
- const rule = new ScoreORule()
- let state = rule.initialize(definition)
- let result = rule.reduce(definition, state, { type: 'session_started', at: 1 })
- state = result.nextState
- result = rule.reduce(definition, state, {
- type: 'gps_updated',
- at: 2,
- lon: 120.0,
- lat: 30.0,
- accuracyMeters: null,
- })
- state = result.nextState
- result = rule.reduce(definition, state, {
- type: 'punch_requested',
- at: 3,
- lon: 120.0,
- lat: 30.0,
- })
- state = result.nextState
- assert(state.completedControlIds.includes('start-1'), '积分赛应能完成开始点')
- result = rule.reduce(definition, state, {
- type: 'gps_updated',
- at: 4,
- lon: 120.0001,
- lat: 30.0,
- accuracyMeters: null,
- })
- state = result.nextState
- assert(result.presentation.hud.punchButtonEnabled, '自由打点时进入普通点范围应可直接打点')
- result = rule.reduce(definition, state, {
- type: 'punch_requested',
- at: 5,
- lon: 120.0001,
- lat: 30.0,
- })
- state = result.nextState
- assert(state.completedControlIds.includes('control-1'), '积分赛默认无需先选中也应可打普通点')
- const preFinishState = rule.initialize(definition)
- let preFinishResult = rule.reduce(definition, preFinishState, { type: 'session_started', at: 10 })
- let runningState = preFinishResult.nextState
- preFinishResult = rule.reduce(definition, runningState, {
- type: 'gps_updated',
- at: 11,
- lon: 120.0,
- lat: 30.0,
- accuracyMeters: null,
- })
- runningState = preFinishResult.nextState
- preFinishResult = rule.reduce(definition, runningState, {
- type: 'punch_requested',
- at: 12,
- lon: 120.0,
- lat: 30.0,
- })
- runningState = preFinishResult.nextState
- preFinishResult = rule.reduce(definition, runningState, {
- type: 'punch_requested',
- at: 13,
- lon: 120.0003,
- lat: 30.0,
- })
- assert(preFinishResult.effects.some((effect) => effect.type === 'punch_feedback'), '未完成最低点数前打终点应被拦截')
- result = rule.reduce(definition, state, {
- type: 'gps_updated',
- at: 6,
- lon: 120.0003,
- lat: 30.0,
- accuracyMeters: null,
- })
- state = result.nextState
- assert(result.presentation.hud.punchButtonText === '结束打卡', '终点进入范围后按钮文案应切为结束打卡')
- result = rule.reduce(definition, state, {
- type: 'punch_requested',
- at: 7,
- lon: 120.0003,
- lat: 30.0,
- })
- state = result.nextState
- assert(state.status === 'finished' && state.endReason === 'completed', '达到终点解锁条件后应可正常结束')
- }
- function testSettingsLockLifecycle(): void {
- const storage: StorageMap = {
- cmr_user_settings_v1: {
- gpsMarkerStyle: 'dot',
- trackDisplayMode: 'full',
- },
- }
- createWxStorage(storage)
- const runtimeLocked = resolveSystemSettingsState(
- {
- values: {
- gpsMarkerStyle: 'beacon',
- },
- locks: {
- lockGpsMarkerStyle: true,
- },
- },
- 'cmr_user_settings_v1',
- true,
- )
- assert(runtimeLocked.values.gpsMarkerStyle === 'beacon', '本局锁定时应以配置值为准')
- assert(runtimeLocked.locks.lockGpsMarkerStyle, '本局内锁态应生效')
- const runtimeReleased = resolveSystemSettingsState(
- {
- values: {
- gpsMarkerStyle: 'beacon',
- },
- locks: {
- lockGpsMarkerStyle: true,
- },
- },
- 'cmr_user_settings_v1',
- false,
- )
- assert(runtimeReleased.values.gpsMarkerStyle === 'dot', '脱离本局后应回落到玩家持久化设置')
- assert(!runtimeReleased.locks.lockGpsMarkerStyle, '脱离本局后锁态应自动解除')
- }
- function testTimeoutEndReason(): void {
- const definition = buildGameDefinitionFromCourse(buildCourse(), 5, 'classic-sequential')
- const rule = new ScoreORule()
- const state = rule.initialize(definition)
- const result = rule.reduce(definition, state, { type: 'session_timed_out', at: 99 })
- assert(result.nextState.status === 'failed', '超时应进入 failed 状态')
- assert(result.nextState.endReason === 'timed_out', '超时结束原因应为 timed_out')
- }
- function testClassicSequentialSkipConfirmDefault(): void {
- const defaults = getGameModeDefaults('classic-sequential')
- assert(defaults.skipEnabled, '顺序打点默认应开启跳点')
- assert(defaults.skipRequiresConfirm, '顺序打点默认跳点应弹出确认')
- }
- function testRuntimeRestoreDefinition(): void {
- const definition = buildGameDefinitionFromCourse(
- buildCourse(),
- 5,
- 'score-o',
- 2 * 60 * 60 * 1000,
- 10 * 60 * 1000,
- 1,
- false,
- 'enter-confirm',
- 5,
- false,
- )
- const runtime = new GameRuntime()
- runtime.loadDefinition(definition)
- runtime.startSession(1)
- runtime.dispatch({
- type: 'gps_updated',
- at: 2,
- lon: 120.0,
- lat: 30.0,
- accuracyMeters: null,
- })
- runtime.dispatch({
- type: 'punch_requested',
- at: 3,
- lon: 120.0,
- lat: 30.0,
- })
- runtime.dispatch({
- type: 'gps_updated',
- at: 4,
- lon: 120.0001,
- lat: 30.0,
- accuracyMeters: null,
- })
- runtime.dispatch({
- type: 'punch_requested',
- at: 5,
- lon: 120.0001,
- lat: 30.0,
- })
- const savedState = runtime.state
- assert(!!savedState, '恢复测试前应存在对局状态')
- const restoredRuntime = new GameRuntime()
- const restoreResult = restoredRuntime.restoreDefinition(definition, savedState!)
- assert(restoredRuntime.state !== null, '恢复后应保留对局状态')
- assert(restoredRuntime.state!.completedControlIds.includes('control-1'), '恢复后应保留已完成检查点')
- assert(restoredRuntime.state!.status === 'running', '恢复后对局应继续保持 running')
- assert(restoreResult.presentation.hud.punchButtonText === runtime.presentation.hud.punchButtonText, '恢复后 HUD 关键按钮文案应可重建')
- }
- function testLaunchRuntimeAdapter(): void {
- const launchResult: BackendLaunchResult = {
- event: {
- id: 'evt_demo_variant_manual_001',
- displayName: 'Manual Variant Demo',
- },
- launch: {
- source: 'event',
- resolvedRelease: {
- launchMode: 'formal-release',
- source: 'current-release',
- eventId: 'evt_demo_variant_manual_001',
- releaseId: 'rel_runtime_001',
- configLabel: 'runtime demo',
- manifestUrl: 'https://example.com/releases/rel_runtime_001/manifest.json',
- manifestChecksumSha256: 'manifest-sha-001',
- routeCode: 'route-variant-b',
- },
- config: {
- configUrl: 'https://example.com/runtime.json',
- configLabel: 'runtime demo',
- releaseId: 'rel_runtime_001',
- routeCode: 'route-variant-b',
- },
- business: {
- source: 'direct-event',
- eventId: 'evt_demo_variant_manual_001',
- sessionId: 'sess_001',
- sessionToken: 'token_001',
- sessionTokenExpiresAt: '2026-04-03T16:00:00+08:00',
- routeCode: 'route-variant-b',
- },
- variant: {
- id: 'variant_b',
- name: 'B 线',
- routeCode: 'route-variant-b',
- assignmentMode: 'manual',
- },
- runtime: {
- runtimeBindingId: 'rtb_001',
- placeId: 'place_campus',
- placeName: '示范校园',
- mapId: 'map_main',
- mapName: '主图',
- tileReleaseId: 'tile_rel_001',
- courseSetId: 'course_set_001',
- courseVariantId: 'variant_b',
- routeCode: 'route-variant-b',
- },
- presentation: {
- presentationId: 'pres_001',
- templateKey: 'campus-v1',
- version: 'v3',
- },
- contentBundle: {
- bundleId: 'bundle_001',
- bundleType: 'quiz-pack',
- version: 'v7',
- },
- },
- }
- const envelope = adaptBackendLaunchResultToEnvelope(launchResult)
- assert(!!envelope.resolvedRelease, 'resolvedRelease 应映射到 GameLaunchEnvelope.resolvedRelease')
- assert(envelope.resolvedRelease!.manifestUrl === 'https://example.com/releases/rel_runtime_001/manifest.json', 'resolvedRelease.manifestUrl 应正确适配')
- assert(envelope.resolvedRelease!.releaseId === 'rel_runtime_001', 'resolvedRelease.releaseId 应正确适配')
- assert(!!envelope.runtime, 'launch.runtime 应映射到 GameLaunchEnvelope.runtime')
- assert(envelope.runtime!.runtimeBindingId === 'rtb_001', 'runtimeBindingId 应正确适配')
- assert(envelope.runtime!.placeName === '示范校园', 'placeName 应正确适配')
- assert(envelope.runtime!.mapName === '主图', 'mapName 应正确适配')
- assert(envelope.runtime!.courseVariantId === 'variant_b', 'courseVariantId 应正确适配')
- assert(envelope.runtime!.routeCode === 'route-variant-b', 'runtime routeCode 应优先保留后端透出值')
- assert(!!envelope.variant && envelope.variant.variantName === 'B 线', 'variant 摘要应继续保持兼容')
- assert(!!envelope.presentation && envelope.presentation.presentationId === 'pres_001', 'launch.presentation 应映射到 GameLaunchEnvelope.presentation')
- assert(!!envelope.contentBundle && envelope.contentBundle.bundleId === 'bundle_001', 'launch.contentBundle 应映射到 GameLaunchEnvelope.contentBundle')
- }
- function run(): void {
- createWxStorage({})
- testControlInheritance()
- testScoreOFreePunchAndFinishGate()
- testSettingsLockLifecycle()
- testTimeoutEndReason()
- testClassicSequentialSkipConfirmDefault()
- testRuntimeRestoreDefinition()
- testLaunchRuntimeAdapter()
- console.log('runtime smoke tests passed')
- }
- run()
|