Skip to content

Commit f4836ac

Browse files
committed
feat: 타입 기반 텔레메트리 v3, 동적 목표 레벨 설정 (v2.8.0)
- RecordEnhanceWithType(): 타입+레벨별 강화 통계 기록 - RecordSaleWithType(): 타입+레벨별 판매 통계 기록 - 서버 by_type 응답 파싱 및 타입별 최적 레벨 조회 - loopGoldMine: 타입별 동적 목표 레벨 적용 (normal/special/trash) - 서버: extractTypeLevel(), handleOptimalSellPoint by_type 응답
1 parent 7994e5b commit f4836ac

File tree

7 files changed

+1359
-44
lines changed

7 files changed

+1359
-44
lines changed

cmd/sword-api/main.go

Lines changed: 273 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"log"
1010
"net/http"
1111
"os"
12+
"strconv"
13+
"strings"
1214
"sync"
1315
"time"
1416

@@ -346,6 +348,31 @@ var defaultSwordPrices = []SwordPrice{
346348
{Level: 15, MinPrice: 30000000, MaxPrice: 40000000, AvgPrice: 35000000},
347349
}
348350

351+
// extractTypeLevel 키에서 타입과 레벨 추출
352+
// 키 형식: "{type}_{level}" (예: "normal_10", "special_5", "trash_3")
353+
// 반환: (타입, 레벨, 성공여부)
354+
func extractTypeLevel(key string) (string, int, bool) {
355+
parts := strings.Split(key, "_")
356+
if len(parts) < 2 {
357+
return "", 0, false
358+
}
359+
360+
// 마지막 부분이 레벨 숫자
361+
levelStr := parts[len(parts)-1]
362+
level, err := strconv.Atoi(levelStr)
363+
if err != nil {
364+
return "", 0, false
365+
}
366+
367+
// 나머지가 타입 (normal, special, trash만 허용)
368+
itemType := strings.Join(parts[:len(parts)-1], "_")
369+
if itemType != "normal" && itemType != "special" && itemType != "trash" {
370+
return "", 0, false
371+
}
372+
373+
return itemType, level, true
374+
}
375+
349376
func getGameData() GameData {
350377
stats.mu.RLock()
351378
defer stats.mu.RUnlock()
@@ -383,9 +410,49 @@ func getGameData() GameData {
383410
}
384411
}
385412

413+
// 검 가격: 실측 판매 데이터 반영
414+
swordPrices := make([]SwordPrice, len(defaultSwordPrices))
415+
copy(swordPrices, defaultSwordPrices)
416+
417+
// swordSaleStats에서 레벨별 판매 통계 집계
418+
// 키 형식: "{검이름}_{레벨}" (예: "불꽃검_10", "검_8")
419+
levelSales := make(map[int]struct {
420+
totalPrice int
421+
count int
422+
})
423+
for key, stat := range stats.swordSaleStats {
424+
// 키에서 레벨 추출 (마지막 "_" 뒤의 숫자)
425+
parts := strings.Split(key, "_")
426+
if len(parts) < 2 {
427+
continue
428+
}
429+
levelStr := parts[len(parts)-1]
430+
level, err := strconv.Atoi(levelStr)
431+
if err != nil {
432+
continue
433+
}
434+
// 레벨별로 집계
435+
entry := levelSales[level]
436+
entry.totalPrice += stat.TotalPrice
437+
entry.count += stat.Count
438+
levelSales[level] = entry
439+
}
440+
441+
// 실측 평균 가격으로 대체 (minSampleSize 이상일 때만)
442+
for i := range swordPrices {
443+
lvl := swordPrices[i].Level
444+
if entry, ok := levelSales[lvl]; ok && entry.count >= minSampleSize {
445+
realAvgPrice := entry.totalPrice / entry.count
446+
swordPrices[i].AvgPrice = realAvgPrice
447+
// MinPrice, MaxPrice도 실측 기준으로 추정 (±20%)
448+
swordPrices[i].MinPrice = int(float64(realAvgPrice) * 0.8)
449+
swordPrices[i].MaxPrice = int(float64(realAvgPrice) * 1.2)
450+
}
451+
}
452+
386453
return GameData{
387454
EnhanceRates: enhanceRates,
388-
SwordPrices: defaultSwordPrices,
455+
SwordPrices: swordPrices,
389456
BattleRewards: battleRewards,
390457
UpdatedAt: time.Now().Format(time.RFC3339),
391458
}
@@ -920,12 +987,41 @@ func handleOptimalSellPoint(w http.ResponseWriter, r *http.Request) {
920987
}
921988

