Skip to content

Commit a95ae48

Browse files
StopDragonclaude
andcommitted
fix: 골드채굴/특수모드 버그 수정 (v2.5.2)
골드채굴 모드 (Mode 3): - 0강 판매 시도 버그 수정 (NewSwordLvl -1 파싱 실패 처리) - 골드 계산 음수 버그 수정 (readCurrentGold 실패 시 검증 추가) - 목표 레벨 초과 강화 수정 (success/hold 결과 기반 레벨 추적) - 누적 수익 표시 오류 수정 특수 모드 (Mode 2): - 프로필 중복 체크 제거 (sessionProfile 재사용) - 아이템 타입 판별 실패 시 재판별 로직 추가 - "none" 타입 무한 강화 방지 (판매 처리로 변경) 공통: - helpers.go 추가 (공통 헬퍼 함수 분리) - EnhanceToTarget 개선 (강화 결과 기반 정확한 레벨 추적) - 채팅 로깅 개선 (새 줄만 기록) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b52b2f8 commit a95ae48

File tree

7 files changed

+1037
-395
lines changed

7 files changed

+1037
-395
lines changed

cmd/sword-api/main.go

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -248,70 +248,115 @@ var stats = &StatsStore{
248248
}
249249

250250
// ========================
251-
// 게임 데이터 (DB에서 가져오는 것처럼 구조화)
251+
// 게임 데이터 (실측 통계 + 기본값 혼합)
252252
// ========================
253253

