Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,21 @@ final class LanguageGame: Game {
)
if isSuccess {
user.wallet.addGold(gainGold)

/// 정답 횟수 기록
user.record.record(.languageCorrect)
/// 누적 재산 업데이트
user.record.record(.earnMoney(gainGold))

// 재화 획득 시 캐릭터 웃게 만들기
animationSystem?.playSmile()
return gainGold
}
user.wallet.spendGold(Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier))

/// 오답 횟수 기록
user.record.record(.languageIncorrect)

return Int(Double(gainGold) * Policy.Game.Language.incorrectGoldLossMultiplier) * -1
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ final class MissionSystem {
}

/// 기록을 통하여 현재 미션의 상태들을 업데이트 합니다.
/// - Note: 비동기로 처리되어 즉시 반환
/// - Parameter record: 업데이트할 Record
func updateCompletedMissions(record: Record) {
missions
.filter { $0.state == .inProgress }
.forEach { $0.update(record: record) }

sortMissions()
checkHasCompletedMission()
// 메인 큐에 비동기로 추가 (즉시 반환, 순서 보장, 누락 없음)
DispatchQueue.main.async { [weak self] in
self?.performMissionUpdate(record: record)
}
}

func claimMissionReward(mission: Mission, wallet: Wallet) {
Expand All @@ -46,12 +46,25 @@ final class MissionSystem {
sortMissions()
checkHasCompletedMission()
}
}

// MARK: - Helper
private extension MissionSystem {
/// 실제 미션 업데이트 수행 (메인 스레드, 비동기)
func performMissionUpdate(record: Record) {
missions
.filter { $0.state == .inProgress }
.forEach { $0.update(record: record) }

sortMissions()
checkHasCompletedMission()
}

private func checkHasCompletedMission() {
func checkHasCompletedMission() {
hasCompletedMission = missions.contains { $0.state == .claimable }
}

private func sortMissions() {
func sortMissions() {
missions.sort { $0.state.rawValue < $1.state.rawValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// MissionSystemTests.swift
// SoloDeveloperTraining
//
// Created by 김성훈 on 2/4/26.
//

import Testing
import Foundation
@testable import SoloDeveloperTraining

@MainActor
struct MissionSystemTests {

@Test("기본 미션 업데이트 동작 검증")
func testBasicMissionUpdate() async throws {
// Given: 목표 10인 언어 미션 생성
let mission = Mission(
id: 1,
type: .languageMatch(.bronze),
title: "언어 10회 성공",
description: "언어 게임 10회 성공하기",
targetValue: 10,
updateCondition: { $0.languageCorrectCount },
reward: Cost(gold: 100)
)
let missionSystem = MissionSystem(missions: [mission])
let record = Record(missionSystem: missionSystem)

// When: 10번 정답 기록
for _ in 1...10 {
record.record(.languageCorrect)
}

// 비동기 처리 완료 대기
try await Task.sleep(nanoseconds: 100_000_000) // 0.1초

// Then: 미션 완료되어야 함
#expect(mission.currentValue == 10, "미션 값이 10이어야 함")
#expect(mission.state == .claimable, "미션이 완료 상태여야 함")
#expect(missionSystem.hasCompletedMission == true, "완료된 미션이 있어야 함")
}

@Test("연속 업데이트 시 누락 방지 - 100회 연속 기록")
func testConsecutiveUpdateGuarantee() async throws {
// Given: 목표 100인 언어 미션
let mission = Mission(
id: 2,
type: .languageMatch(.silver),
title: "언어 100회 성공",
description: "언어 게임 100회 성공하기",
targetValue: 100,
updateCondition: { $0.languageCorrectCount },
reward: Cost(gold: 1000)
)
let missionSystem = MissionSystem(missions: [mission])
let record = Record(missionSystem: missionSystem)

// When: 100번 빠르게 연속 기록
for _ in 1...100 {
record.record(.languageCorrect)
}

// 비동기 처리 완료 대기
try await Task.sleep(nanoseconds: 100_000_000) // 0.1초

// Then: 정확히 100이어야 함 (누락 없음)
#expect(record.languageCorrectCount == 100, "레코드가 정확히 100이어야 함")
#expect(mission.currentValue == 100, "미션 값이 정확히 100이어야 함")
#expect(mission.state == .claimable, "미션이 완료되어야 함")
}

@Test("여러 미션 동시 업데이트 시 누락 방지 - 30개 미션")
func testMultipleMissionsUpdateWithoutLoss() async throws {
// Given: 30개의 미션 생성
let missions = (1...30).map { idx in
Mission(
id: idx,
type: .languageMatch(.bronze),
title: "미션 \(idx)",
description: "테스트 미션",
targetValue: 1,
updateCondition: { $0.languageCorrectCount },
reward: Cost(gold: 100)
)
}
let missionSystem = MissionSystem(missions: missions)
let record = Record(missionSystem: missionSystem)

// When: 1번만 기록
record.record(.languageCorrect)

// 비동기 처리 완료 대기
try await Task.sleep(nanoseconds: 100_000_000) // 0.1초

// Then: 모든 미션이 완료되어야 함 (누락 없음)
let completedCount = missions.filter { $0.state == .claimable }.count
#expect(completedCount == 30, "30개 미션 모두 완료되어야 함 - 누락 없음")
#expect(missionSystem.hasCompletedMission == true, "완료 플래그가 true여야 함")
}

@Test("earnMoney 이벤트로 누적 골드 미션 업데이트")
func testEarnMoneyEventMissionUpdate() async throws {
// Given: 누적 골드 미션
let mission = Mission(
id: 100,
type: .tap(.gold),
title: "1000골드 획득",
description: "누적 1000골드 벌기",
targetValue: 1000,
updateCondition: { $0.totalEarnedMoney },
reward: Cost(gold: 500, diamond: 10)
)
let missionSystem = MissionSystem(missions: [mission])
let record = Record(missionSystem: missionSystem)

// When: 여러 번 골드 획득
record.record(.earnMoney(300))
record.record(.earnMoney(400))
record.record(.earnMoney(300))

// 비동기 처리 완료 대기
try await Task.sleep(nanoseconds: 100_000_000) // 0.1초

// Then: 정확히 1000이어야 함
#expect(record.totalEarnedMoney == 1000, "총 획득 골드가 1000이어야 함")
#expect(mission.currentValue == 1000, "미션 값이 1000이어야 함")
#expect(mission.state == .claimable, "미션이 완료되어야 함")
}
}