922989
// 예상 시간 계산 (초 단위)
923-
// 파밍 시간 + 강화 시간
924-
// 파밍: 약 3초, 강화: 약 2초/회
990+
// 실제 클라이언트 설정 기반:
991+
// - TrashDelay: 1.2초 (파밍/판매 후)
992+
// - LowDelay: 1.5초 (0-8강)
993+
// - MidDelay: 2.5초 (9강)
994+
// - HighDelay: 3.5초 (10강+)
995+
// + 응답 대기/처리 오버헤드: 약 1초
925996
calcExpectedTime := func(targetLevel int) float64 {
926-
farmTime := 3.0 // 아이템 파밍
927-
enhanceTime := calcExpectedTrials(targetLevel) * 2.0 // 강화당 2초
928-
return farmTime + enhanceTime
997+
const (
998+
farmTime = 1.2 // TrashDelay (판매 후 새 검 받기)
999+
lowDelay = 2.5 // LowDelay(1.5) + 응답대기(1.0)
1000+
midDelay = 3.5 // MidDelay(2.5) + 응답대기(1.0)
1001+
highDelay = 4.5 // HighDelay(3.5) + 응답대기(1.0)
1002+
slowdownLvl = 9 // SlowdownLevel
1003+
)
1004+
1005+
totalTime := farmTime
1006+
for lvl := 0; lvl < targetLevel && lvl < len(gameData.EnhanceRates); lvl++ {
1007+
rate := gameData.EnhanceRates[lvl].SuccessRate / 100.0
1008+
if rate <= 0 {
1009+
continue
1010+
}
1011+
expectedTries := 1.0 / rate
1012+
1013+
// 레벨별 딜레이 적용
1014+
var delay float64
1015+
if lvl >= 10 {
1016+
delay = highDelay
1017+
} else if lvl >= slowdownLvl {
1018+
delay = midDelay
1019+
} else {
1020+
delay = lowDelay
1021+
}
1022+
totalTime += expectedTries * delay
1023+
}
1024+
return totalTime
9291025
}
9301026

9311027
type LevelEfficiency struct {
@@ -984,10 +1080,181 @@ func handleOptimalSellPoint(w http.ResponseWriter, r *http.Request) {
9841080
}
9851081
}
9861082