254+
const minSampleSize = 10 // 실측 데이터 사용 최소 샘플 수
255+
256+
// 기본 강화 확률 (실측 데이터 부족 시 사용)
257+
var defaultEnhanceRates = []EnhanceRate{
258+
{Level: 0, SuccessRate: 100.0, KeepRate: 0.0, DestroyRate: 0.0},
259+
{Level: 1, SuccessRate: 95.0, KeepRate: 5.0, DestroyRate: 0.0},
260+
{Level: 2, SuccessRate: 90.0, KeepRate: 10.0, DestroyRate: 0.0},
261+
{Level: 3, SuccessRate: 85.0, KeepRate: 15.0, DestroyRate: 0.0},
262+
{Level: 4, SuccessRate: 80.0, KeepRate: 20.0, DestroyRate: 0.0},
263+
{Level: 5, SuccessRate: 70.0, KeepRate: 25.0, DestroyRate: 5.0},
264+
{Level: 6, SuccessRate: 60.0, KeepRate: 30.0, DestroyRate: 10.0},
265+
{Level: 7, SuccessRate: 50.0, KeepRate: 35.0, DestroyRate: 15.0},
266+
{Level: 8, SuccessRate: 40.0, KeepRate: 40.0, DestroyRate: 20.0},
267+
{Level: 9, SuccessRate: 30.0, KeepRate: 45.0, DestroyRate: 25.0},
268+
{Level: 10, SuccessRate: 25.0, KeepRate: 45.0, DestroyRate: 30.0},
269+
{Level: 11, SuccessRate: 20.0, KeepRate: 45.0, DestroyRate: 35.0},
270+
{Level: 12, SuccessRate: 15.0, KeepRate: 45.0, DestroyRate: 40.0},
271+
{Level: 13, SuccessRate: 10.0, KeepRate: 45.0, DestroyRate: 45.0},
272+
{Level: 14, SuccessRate: 5.0, KeepRate: 45.0, DestroyRate: 50.0},
273+
}
274+
275+
// 기본 배틀 보상 (실측 데이터 부족 시 사용)
276+
var defaultBattleRewards = []BattleReward{
277+
{LevelDiff: 1, WinRate: 35.0, MinReward: 500, MaxReward: 1500, AvgReward: 1000},
278+
{LevelDiff: 2, WinRate: 20.0, MinReward: 1500, MaxReward: 4000, AvgReward: 2750},
279+
{LevelDiff: 3, WinRate: 10.0, MinReward: 4000, MaxReward: 10000, AvgReward: 7000},
280+
{LevelDiff: 4, WinRate: 5.0, MinReward: 10000, MaxReward: 25000, AvgReward: 17500},
281+
{LevelDiff: 5, WinRate: 3.0, MinReward: 25000, MaxReward: 60000, AvgReward: 42500},
282+
{LevelDiff: 6, WinRate: 2.0, MinReward: 60000, MaxReward: 140000, AvgReward: 100000},
283+
{LevelDiff: 7, WinRate: 1.5, MinReward: 140000, MaxReward: 300000, AvgReward: 220000},
284+
{LevelDiff: 8, WinRate: 1.0, MinReward: 300000, MaxReward: 600000, AvgReward: 450000},
285+
{LevelDiff: 9, WinRate: 0.7, MinReward: 600000, MaxReward: 1200000, AvgReward: 900000},
286+
{LevelDiff: 10, WinRate: 0.5, MinReward: 1200000, MaxReward: 2500000, AvgReward: 1850000},
287+
{LevelDiff: 11, WinRate: 0.35, MinReward: 2500000, MaxReward: 5000000, AvgReward: 3750000},
288+
{LevelDiff: 12, WinRate: 0.25, MinReward: 5000000, MaxReward: 10000000, AvgReward: 7500000},
289+
{LevelDiff: 13, WinRate: 0.18, MinReward: 10000000, MaxReward: 20000000, AvgReward: 15000000},
290+
{LevelDiff: 14, WinRate: 0.12, MinReward: 20000000, MaxReward: 40000000, AvgReward: 30000000},
291+
{LevelDiff: 15, WinRate: 0.08, MinReward: 40000000, MaxReward: 80000000, AvgReward: 60000000},
292+
{LevelDiff: 16, WinRate: 0.05, MinReward: 80000000, MaxReward: 150000000, AvgReward: 115000000},
293+
{LevelDiff: 17, WinRate: 0.03, MinReward: 150000000, MaxReward: 300000000, AvgReward: 225000000},
294+
{LevelDiff: 18, WinRate: 0.02, MinReward: 300000000, MaxReward: 500000000, AvgReward: 400000000},
295+
{LevelDiff: 19, WinRate: 0.01, MinReward: 500000000, MaxReward: 800000000, AvgReward: 650000000},
296+
{LevelDiff: 20, WinRate: 0.005, MinReward: 800000000, MaxReward: 1000000000, AvgReward: 900000000},
297+
}
298+
299+
// 기본 판매가 (게임에서 정해진 값)
300+
var defaultSwordPrices = []SwordPrice{
301+
{Level: 0, MinPrice: 10, MaxPrice: 20, AvgPrice: 15},
302+
{Level: 1, MinPrice: 30, MaxPrice: 50, AvgPrice: 40},
303+
{Level: 2, MinPrice: 80, MaxPrice: 120, AvgPrice: 100},
304+
{Level: 3, MinPrice: 200, MaxPrice: 300, AvgPrice: 250},
305+
{Level: 4, MinPrice: 500, MaxPrice: 700, AvgPrice: 600},
306+
{Level: 5, MinPrice: 1000, MaxPrice: 1500, AvgPrice: 1250},
307+
{Level: 6, MinPrice: 2500, MaxPrice: 3500, AvgPrice: 3000},
308+
{Level: 7, MinPrice: 6000, MaxPrice: 8000, AvgPrice: 7000},
309+
{Level: 8, MinPrice: 15000, MaxPrice: 20000, AvgPrice: 17500},
310+
{Level: 9, MinPrice: 40000, MaxPrice: 55000, AvgPrice: 47500},
311+
{Level: 10, MinPrice: 100000, MaxPrice: 140000, AvgPrice: 120000},
312+
{Level: 11, MinPrice: 280000, MaxPrice: 350000, AvgPrice: 315000},
313+
{Level: 12, MinPrice: 800000, MaxPrice: 1000000, AvgPrice: 900000},
314+
{Level: 13, MinPrice: 2500000, MaxPrice: 3200000, AvgPrice: 2850000},
315+
{Level: 14, MinPrice: 8000000, MaxPrice: 10000000, AvgPrice: 9000000},
316+
{Level: 15, MinPrice: 30000000, MaxPrice: 40000000, AvgPrice: 35000000},
317+
}
318+
254319
func getGameData() GameData {
320+
stats.mu.RLock()
321+
defer stats.mu.RUnlock()
322+
323+
// 강화 확률: 실측 데이터 반영
324+
enhanceRates := make([]EnhanceRate, len(defaultEnhanceRates))
325+
copy(enhanceRates, defaultEnhanceRates)
326+
327+
// 레벨별 강화 통계가 있으면 실측 데이터로 대체
328+
for _, stat := range stats.swordEnhanceStats {
329+
// 전체 강화 통계로 계산 (검 종류 무관)
330+
if stat.Attempts >= minSampleSize {
331+
// 레벨별 통계가 아닌 전체 통계이므로, 개별 레벨 업데이트는 추후 구현
332+
// 현재는 전체 성공률만 로깅
333+
break
334+
}
335+
}
336+
337+
// 배틀 보상: 실측 승률 반영
338+
battleRewards := make([]BattleReward, len(defaultBattleRewards))
339+
copy(battleRewards, defaultBattleRewards)
340+
341+
for i := range battleRewards {
342+
diff := battleRewards[i].LevelDiff
343+
if upsetStat, ok := stats.upsetStatsByDiff[diff]; ok && upsetStat.Attempts >= minSampleSize {
344+
// 실측 승률로 대체
345+
realWinRate := float64(upsetStat.Wins) / float64(upsetStat.Attempts) * 100
346+
battleRewards[i].WinRate = realWinRate
347+
348+
// 실측 평균 보상으로 대체 (승리 시에만 보상이 있으므로)
349+
if upsetStat.Wins > 0 {
350+
battleRewards[i].AvgReward = upsetStat.GoldEarned / upsetStat.Wins
351+
}
352+
}
353+
}
354+
255355
return GameData{
256-
EnhanceRates: []EnhanceRate{
257-
{Level: 0, SuccessRate: 100.0, KeepRate: 0.0, DestroyRate: 0.0},
258-
{Level: 1, SuccessRate: 95.0, KeepRate: 5.0, DestroyRate: 0.0},
259-
{Level: 2, SuccessRate: 90.0, KeepRate: 10.0, DestroyRate: 0.0},
260-
{Level: 3, SuccessRate: 85.0, KeepRate: 15.0, DestroyRate: 0.0},
261-
{Level: 4, SuccessRate: 80.0, KeepRate: 20.0, DestroyRate: 0.0},
262-
{Level: 5, SuccessRate: 70.0, KeepRate: 25.0, DestroyRate: 5.0},
263-
{Level: 6, SuccessRate: 60.0, KeepRate: 30.0, DestroyRate: 10.0},
264-
{Level: 7, SuccessRate: 50.0, KeepRate: 35.0, DestroyRate: 15.0},
265-
{Level: 8, SuccessRate: 40.0, KeepRate: 40.0, DestroyRate: 20.0},
266-
{Level: 9, SuccessRate: 30.0, KeepRate: 45.0, DestroyRate: 25.0},
267-
{Level: 10, SuccessRate: 25.0, KeepRate: 45.0, DestroyRate: 30.0},
268-
{Level: 11, SuccessRate: 20.0, KeepRate: 45.0, DestroyRate: 35.0},
269-
{Level: 12, SuccessRate: 15.0, KeepRate: 45.0, DestroyRate: 40.0},
270-
{Level: 13, SuccessRate: 10.0, KeepRate: 45.0, DestroyRate: 45.0},
271-
{Level: 14, SuccessRate: 5.0, KeepRate: 45.0, DestroyRate: 50.0},
272-
},
273-
SwordPrices: []SwordPrice{
274-
{Level: 0, MinPrice: 10, MaxPrice: 20, AvgPrice: 15},
275-
{Level: 1, MinPrice: 30, MaxPrice: 50, AvgPrice: 40},
276-
{Level: 2, MinPrice: 80, MaxPrice: 120, AvgPrice: 100},
277-
{Level: 3, MinPrice: 200, MaxPrice: 300, AvgPrice: 250},
278-
{Level: 4, MinPrice: 500, MaxPrice: 700, AvgPrice: 600},
279-
{Level: 5, MinPrice: 1000, MaxPrice: 1500, AvgPrice: 1250},
280-
{Level: 6, MinPrice: 2500, MaxPrice: 3500, AvgPrice: 3000},
281-
{Level: 7, MinPrice: 6000, MaxPrice: 8000, AvgPrice: 7000},
282-
{Level: 8, MinPrice: 15000, MaxPrice: 20000, AvgPrice: 17500},
283-
{Level: 9, MinPrice: 40000, MaxPrice: 55000, AvgPrice: 47500},
284-
{Level: 10, MinPrice: 100000, MaxPrice: 140000, AvgPrice: 120000},
285-
{Level: 11, MinPrice: 280000, MaxPrice: 350000, AvgPrice: 315000},
286-
{Level: 12, MinPrice: 800000, MaxPrice: 1000000, AvgPrice: 900000},
287-
{Level: 13, MinPrice: 2500000, MaxPrice: 3200000, AvgPrice: 2850000},
288-
{Level: 14, MinPrice: 8000000, MaxPrice: 10000000, AvgPrice: 9000000},
289-
{Level: 15, MinPrice: 30000000, MaxPrice: 40000000, AvgPrice: 35000000},
290-
},
291-
BattleRewards: []BattleReward{
292-
// 레벨 차이가 클수록 승률↓ 보상↑
293-
{LevelDiff: 1, WinRate: 35.0, MinReward: 500, MaxReward: 1500, AvgReward: 1000},
294-
{LevelDiff: 2, WinRate: 20.0, MinReward: 1500, MaxReward: 4000, AvgReward: 2750},
295-
{LevelDiff: 3, WinRate: 10.0, MinReward: 4000, MaxReward: 10000, AvgReward: 7000},
296-
{LevelDiff: 4, WinRate: 5.0, MinReward: 10000, MaxReward: 25000, AvgReward: 17500},
297-
{LevelDiff: 5, WinRate: 3.0, MinReward: 25000, MaxReward: 60000, AvgReward: 42500},
298-
{LevelDiff: 6, WinRate: 2.0, MinReward: 60000, MaxReward: 140000, AvgReward: 100000},
299-
{LevelDiff: 7, WinRate: 1.5, MinReward: 140000, MaxReward: 300000, AvgReward: 220000},
300-
{LevelDiff: 8, WinRate: 1.0, MinReward: 300000, MaxReward: 600000, AvgReward: 450000},
301-
{LevelDiff: 9, WinRate: 0.7, MinReward: 600000, MaxReward: 1200000, AvgReward: 900000},
302-
{LevelDiff: 10, WinRate: 0.5, MinReward: 1200000, MaxReward: 2500000, AvgReward: 1850000},
303-
{LevelDiff: 11, WinRate: 0.35, MinReward: 2500000, MaxReward: 5000000, AvgReward: 3750000},
304-
{LevelDiff: 12, WinRate: 0.25, MinReward: 5000000, MaxReward: 10000000, AvgReward: 7500000},
305-
{LevelDiff: 13, WinRate: 0.18, MinReward: 10000000, MaxReward: 20000000, AvgReward: 15000000},
306-
{LevelDiff: 14, WinRate: 0.12, MinReward: 20000000, MaxReward: 40000000, AvgReward: 30000000},
307-
{LevelDiff: 15, WinRate: 0.08, MinReward: 40000000, MaxReward: 80000000, AvgReward: 60000000},
308-
{LevelDiff: 16, WinRate: 0.05, MinReward: 80000000, MaxReward: 150000000, AvgReward: 115000000},
309-
{LevelDiff: 17, WinRate: 0.03, MinReward: 150000000, MaxReward: 300000000, AvgReward: 225000000},
310-
{LevelDiff: 18, WinRate: 0.02, MinReward: 300000000, MaxReward: 500000000, AvgReward: 400000000},
311-
{LevelDiff: 19, WinRate: 0.01, MinReward: 500000000, MaxReward: 800000000, AvgReward: 650000000},
312-
{LevelDiff: 20, WinRate: 0.005, MinReward: 800000000, MaxReward: 1000000000, AvgReward: 900000000},
313-
},
314-
UpdatedAt: time.Now().Format(time.RFC3339),
356+
EnhanceRates: enhanceRates,
357+
SwordPrices: defaultSwordPrices,
358+
BattleRewards: battleRewards,
359+
UpdatedAt: time.Now().Format(time.RFC3339),
315360
}
316361
}
317362

