Skip to content

Commit c7088b5

Browse files
committed
feat: 프로필 분석 대폭 개선 + 버그 수정 (v2.8.6)
- 프로필 분석: 최적 판매 레벨 추천, GPM 효율 테이블, 추천 액션 등 추가 - 프로필 분석: 타입별 최적 레벨, 최고 기록, 샘플 수 신뢰도 표시 - 특수 모드: 골드 부족 시 무한 루프 버그 수정 - 특수 모드: 레벨 0 판매 시도 방지 - 설정: 미사용 옵션 제거 (GoldMineTarget, MinGold, BattleMinGold)
1 parent 5f67b4a commit c7088b5

File tree

4 files changed

+235
-39
lines changed

4 files changed

+235
-39
lines changed

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.8.5"
22+
const VERSION = "2.8.6"
2323

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

internal/config/config.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,9 @@ type Config struct {
2222
HighDelay float64 `json:"high_delay"`
2323
SlowdownLevel int `json:"slowdown_level"`
2424

25-
// 게임 설정
26-
GoldMineTarget int `json:"gold_mine_target"`
27-
MinGold int `json:"min_gold"`
28-
2925
// 배틀 설정
30-
BattleLevelDiff int `json:"battle_level_diff"` // 역배 레벨 차이 (1-3)
26+
BattleLevelDiff int `json:"battle_level_diff"` // 역배 레벨 차이 (1-20)
3127
BattleCooldown float64 `json:"battle_cooldown"` // 배틀 간 쿨다운 (초)
32-
BattleMinGold int `json:"battle_min_gold"` // 최소 보유 골드 (이하면 중단)
3328

3429
// 클립보드 텍스트 읽기
3530
ChatOffsetY int `json:"chat_offset_y"` // 입력창에서 채팅 영역까지 거리 (픽셀)
@@ -52,11 +47,8 @@ func Default() *Config {
5247
MidDelay: 2.5,
5348
HighDelay: 3.5,
5449
SlowdownLevel: 9,
55-
GoldMineTarget: 10,
56-
MinGold: 0,
5750
BattleLevelDiff: 2,
5851
BattleCooldown: 5.0,
59-
BattleMinGold: 1000,
6052
ChatOffsetY: 40, // 입력창 클릭 좌표에서 40픽셀 위 (채팅 영역 클릭용)
6153
// 오버레이 기본값
6254
OverlayChatWidth: 380,

internal/game/engine.go

Lines changed: 91 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -934,13 +934,19 @@ func (e *Engine) loopSpecial() {
934934
overlay.UpdateStatus("⭐ 특수 강화 완료!\n[%s] +%d", itemName, result.FinalLevel)
935935
e.telem.TrySend()
936936
return // 목표 달성 → 종료
937-
} else {
937+
} else if result.Destroyed {
938938
// 파괴됨 → 다시 특수 아이템 찾기
939939
fmt.Printf("💥 강화 중 파괴됨 (최종 레벨: +%d) → 다시 특수 아이템 찾기\n", result.FinalLevel)
940940
overlay.UpdateStatus("💥 특수 파괴됨\n다시 특수 찾는 중...")
941941
e.telem.TrySend()
942942
time.Sleep(time.Duration(e.cfg.TrashDelay * float64(time.Second)))
943943
continue // 루프 계속 → 특수 아이템 다시 찾기
944+
} else {
945+
// 골드 부족 또는 사용자 중지 → 종료 (무한 루프 방지)
946+
fmt.Printf("⚠️ 강화 중단됨 (레벨: +%d) → 종료\n", result.FinalLevel)
947+
overlay.UpdateStatus("⚠️ 강화 중단\n[%s] +%d", itemName, result.FinalLevel)
948+
e.telem.TrySend()
949+
return
944950
}
945951
} else {
946952
// 강화 목표 없으면 (보관만) 바로 종료
@@ -954,14 +960,23 @@ func (e *Engine) loopSpecial() {
954960
// 4. 쓰레기/일반/미판별이면 /판매로 새 아이템 받기 (v3 변경점)
955961
// "none"도 포함: 타입 판별 실패 시 계속 강화하면 안되므로 판매 처리
956962
if state.ItemType == "trash" || state.ItemType == "normal" || state.ItemType == "unknown" || state.ItemType == "none" {
963+
// 현재 레벨 추출
964+
saleLevel := e.ExtractCurrentLevel(state)
965+
966+
// 레벨 0은 판매 불가 → /강화 재시도 필요
967+
if saleLevel == 0 {
968+
fmt.Printf(" ⚠️ +0 상태 → 판매 불가, 강화 재시도\n")
969+
overlay.UpdateStatus("⭐ 특수 아이템 뽑기\n⚠️ +0 판매 불가\n강화 재시도...")
970+
time.Sleep(time.Duration(e.cfg.TrashDelay * float64(time.Second)))
971+
continue
972+
}
973+
957974
e.telem.RecordFarmingWithItem(itemName, state.ItemType)
958975
e.sessionStats.trashCount++
959976
displayName := itemName
960977
if displayName == "" {
961978
displayName = GetItemTypeLabel(state.ItemType)
962979
}
963-
// 현재 레벨 추출 (판매 통계용)
964-
saleLevel := e.ExtractCurrentLevel(state)
965980
overlay.UpdateStatus("⭐ 특수 아이템 뽑기\n쓰레기: %d회\n🗑️ %s\n\n📋 판단: %s → 판매", e.sessionStats.trashCount, displayName, GetItemTypeLabel(state.ItemType))
966981
fmt.Printf(" 🗑️ [%s] → /판매\n", displayName)
967982

@@ -978,10 +993,19 @@ func (e *Engine) loopSpecial() {
978993
}
979994

980995
// 5. 예상치 못한 타입 - 안전하게 판매 처리 (무한 강화 방지)
981-
fmt.Printf(" ❓ 예상치 못한 아이템 타입: [%s] - 판매 처리\n", state.ItemType)
982-
overlay.UpdateStatus("⭐ 특수 아이템 뽑기\n❓ 타입 불명 → 판매")
983996
// 현재 레벨 추출 (판매 통계용)
984997
unknownSaleLevel := e.ExtractCurrentLevel(state)
998+
999+
// 레벨 0은 판매 불가 → /강화 재시도 필요
1000+
if unknownSaleLevel == 0 {
1001+
fmt.Printf(" ⚠️ +0 상태 → 판매 불가, 강화 재시도\n")
1002+
overlay.UpdateStatus("⭐ 특수 아이템 뽑기\n⚠️ +0 판매 불가\n강화 재시도...")
1003+
time.Sleep(time.Duration(e.cfg.TrashDelay * float64(time.Second)))
1004+
continue
1005+
}
1006+
1007+
fmt.Printf(" ❓ 예상치 못한 아이템 타입: [%s] - 판매 처리\n", state.ItemType)
1008+
overlay.UpdateStatus("⭐ 특수 아이템 뽑기\n❓ 타입 불명 → 판매")
9851009
e.sendCommand("/판매")
9861010
// 판매 응답 대기
9871011
unknownSaleText := e.readChatTextWaitForChange(5 * time.Second)
@@ -2158,10 +2182,8 @@ func (e *Engine) showSettings(reader *bufio.Reader) {
21582182
fmt.Printf("2. 중간 속도: %.1f초\n", e.cfg.MidDelay)
21592183
fmt.Printf("3. 고강 속도: %.1f초\n", e.cfg.HighDelay)
21602184
fmt.Printf("4. 좌표 고정: %v\n", e.cfg.LockXY)
2161-
fmt.Printf("5. 골드 채굴 목표: +%d\n", e.cfg.GoldMineTarget)
2162-
fmt.Printf("6. 배틀 역배 레벨차: %d\n", e.cfg.BattleLevelDiff)
2163-
fmt.Printf("7. 배틀 쿨다운: %.1f초\n", e.cfg.BattleCooldown)
2164-
fmt.Printf("8. 배틀 최소 골드: %dG\n", e.cfg.BattleMinGold)
2185+
fmt.Printf("5. 배틀 역배 레벨차: %d\n", e.cfg.BattleLevelDiff)
2186+
fmt.Printf("6. 배틀 쿨다운: %.1f초\n", e.cfg.BattleCooldown)
21652187
fmt.Println("0. 돌아가기")
21662188
fmt.Print("선택: ")
21672189

@@ -2191,29 +2213,17 @@ func (e *Engine) showSettings(reader *bufio.Reader) {
21912213
e.cfg.LockXY = !e.cfg.LockXY
21922214
fmt.Printf("좌표 고정: %v\n", e.cfg.LockXY)
21932215
case "5":
2194-
fmt.Print("골드 채굴 목표 레벨 (1-20): ")
2195-
val, _ := reader.ReadString('\n')
2196-
if v, err := strconv.Atoi(strings.TrimSpace(val)); err == nil && v >= 1 && v <= 20 {
2197-
e.cfg.GoldMineTarget = v
2198-
}
2199-
case "6":
22002216
fmt.Print("배틀 역배 레벨차 (1-20): ")
22012217
val, _ := reader.ReadString('\n')
22022218
if v, err := strconv.Atoi(strings.TrimSpace(val)); err == nil && v >= 1 && v <= 20 {
22032219
e.cfg.BattleLevelDiff = v
22042220
}
2205-
case "7":
2221+
case "6":
22062222
fmt.Print("배틀 쿨다운 (초): ")
22072223
val, _ := reader.ReadString('\n')
22082224
if v, err := strconv.ParseFloat(strings.TrimSpace(val), 64); err == nil && v > 0 {
22092225
e.cfg.BattleCooldown = v
22102226
}
2211-
case "8":
2212-
fmt.Print("배틀 최소 골드: ")
2213-
val, _ := reader.ReadString('\n')
2214-
if v, err := strconv.Atoi(strings.TrimSpace(val)); err == nil && v >= 0 {
2215-
e.cfg.BattleMinGold = v
2216-
}
22172227
case "0":
22182228
e.cfg.Save()
22192229
return
@@ -2315,14 +2325,32 @@ func (e *Engine) showMyProfile() {
23152325
fmt.Println()
23162326
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
23172327

2328+
// 아이템 타입 분석
2329+
itemType := "normal"
2330+
if profile.SwordName != "" {
2331+
itemType = DetermineItemType(profile.SwordName)
2332+
}
2333+
itemTypeKr := map[string]string{"normal": "일반", "special": "특수", "trash": "쓰레기"}[itemType]
2334+
if itemTypeKr == "" {
2335+
itemTypeKr = "일반"
2336+
}
2337+
23182338
// 1. 내 검 정보
23192339
fmt.Println("⚔️ 내 검 정보")
23202340
fmt.Printf(" 이름: %s\n", profile.Name)
23212341
if profile.SwordName != "" {
2322-
fmt.Printf(" 보유 검: [+%d] %s\n", profile.Level, profile.SwordName)
2342+
fmt.Printf(" 보유 검: [+%d] %s (%s)\n", profile.Level, profile.SwordName, itemTypeKr)
23232343
} else {
23242344
fmt.Printf(" 보유 검: +%d\n", profile.Level)
23252345
}
2346+
// 최고 기록 표시
2347+
if profile.BestLevel > 0 {
2348+
if profile.BestSword != "" {
2349+
fmt.Printf(" 최고 기록: [+%d] %s\n", profile.BestLevel, profile.BestSword)
2350+
} else {
2351+
fmt.Printf(" 최고 기록: +%d\n", profile.BestLevel)
2352+
}
2353+
}
23262354
fmt.Printf(" 전적: %d승 %d패\n", profile.Wins, profile.Losses)
23272355
if profile.Gold > 0 {
23282356
fmt.Printf(" 보유 골드: %sG\n", FormatGold(profile.Gold))
@@ -2333,23 +2361,57 @@ func (e *Engine) showMyProfile() {
23332361
fmt.Println("💰 예상 판매가")
23342362
price := GetSwordPrice(profile.Level)
23352363
if price != nil {
2336-
fmt.Printf(" 최소: %sG\n", FormatGold(price.MinPrice))
2337-
fmt.Printf(" 평균: %sG\n", FormatGold(price.AvgPrice))
2338-
fmt.Printf(" 최대: %sG\n", FormatGold(price.MaxPrice))
2364+
fmt.Printf(" 현재 +%d강: %sG (최소 %sG ~ 최대 %sG)\n",
2365+
profile.Level, FormatGold(price.AvgPrice), FormatGold(price.MinPrice), FormatGold(price.MaxPrice))
23392366
} else {
23402367
fmt.Println(" 데이터 없음")
23412368
}
23422369
fmt.Println()
23432370

2344-
// 3. 강화 확률표
2371+
// 3. 최적 판매 추천 (서버 API 활용)
2372+
fmt.Println("🎯 최적 판매 추천")
2373+
optimalLevel, optSource := GetOptimalSellLevel(profile.Gold)
2374+
fmt.Printf(" 전체 최적: +%d강 (%s)\n", optimalLevel, optSource)
2375+
2376+
// 타입별 최적 레벨
2377+
typeOptLevel, isDefault := GetOptimalLevelByType(itemType)
2378+
if isDefault {
2379+
fmt.Printf(" %s 검 최적: +%d강 (기본값)\n", itemTypeKr, typeOptLevel)
2380+
} else {
2381+
fmt.Printf(" %s 검 최적: +%d강 (실측 데이터)\n", itemTypeKr, typeOptLevel)
2382+
}
2383+
2384+
// 현재 레벨과 최적 레벨 비교 추천
2385+
if profile.Level < typeOptLevel {
2386+
nextPrice := GetSwordPrice(typeOptLevel)
2387+
if nextPrice != nil && price != nil {
2388+
profit := nextPrice.AvgPrice - price.AvgPrice
2389+
fmt.Printf(" 💡 +%d강까지 강화 추천 (예상 추가 수익: +%sG)\n", typeOptLevel, FormatGold(profit))
2390+
} else {
2391+
fmt.Printf(" 💡 +%d강까지 강화 추천\n", typeOptLevel)
2392+
}
2393+
} else if profile.Level == typeOptLevel {
2394+
fmt.Println(" ✅ 현재 최적 판매 레벨입니다!")
2395+
} else {
2396+
fmt.Printf(" ⚠️ 최적 레벨(+%d) 초과 - 리스크 관리 필요\n", typeOptLevel)
2397+
}
2398+
fmt.Println()
2399+
2400+
// 4. 레벨별 효율 분석 (GPM 테이블)
2401+
PrintLevelEfficiencyTable(profile.Level, itemType)
2402+
2403+
// 5. 강화 확률표
23452404
PrintEnhanceRateTable(profile.Level)
23462405

2347-
// 4. 목표별 성공 확률
2406+
// 6. 목표별 성공 확률
23482407
PrintTargetSuccessChance(profile.Level)
23492408

2350-
// 5. 역배 기대값
2409+
// 7. 역배 기대값
23512410
PrintUpsetAnalysis(profile.Level, profile.Gold)
23522411

2412+
// 8. 추천 액션 요약
2413+
PrintRecommendedActions(profile.Level, profile.Gold, itemType, typeOptLevel)
2414+
23532415
fmt.Println()
23542416
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
23552417
fmt.Println()

internal/game/helpers.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,148 @@ func PrintUpsetAnalysis(level, gold int) {
447447
fmt.Printf(" 💡 배팅 기준: %sG (보유 골드의 10%%)\n", FormatGold(betAmount))
448448
}
449449

450+
// PrintLevelEfficiencyTable 레벨별 효율 분석 테이블 출력 (GPM 포함)
451+
// currentLevel: 현재 레벨, itemType: 아이템 타입 ("normal", "special", "trash")
452+
func PrintLevelEfficiencyTable(currentLevel int, itemType string) {
453+
fmt.Println("📈 레벨별 효율 분석 (GPM = 분당 골드)")
454+
455+
// 타입별 효율 데이터 조회
456+
typeEffs := GetLevelEfficienciesByType(itemType)
457+
if typeEffs == nil || len(typeEffs) == 0 {
458+
// 타입별 데이터 없으면 전체 데이터 사용
459+
allEffs := GetAllLevelEfficiencies()
460+
if allEffs == nil || len(allEffs) == 0 {
461+
fmt.Println(" 데이터 없음 (서버 연결 확인)")
462+
fmt.Println()
463+
return
464+
}
465+
466+
fmt.Println(" 레벨 | 성공확률 | 평균 판매가 | GPM | 추천")
467+
fmt.Println(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
468+
469+
for _, eff := range allEffs {
470+
marker := " "
471+
if eff.Level == currentLevel {
472+
marker = "▶ "
473+
}
474+
recommend := ""
475+
if eff.Recommendation == "optimal" {
476+
recommend = "⭐ 최적"
477+
}
478+
fmt.Printf(" %s+%2d | %5.2f%% | %10sG | %7.1f | %s\n",
479+
marker, eff.Level, eff.SuccessProb, FormatGold(eff.AvgPrice), eff.GoldPerMinute, recommend)
480+
}
481+
} else {
482+
// 타입별 데이터 출력 (샘플 수 포함)
483+
fmt.Println(" 레벨 | 성공확률 | 평균 판매가 | GPM | 샘플 | 추천")
484+
fmt.Println(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
485+
486+
for _, eff := range typeEffs {
487+
marker := " "
488+
if eff.Level == currentLevel {
489+
marker = "▶ "
490+
}
491+
recommend := ""
492+
if eff.Recommendation == "optimal" {
493+
recommend = "⭐ 최적"
494+
}
495+
sampleStr := fmt.Sprintf("%4d", eff.SampleSize)
496+
if eff.SampleSize < 10 {
497+
sampleStr = fmt.Sprintf("%4d⚠", eff.SampleSize) // 샘플 적음 경고
498+
}
499+
fmt.Printf(" %s+%2d | %5.2f%% | %10sG | %7.1f | %s | %s\n",
500+
marker, eff.Level, eff.SuccessProb, FormatGold(eff.AvgPrice), eff.GoldPerMinute, sampleStr, recommend)
501+
}
502+
}
503+
fmt.Println()
504+
}
505+
506+
// PrintRecommendedActions 추천 액션 요약 출력
507+
// level: 현재 레벨, gold: 보유 골드, itemType: 아이템 타입, optimalLevel: 최적 판매 레벨
508+
func PrintRecommendedActions(level, gold int, itemType string, optimalLevel int) {
509+
fmt.Println("🎯 추천 액션")
510+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
511+
512+
itemTypeKr := map[string]string{"normal": "일반", "special": "특수", "trash": "쓰레기"}[itemType]
513+
if itemTypeKr == "" {
514+
itemTypeKr = "일반"
515+
}
516+
517+
currentPrice := GetSwordPrice(level)
518+
optimalPrice := GetSwordPrice(optimalLevel)
519+
currentPriceVal := 0
520+
if currentPrice != nil {
521+
currentPriceVal = currentPrice.AvgPrice
522+
}
523+
524+
actionNum := 1
525+
526+
// 1. 강화 추천 여부
527+
if level < optimalLevel {
528+
successChance := CalcEnhanceSuccessChance(level, optimalLevel)
529+
trials := CalcExpectedTrials(level, optimalLevel)
530+
expectedProfit := 0
531+
if optimalPrice != nil {
532+
expectedProfit = optimalPrice.AvgPrice - currentPriceVal
533+
}
534+
fmt.Printf("%d. [강화 추천] +%d강 → +%d강\n", actionNum, level, optimalLevel)
535+
fmt.Printf(" └─ 성공률 %.2f%%, 평균 %.0f회 시도\n", successChance, trials)
536+
if expectedProfit > 0 {
537+
fmt.Printf(" └─ 성공 시 추가 수익: +%sG\n", FormatGold(expectedProfit))
538+
}
539+
actionNum++
540+
} else if level == optimalLevel {
541+
fmt.Printf("%d. [판매 추천] 현재 +%d강 = 최적 판매 레벨\n", actionNum, level)
542+
fmt.Printf(" └─ 예상 판매가: %sG\n", FormatGold(currentPriceVal))
543+
actionNum++
544+
} else {
545+
// 최적 레벨 초과
546+
fmt.Printf("%d. [판매 권장] 최적 레벨(+%d) 초과, 리스크 관리 필요\n", actionNum, optimalLevel)
547+
fmt.Printf(" └─ 현재 판매가: %sG (고점 판매 고려)\n", FormatGold(currentPriceVal))
548+
actionNum++
549+
}
550+
551+
// 2. 배틀 추천 여부
552+
if gold >= 100 && level > 0 {
553+
// 역배 기대값 계산 (레벨차 1~2)
554+
betAmount := gold / 10
555+
if betAmount < 100 {
556+
betAmount = 100
557+
}
558+
bestDiff := 0
559+
bestEV := float64(-9999)
560+
for diff := 1; diff <= 2; diff++ {
561+
ev, _, _ := CalcUpsetExpectedValue(level, level+diff, betAmount)
562+
if ev > bestEV {
563+
bestEV = ev
564+
bestDiff = diff
565+
}
566+
}
567+
if bestEV > 0 {
568+
_, winRate, avgReward := CalcUpsetExpectedValue(level, level+bestDiff, betAmount)
569+
fmt.Printf("%d. [배틀 추천] 역배 +%d (상대 +%d강)\n", actionNum, bestDiff, level+bestDiff)
570+
fmt.Printf(" └─ 승률 %.0f%%, 보상 %sG, 기대값 +%.0fG\n", winRate, FormatGold(avgReward), bestEV)
571+
actionNum++
572+
} else if gold >= 500 {
573+
fmt.Printf("%d. [배틀 가능] 역배 기대값 음수 - 신중하게 판단\n", actionNum)
574+
actionNum++
575+
}
576+
}
577+
578+
// 3. 특수 아이템인 경우 보관 추천
579+
if itemType == "special" {
580+
fmt.Printf("%d. [보관 추천] 특수 아이템 - 판매보다 보관 고려\n", actionNum)
581+
actionNum++
582+
}
583+
584+
// 4. 골드 부족 경고
585+
if gold < 100 && gold >= 0 {
586+
fmt.Printf("%d. [주의] 골드 부족 (%sG) - 강화/배틀 제한됨\n", actionNum, FormatGold(gold))
587+
}
588+
589+
fmt.Println()
590+
}
591+
450592
// =============================================================================
451593
// 배틀 관련 헬퍼
452594
// =============================================================================

0 commit comments

Comments
 (0)