1083+
// 타입별 판매가 집계 (normal_10, special_10 등에서 추출)
1084+
typeLevelPrices := make(map[string]map[int]struct {
1085+
totalPrice int
1086+
count int
1087+
})
1088+
for key, stat := range stats.swordSaleStats {
1089+
itemType, level, ok := extractTypeLevel(key)
1090+
if !ok {
1091+
continue
1092+
}
1093+
if typeLevelPrices[itemType] == nil {
1094+
typeLevelPrices[itemType] = make(map[int]struct {
1095+
totalPrice int
1096+
count int
1097+
})
1098+
}
1099+
entry := typeLevelPrices[itemType][level]
1100+
entry.totalPrice += stat.TotalPrice
1101+
entry.count += stat.Count
1102+
typeLevelPrices[itemType][level] = entry
1103+
}
1104+
1105+
// 타입별 강화 확률 집계 (normal_10, special_10 등에서 추출)
1106+
typeLevelEnhance := make(map[string]map[int]struct {
1107+
attempts int
1108+
success int
1109+
})
1110+
for key, stat := range stats.swordEnhanceStats {
1111+
itemType, level, ok := extractTypeLevel(key)
1112+
if !ok {
1113+
continue
1114+
}
1115+
if typeLevelEnhance[itemType] == nil {
1116+
typeLevelEnhance[itemType] = make(map[int]struct {
1117+
attempts int
1118+
success int
1119+
})
1120+
}
1121+
entry := typeLevelEnhance[itemType][level]
1122+
entry.attempts += stat.Attempts
1123+
entry.success += stat.Success
1124+
typeLevelEnhance[itemType][level] = entry
1125+
}
1126+
1127+
// 타입별 강화 성공률 계산 (샘플 부족 시 기본값 사용)
1128+
getEnhanceRateForType := func(itemType string, level int) float64 {
1129+
if typeData, ok := typeLevelEnhance[itemType]; ok {
1130+
if entry, ok := typeData[level]; ok && entry.attempts >= minSampleSize {
1131+
return float64(entry.success) / float64(entry.attempts)
1132+
}
1133+
}
1134+
// 기본값 사용
1135+
if level < len(gameData.EnhanceRates) {
1136+
return gameData.EnhanceRates[level].SuccessRate / 100.0
1137+
}
1138+
return 0.05 // 매우 낮은 기본값
1139+
}
1140+
1141+
// 타입별 평균 판매가 계산 (샘플 부족 시 기본값 사용)
1142+
getAvgPriceForType := func(itemType string, level int) int {
1143+
if typeData, ok := typeLevelPrices[itemType]; ok {
1144+
if entry, ok := typeData[level]; ok && entry.count >= minSampleSize {
1145+
return entry.totalPrice / entry.count
1146+
}
1147+
}
1148+
// 기본값 사용
1149+
if level < len(gameData.SwordPrices) {
1150+
return gameData.SwordPrices[level].AvgPrice
1151+
}
1152+
return 0
1153+
}
1154+
1155+
// 타입별 예상 시간 계산
1156+
calcExpectedTimeForType := func(itemType string, targetLevel int) float64 {
1157+
const (
1158+
farmTime = 1.2
1159+
lowDelay = 2.5
1160+
midDelay = 3.5
1161+
highDelay = 4.5
1162+
slowdownLvl = 9
1163+
)
1164+
1165+
totalTime := farmTime
1166+
for lvl := 0; lvl < targetLevel; lvl++ {
1167+
rate := getEnhanceRateForType(itemType, lvl)
1168+
if rate <= 0 {
1169+
continue
1170+
}
1171+
expectedTries := 1.0 / rate
1172+
1173+
var delay float64
1174+
if lvl >= 10 {
1175+
delay = highDelay
1176+
} else if lvl >= slowdownLvl {
1177+
delay = midDelay
1178+
} else {
1179+
delay = lowDelay
1180+
}
1181+
totalTime += expectedTries * delay
1182+
}
1183+
return totalTime
1184+
}
1185+
1186+
// 타입별 최적 레벨 계산
1187+
type TypeOptimal struct {
1188+
Type string `json:"type"`
1189+
OptimalLevel int `json:"optimal_level"`
1190+
OptimalGPM float64 `json:"optimal_gpm"`
1191+
SampleSize int `json:"sample_size"`
1192+
EnhanceSamples int `json:"enhance_samples"`
1193+
IsDefault bool `json:"is_default"`
1194+
}
1195+
1196+
calcTypeOptimal := func(itemType string) TypeOptimal {
1197+
bestLvl := 10
1198+
bestGpm := 0.0
1199+
totalSales := 0
1200+
totalEnhance := 0
1201+
1202+
// 해당 타입의 총 샘플 수 계산
1203+
if typeData, ok := typeLevelPrices[itemType]; ok {
1204+
for _, entry := range typeData {
1205+
totalSales += entry.count
1206+
}
1207+
}
1208+
if typeData, ok := typeLevelEnhance[itemType]; ok {
1209+
for _, entry := range typeData {
1210+
totalEnhance += entry.attempts
1211+
}
1212+
}
1213+
1214+
isDefault := totalSales < minSampleSize || totalEnhance < minSampleSize
1215+
1216+
for level := 5; level <= 15; level++ {
1217+
price := getAvgPriceForType(itemType, level)
1218+
timeSeconds := calcExpectedTimeForType(itemType, level)
1219+
1220+
// 성공 확률 계산
1221+
successProb := 1.0
1222+
for lvl := 0; lvl < level; lvl++ {
1223+
successProb *= getEnhanceRateForType(itemType, lvl)
1224+
}
1225+
1226+
gpm := 0.0
1227+
if timeSeconds > 0 {
1228+
gpm = (float64(price) * successProb) / (timeSeconds / 60.0)
1229+
}
1230+
1231+
if gpm > bestGpm {
1232+
bestGpm = gpm
1233+
bestLvl = level
1234+
}
1235+
}
1236+
1237+
return TypeOptimal{
1238+
Type: itemType,
1239+
OptimalLevel: bestLvl,
1240+
OptimalGPM: bestGpm,
1241+
SampleSize: totalSales,
1242+
EnhanceSamples: totalEnhance,
1243+
IsDefault: isDefault,
1244+
}
1245+
}
1246+
1247+
typeOptimalLevels := map[string]TypeOptimal{
1248+
"normal": calcTypeOptimal("normal"),
1249+
"special": calcTypeOptimal("special"),
1250+
"trash": calcTypeOptimal("trash"),
1251+
}
1252+
9871253
json.NewEncoder(w).Encode(map[string]interface{}{
9881254
"optimal_level": bestLevel,
9891255
"optimal_gpm": bestGPM,
9901256
"level_efficiencies": efficiencies,
1257+
"by_type": typeOptimalLevels,
9911258
"note": "gold_per_minute = (avg_price × success_prob) / (expected_time / 60)",
9921259
})
9931260
}

cmd/sword-macro/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func init() {
1919
runtime.LockOSThread()
2020
}
2121

22-
const VERSION = "2.7.3"
22+
const VERSION = "2.8.0"
2323

2424
func main() {
2525
// Windows 콘솔 ANSI 지원 활성화 및 UTF-8 설정

0 commit comments

Comments
 (0)