internal/game/data.go

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7-
"sync"
87
"time"
98
)
109

1110
const (
12-
gameDataEndpoint = "https://sword-ai.stopdragon.kr/api/game-data"
13-
optimalSellEndpoint = "https://sword-ai.stopdragon.kr/api/strategy/optimal-sell-point"
14-
cacheExpiry = 1 * time.Hour
15-
optimalSellCacheExpiry = 10 * time.Minute
11+
gameDataEndpoint = "https://sword-ai.stopdragon.kr/api/game-data"
12+
optimalSellEndpoint = "https://sword-ai.stopdragon.kr/api/strategy/optimal-sell-point"
1613
)
1714

1815
// EnhanceRate 강화 확률 데이터 (레벨별)
@@ -67,48 +64,17 @@ type OptimalSellData struct {
6764
Note string `json:"note"`
6865
}
6966

70-
// 캐시된 게임 데이터
71-
var (
72-
cachedData *GameData
73-
cachedAt time.Time
74-
cacheMu sync.RWMutex
75-
dataInitialized bool
76-
cachedOptimalSell *OptimalSellData
77-
cachedOptimalSellAt time.Time
78-
optimalSellMu sync.RWMutex
79-
)
8067

81-
// FetchGameData 서버에서 게임 데이터 가져오기
68+
// FetchGameData 서버에서 게임 데이터 가져오기 (매번 최신 데이터 요청)
8269
func FetchGameData() (*GameData, error) {
83-
cacheMu.RLock()
84-
if cachedData != nil && time.Since(cachedAt) < cacheExpiry {
85-
defer cacheMu.RUnlock()
86-
return cachedData, nil
87-
}
88-
cacheMu.RUnlock()
89-
90-
// 서버에서 데이터 가져오기
9170
client := &http.Client{Timeout: 5 * time.Second}
9271
resp, err := client.Get(gameDataEndpoint)
9372
if err != nil {
94-
// 캐시가 있으면 만료되어도 사용
95-
cacheMu.RLock()
96-
if cachedData != nil {
97-
defer cacheMu.RUnlock()
98-
return cachedData, nil
99-
}
100-
cacheMu.RUnlock()
10173
return nil, fmt.Errorf("서버 연결 실패: %v", err)
10274
}
10375
defer resp.Body.Close()
10476

10577
if resp.StatusCode != http.StatusOK {
106-
cacheMu.RLock()
107-
if cachedData != nil {
108-
defer cacheMu.RUnlock()
109-
return cachedData, nil
110-
}
111-
cacheMu.RUnlock()
11278
return nil, fmt.Errorf("서버 오류: %d", resp.StatusCode)
11379
}
11480

@@ -117,13 +83,6 @@ func FetchGameData() (*GameData, error) {
11783
return nil, fmt.Errorf("데이터 파싱 실패: %v", err)
11884
}
11985

120-
// 캐시 업데이트
121-
cacheMu.Lock()
122-
cachedData = &data
123-
cachedAt = time.Now()
124-
dataInitialized = true
125-
cacheMu.Unlock()
126-
12786
return &data, nil
12887
}
12988

