Skip to content

Commit 4aa0a59

Browse files
committed
Move analysis stuff to it's own namespace
1 parent 3bc00df commit 4aa0a59

File tree

5 files changed

+550
-545
lines changed

5 files changed

+550
-545
lines changed

Motely.Tests/AnalyzerUnitTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Motely.Analysis;
12
using static VerifyXunit.Verifier;
23

34
namespace Motely.Tests;
@@ -39,7 +40,7 @@ await Verify(actualOutput)
3940

4041
private string GetAnalyzerOutput(string seed)
4142
{
42-
return SeedAnalyzer.Analyze(new(seed, MotelyDeck.Red, MotelyStake.White)).ToString();
43+
return MotelySeedAnalyzer.Analyze(new(seed, MotelyDeck.Red, MotelyStake.White)).ToString();
4344
}
4445

4546
// This method is now only used by other tests that don't use Verify yet
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
2+
using System.ComponentModel;
3+
4+
namespace Motely.Analysis;
5+
6+
/// <summary>
7+
/// Filter descriptor for seed analysis
8+
/// </summary>
9+
public sealed class MotelyAnalyzerFilterDesc() : IMotelySeedFilterDesc<MotelyAnalyzerFilterDesc.AnalyzerFilter>
10+
{
11+
public MotelySeedAnalysis? LastAnalysis { get; private set; } = null;
12+
13+
public AnalyzerFilter CreateFilter(ref MotelyFilterCreationContext ctx)
14+
{
15+
return new AnalyzerFilter(this);
16+
}
17+
18+
public readonly struct AnalyzerFilter(MotelyAnalyzerFilterDesc filterDesc) : IMotelySeedFilter
19+
{
20+
21+
public MotelyAnalyzerFilterDesc FilterDesc { get; } = filterDesc;
22+
23+
public readonly VectorMask Filter(ref MotelyVectorSearchContext ctx)
24+
{
25+
return ctx.SearchIndividualSeeds(CheckSeed);
26+
}
27+
28+
private ref struct AnteAnalysisState
29+
{
30+
public MotelySingleTarotStream ArcanaStream;
31+
public readonly bool HasArcanaStream => !ArcanaStream.IsNull;
32+
public MotelySinglePlanetStream CelestialStream;
33+
public readonly bool HasCelestialStream => !CelestialStream.IsNull;
34+
public MotelySingleSpectralStream SpectralStream;
35+
public readonly bool HasSpectralStream => !SpectralStream.IsNull;
36+
public MotelySingleStandardCardStream StandardStream;
37+
public readonly bool HasStandardStream => !StandardStream.IsInvalid;
38+
public MotelySingleJokerStream BuffoonStream;
39+
public readonly bool HasBuffoonStream => !BuffoonStream.IsNull;
40+
}
41+
42+
public readonly bool CheckSeed(ref MotelySingleSearchContext ctx)
43+
{
44+
// Create voucher state to track activated vouchers across antes
45+
MotelyRunState voucherState = new();
46+
47+
List<MotelyAnteAnalysis> antes = [];
48+
49+
// Analyze each ante
50+
for (int ante = 1; ante <= 8; ante++)
51+
{
52+
53+
AnteAnalysisState state = new()
54+
{
55+
ArcanaStream = default,
56+
CelestialStream = default,
57+
SpectralStream = default,
58+
StandardStream = MotelySingleStandardCardStream.Invalid,
59+
BuffoonStream = default
60+
};
61+
62+
// Boss
63+
MotelyBossBlind boss = ctx.GetBossForAnte(ante);
64+
65+
// Voucher - get with state for proper progression
66+
MotelyVoucher voucher = ctx.GetAnteFirstVoucher(ante, voucherState);
67+
68+
// TEST: Activate ALL vouchers from ante 1 onwards
69+
// if (ShouldActivateVoucher(voucher))
70+
voucherState.ActivateVoucher(voucher);
71+
72+
// Tags
73+
MotelySingleTagStream tagStream = ctx.CreateTagStream(ante);
74+
75+
MotelyTag smallTag = ctx.GetNextTag(ref tagStream);
76+
MotelyTag bigTag = ctx.GetNextTag(ref tagStream);
77+
78+
// Shop Queue
79+
MotelySingleShopItemStream shopStream = ctx.CreateShopItemStream(ante);
80+
81+
int maxSlots = ante == 1 ? 15 : 50;
82+
MotelyItem[] shopItems = new MotelyItem[maxSlots];
83+
84+
for (int i = 0; i < maxSlots; i++)
85+
{
86+
shopItems[i] = ctx.GetNextShopItem(ref shopStream);
87+
}
88+
89+
// Packs - Get the actual shop packs (not tag-generated ones)
90+
// Balatro generates 2 base shop packs, then tags can add more up to 4 in ante 1 or 6 in other antes
91+
var packStream = ctx.CreateBoosterPackStream(ante);
92+
93+
int maxPacks = ante == 1 ? 4 : 6;
94+
MotelyBoosterPackAnalysis[] packs = new MotelyBoosterPackAnalysis[maxPacks];
95+
96+
// Get all packs up to the maximum
97+
for (int i = 0; i < maxPacks; i++)
98+
{
99+
MotelyBoosterPack pack = ctx.GetNextBoosterPack(ref packStream);
100+
MotelySingleItemSet packContent = GetPackContents(ref ctx, ante, pack, ref state);
101+
102+
packs[i] = new(pack, packContent.AsArray());
103+
}
104+
105+
antes.Add(new(
106+
ante,
107+
boss,
108+
voucher,
109+
smallTag,
110+
bigTag,
111+
shopItems,
112+
packs
113+
));
114+
}
115+
116+
FilterDesc.LastAnalysis = new(null, antes);
117+
118+
return false; // Always return false since we're just analyzing
119+
}
120+
121+
private static bool ShouldActivateVoucher(MotelyVoucher voucher)
122+
{
123+
// Match Immolate's banned vouchers list
124+
// Magic Trick should be activated (it upgrades to Illusion)
125+
return voucher != MotelyVoucher.Illusion &&
126+
voucher != MotelyVoucher.TarotTycoon &&
127+
voucher != MotelyVoucher.TarotMerchant &&
128+
voucher != MotelyVoucher.PlanetTycoon &&
129+
voucher != MotelyVoucher.PlanetMerchant;
130+
}
131+
132+
133+
private static MotelySingleItemSet GetPackContents(
134+
ref MotelySingleSearchContext ctx, int ante, MotelyBoosterPack pack, ref AnteAnalysisState state
135+
)
136+
{
137+
var packType = pack.GetPackType();
138+
var packSize = pack.GetPackSize();
139+
140+
switch (packType)
141+
{
142+
case MotelyBoosterPackType.Arcana:
143+
144+
if (!state.HasArcanaStream)
145+
state.ArcanaStream = ctx.CreateArcanaPackTarotStream(ante);
146+
147+
return ctx.GetNextArcanaPackContents(ref state.ArcanaStream, packSize);
148+
149+
case MotelyBoosterPackType.Celestial:
150+
151+
if (!state.HasCelestialStream)
152+
state.CelestialStream = ctx.CreateCelestialPackPlanetStream(ante);
153+
154+
return ctx.GetNextCelestialPackContents(ref state.CelestialStream, packSize);
155+
156+
case MotelyBoosterPackType.Spectral:
157+
158+
if (!state.HasSpectralStream)
159+
state.SpectralStream = ctx.CreateSpectralPackSpectralStream(ante);
160+
161+
return ctx.GetNextSpectralPackContents(ref state.SpectralStream, packSize);
162+
163+
case MotelyBoosterPackType.Buffoon:
164+
165+
if (!state.HasBuffoonStream)
166+
state.BuffoonStream = ctx.CreateBuffoonPackJokerStream(ante);
167+
168+
return ctx.GetNextBuffoonPackContents(ref state.BuffoonStream, packSize);
169+
170+
case MotelyBoosterPackType.Standard:
171+
172+
if (!state.HasStandardStream)
173+
state.StandardStream = ctx.CreateStandardPackCardStream(ante);
174+
175+
return ctx.GetNextStandardPackContents(ref state.StandardStream, packSize);
176+
177+
default:
178+
throw new InvalidEnumArgumentException();
179+
}
180+
}
181+
}
182+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
4+
namespace Motely.Analysis;
5+
6+
public sealed record class MotelySeedAnalysisConfig
7+
(
8+
string Seed,
9+
MotelyDeck Deck,
10+
MotelyStake Stake
11+
);
12+
13+
/// <summary>
14+
/// Contains all analysis data for a seed
15+
/// </summary>
16+
public sealed record class MotelySeedAnalysis
17+
(
18+
string? Error,
19+
IReadOnlyList<MotelyAnteAnalysis> Antes
20+
)
21+
{
22+
public override string ToString()
23+
{
24+
if (!string.IsNullOrEmpty(Error))
25+
{
26+
return $"❌ Error analyzing seed: {Error}";
27+
}
28+
29+
StringBuilder sb = new();
30+
31+
// Match TheSoul's format exactly
32+
foreach (var ante in Antes)
33+
{
34+
sb.AppendLine($"==ANTE {ante.Ante}==");
35+
sb.AppendLine($"Boss: {FormatUtils.FormatBoss(ante.Boss)}");
36+
sb.AppendLine($"Voucher: {FormatUtils.FormatVoucher(ante.Voucher)}");
37+
38+
// Tags
39+
sb.AppendLine($"Tags: {FormatUtils.FormatTag(ante.SmallBlindTag)}, {FormatUtils.FormatTag(ante.BigBlindTag)}");
40+
41+
// Shop Queue - match TheSoul format exactly: "Shop Queue: " on its own line, then numbered items
42+
sb.AppendLine("Shop Queue: ");
43+
foreach ((int i, MotelyItem item) in ante.ShopQueue.Index())
44+
{
45+
sb.AppendLine($"{i + 1}) {FormatUtils.FormatItem(item)}");
46+
}
47+
sb.AppendLine();
48+
49+
// Packs - match Immolate format exactly: "Pack Name - Card1, Card2, Card3"
50+
sb.AppendLine("Packs: ");
51+
foreach (var pack in ante.Packs)
52+
{
53+
// Format: "Pack Name - Card1, Card2, Card3"
54+
var contents = pack.Items.Count > 0
55+
? " - " + string.Join(", ", pack.Items.Select(item => FormatUtils.FormatItem(item)))
56+
: "";
57+
sb.AppendLine($"{FormatUtils.FormatPackName(pack.Type)}{contents}");
58+
}
59+
sb.AppendLine();
60+
}
61+
62+
return sb.ToString();
63+
}
64+
}
65+
66+
public sealed record class MotelyAnteAnalysis
67+
(
68+
int Ante,
69+
MotelyBossBlind Boss,
70+
MotelyVoucher Voucher,
71+
MotelyTag SmallBlindTag,
72+
MotelyTag BigBlindTag,
73+
IReadOnlyList<MotelyItem> ShopQueue,
74+
IReadOnlyList<MotelyBoosterPackAnalysis> Packs
75+
);
76+
77+
public sealed record class MotelyBoosterPackAnalysis
78+
(
79+
MotelyBoosterPack Type,
80+
IReadOnlyList<MotelyItem> Items
81+
);
82+
83+
/// <summary>
84+
/// Consolidated seed analyzer that captures seed data and provides various output formats
85+
/// </summary>
86+
public static partial class MotelySeedAnalyzer
87+
{
88+
/// <summary>
89+
/// Analyzes a seed and returns structured data
90+
/// </summary>
91+
public static MotelySeedAnalysis Analyze(MotelySeedAnalysisConfig cfg)
92+
{
93+
try
94+
{
95+
MotelyAnalyzerFilterDesc filterDesc = new();
96+
97+
var searchSettings = new MotelySearchSettings<MotelyAnalyzerFilterDesc.AnalyzerFilter>(filterDesc)
98+
.WithDeck(cfg.Deck)
99+
.WithStake(cfg.Stake)
100+
.WithListSearch([cfg.Seed])
101+
.WithThreadCount(1);
102+
103+
using var search = searchSettings.Start();
104+
105+
search.AwaitCompletion();
106+
107+
Debug.Assert(filterDesc.LastAnalysis != null);
108+
109+
return filterDesc.LastAnalysis;
110+
}
111+
catch (Exception ex)
112+
{
113+
return new MotelySeedAnalysis(ex.ToString(), []);
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)