@@ -302,37 +261,16 @@ func CalcOptimalSellLevel(currentGold int) int {
302261
return bestLevel
303262
}
304263

305-
// FetchOptimalSellData 서버에서 최적 판매 시점 데이터 가져오기
264+
// FetchOptimalSellData 서버에서 최적 판매 시점 데이터 가져오기 (매번 최신 데이터 요청)
306265
func FetchOptimalSellData() (*OptimalSellData, error) {
307-
optimalSellMu.RLock()
308-
if cachedOptimalSell != nil && time.Since(cachedOptimalSellAt) < optimalSellCacheExpiry {
309-
defer optimalSellMu.RUnlock()
310-
return cachedOptimalSell, nil
311-
}
312-
optimalSellMu.RUnlock()
313-
314-
// 서버에서 데이터 가져오기
315266
client := &http.Client{Timeout: 5 * time.Second}
316267
resp, err := client.Get(optimalSellEndpoint)
317268
if err != nil {
318-
// 캐시가 있으면 만료되어도 사용
319-
optimalSellMu.RLock()
320-
if cachedOptimalSell != nil {
321-
defer optimalSellMu.RUnlock()
322-
return cachedOptimalSell, nil
323-
}
324-
optimalSellMu.RUnlock()
325269
return nil, fmt.Errorf("서버 연결 실패: %v", err)
326270
}
327271
defer resp.Body.Close()
328272

329273
if resp.StatusCode != http.StatusOK {
330-
optimalSellMu.RLock()
331-
if cachedOptimalSell != nil {
332-
defer optimalSellMu.RUnlock()
333-
return cachedOptimalSell, nil
334-
}
335-
optimalSellMu.RUnlock()
336274
return nil, fmt.Errorf("서버 오류: %d", resp.StatusCode)
337275
}
338276

@@ -341,12 +279,6 @@ func FetchOptimalSellData() (*OptimalSellData, error) {
341279
return nil, fmt.Errorf("데이터 파싱 실패: %v", err)
342280
}
343281

344-
// 캐시 업데이트
345-
optimalSellMu.Lock()
346-
cachedOptimalSell = &data
347-
cachedOptimalSellAt = time.Now()
348-
optimalSellMu.Unlock()
349-
350282
return &data, nil
351283
}
352284

0 commit comments

Comments
 (0)