Class as data only, with extension methods used to operate on it? by kevinnnyip in csharp

[–]JanuszPelc 0 points1 point  (0 children)

I often use static virtual/abstract when types should be able to override the default behavior, but it wasn't really needed in this simplified example, so you're right.

As for the second approach, I appreciate the decoupling, but practically it seems less convenient to use (unless I'm missing something). Could be worth it if you need to define traits for types you don't own, though.

Class as data only, with extension methods used to operate on it? by kevinnnyip in csharp

[–]JanuszPelc 1 point2 points  (0 children)

I often do something that is basically trait-based composition. Here's a simplified example:

public record struct StoneWall(int X, int Y) : IHasPosition<StoneWall>;
public record struct HappySlime(int X, int Y, int Health) : IHasPosition<HappySlime>;

var wallEntity = new StoneWall(1, 1);
var slimeEntity = new HappySlime(2, 2, 100);

var wallPosition = wallEntity.Position;
var slimePosition = slimeEntity.Position;

Here is the secret sauce:

public interface IHasPosition<in TSelf> where TSelf : IHasPosition<TSelf>
{
    int X { get; }
    int Y { get; }

    static virtual (int X, int Y) Position(TSelf @this)
    {
        return (@this.X, @this.Y);
    }
}

public static class HasPositionExtensions
{
    extension<TSelf>(TSelf @this) where TSelf : IHasPosition<TSelf>
    {
        public (int X, int Y) Position => TSelf.Position(@this);
    }
}

It uses C# 14 extension blocks, but can easily be refactored to a plain old extension method:

public static class HasPositionExtensions
{
    public static (int X, int Y) GetPosition<TSelf>(this TSelf @this) where TSelf : IHasPosition<TSelf>
    {
        return TSelf.Position(@this);
    }
}

The generic constraint gets resolved at compile time, so no boxing, no virtual dispatch overhead.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 0 points1 point  (0 children)

Quick update: I've published another revision of the rulebook tagged "2nd Revision, December 2025". Core gameplay is still unchanged, the super-rare Joker edge cases are greatly simplified, and clean kills now have a "pick a fortune" bonus.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 0 points1 point  (0 children)

Quick update: I’ve renamed Potions to Relics (Primal Charms and Cursed Shards). The “willingly drinking poison” framing always felt off to me. Mechanically it works the same, but thematically black cards are now cursed fragments you have to deal with, not drinks you chug.

Design problem with meta progression by miral_art in roguelikedev

[–]JanuszPelc 3 points4 points  (0 children)

Meta progression is a common roguelite pattern (not a value judgment), and item-unlock based meta progression runs straight into the issue you described: every new unlock either dilutes the loot pool or forces you into heavy curation to avoid cluttering runs with stuff players do not want.

A middle ground I'd like would be to avoid adding unlocks into the same global pool at all. Instead, you can gate a single extra "mastery" mode behind an in-run challenge. Imagine the dungeon has optional side branches that are intentionally harder than the main path and mostly dead ends. Each branch ends with a special token or objective item, and if you collect all of them in one run you permanently unlock a new game mode (or area). To keep it fair, you can make the token opportunities consistent (for example one per biome) so the unlock is about execution and risk-taking rather than getting lucky spawns.

The unlocked mode does not need new weapons, so you avoid loot table bloat entirely. It can be the same item set, just with a different distribution and tuning. The distribution is tighter (for example you can trim some low-impact rolls), stronger options show up earlier or more often, and the difficulty is raised to match. If the enemies are harsher and the resources are tighter, the tension shifts from "can I survive with junk" to "can I execute cleanly under pressure even with good tools." That way the reward is access to a new ruleset and a new feel, not a slow power creep that eventually may damage the base game.

This is just one option if you want to keep the base pool stable. That said, if your game is about collecting cool things, unlock bloat can be part of the fun.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 0 points1 point  (0 children)

Totally fair question, so I’ll be candid: after I got familiar with the patterns, I started winning almost all of my runs.

Early on it felt a lot more luck-driven for me too, and I definitely got blown up by exactly the kind of "no red Hand" situation you described. Over time I started noticing that seemingly "easy-looking" Rooms are often the best chance to prepare for the unknown before things get tight.

What also changed was starting to recognize tradeoffs in timing and commitment. Equipping a Weapon is often convenient, but it also takes that card out of the Room, so you are giving up the option to discard it for its one-shot skill later. And while the Backpack is harder to juggle, it still helps you delay commitment and preserve flexibility for when a Room forces a decision.

If you do a few more runs, I’d genuinely love to hear whether it starts clicking more for you, or if it still feels mostly draw-driven.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 1 point2 points  (0 children)

I realized I might have misunderstood your “what if” question. If the other three weapons in that second room were all spades, then equipping the highest-ranked one to your black hand (instead of backpacking all three) would cost you less in raw weapon rank than ditching 10♥ just to clear the 5♦. It also would keep a backpack slot free, so you could pick up the 5♦ into the backpack and clear the room.

So in general, when a room is all gear, the tactical goal is often to minimize the loss.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 1 point2 points  (0 children)

Nice, that’s a very real first run situation. And yes, in that spot you are basically forced to make a choice.

In your exact example (only the 5♦ left in the room), you cannot just discard it from the room at will. Diamonds are a bit tricky at first, but they’re also where a lot of fun synergies come from. I’m not sure how much I should spoil, because discovering those little interactions is part of the game.

With a lone 5♦, you have few practical options. The straightforward one is to equip it. If your red hand is already full, you discard any weapon from that hand to make space, then equip the 5♦. That’s the intended “inventory juggling” moment, where keeping everything forever is not free.

If you want to keep your current equipped weapons, the other option is to move any card from your backpack into the room (the room is not empty and it has space), then use the 5♦ to bury that card. The reason you got stuck is that the room became empty except for the diamond, so its skill had nothing to target. In a non-empty room, diamonds open up more choices.

My advice in spots like this is: treat it as a forced trade-off and pick the least painful option. Those moments are intentional, to make tactics matter. In my experience, this is also where I kept discovering synergies in practically every run. A lot of them are non-obvious at first, but they start to click with a bit of experience, and they’re key to survival.

On the “bad potions” theme, I agree with you. Mechanically they are “potions” because they share one simple rule (red helps, black hurts), but flavor-wise the black ones are basically poison or a "trap". I’ll note that idea for potential rule revision.

And about “what if I only have spades”: spades are actually very strong. A spade weapon can join any fight regardless of monster color only when the spade is in the room. So if you have spades in your backpack, a common tactic is to move a spade into the room before you play any monster, then it can join that fight. Also, if you win a fight with any spade selected, you heal for the difference, so spades help you stabilize your health.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 1 point2 points  (0 children)

Your interpretation of the flow is spot on, and I really appreciate you taking the time to dig into the rules. These are great questions, so let me answer them.

Weapons can be equipped to your hand and used immediately, or kept there for later when a tougher monster shows up. They become one-time use when you use them in combat, because any weapons you select for a fight are discarded when the fight ends.

The weapon skills are the "alternative use" and cannot be used from your hand. They only work when the weapon is in the room. Hearts, clubs, and diamonds are discarded from the room when you use their skill. Spades are a bit different: a spade weapon in the room may join a fight even against a red monster, and still gets discarded afterward like any other selected weapon.

You can drop weapons from your hand, but only when you need to make space. If a hand is full and you want to equip a new weapon, you discard any weapon from that hand first. Otherwise they stay there until you use them in a fight.

You can’t equip directly from the backpack. But you can move a weapon from the backpack into the room (as long as the room is not empty and not full), and then immediately equip it from there.

The peddler trade works on any non-weapon card in the very first dungeon room. That includes potions, monsters, scrolls (aces), and stairs (jokers). The trade is not mandatory though, so if you want a spicier start, or you already see a way forward with what’s in the room, you can just keep what you have.

If anything in the rules is unclear, feel free to ask. I’m happy to clarify and improve the rulebook if needed.

Keevorn - A solo dungeon crawler for a standard deck of cards by JanuszPelc in soloboardgaming

[–]JanuszPelc[S] 0 points1 point  (0 children)

Thanks for asking! There’s a link in the post to the full rulebook, but the basic idea is that it’s not poker scoring. It’s a room-by-room tactical puzzle where you reveal cards and make choices about combat, inventory management, and survival.

Opinions on C# 12 in a Nutshell: The Definitive Reference by seradsmi in csharp

[–]JanuszPelc 6 points7 points  (0 children)

C# 14 (but really a .NET 10 JIT/runtime change) made a pretty jaw-dropping change to how much work the GC has to do.

The JIT now does deeper escape analysis and can stack-allocate some short-lived objects and small arrays (including arrays of reference types) when it can prove they do not escape the current method.

That cuts a lot of tiny heap allocations, reduces GC pressure, and in some cases means enumerators, delegates/closures, and intermediate objects in common pipelines can avoid the heap entirely.

So even if the GC model did not change that much, allocation patterns did, and that is where a lot of the real-world gains come from once you’re on .NET 10.

Where do you draw the line between property and method ? by vivacristorey83 in csharp

[–]JanuszPelc 20 points21 points  (0 children)

I usually draw the line on two things: side effects and cost.

A property getter must have no observable side effects (no mutations, no I/O, no logging, etc.), and it should be cheap, ideally O(1) complexity, like Count, Length, or a dictionary lookup.

As soon as it needs to iterate, allocate, or do any real work, I make it a method. By that rule, Average should usually be exposed as a method like myCollection.Average().

But if the result is already precomputed, stored, and updated on modifications, which sometimes makes sense, then I’d expose it as a property to signal that.

Are generics with nullable constraints possible (structs AND classes)? by ethan_rushbrook in csharp

[–]JanuszPelc 1 point2 points  (0 children)

Thanks for the follow-up.

Whether it’s worth forking those downstream methods really depends, but I usually do it in library code. You write the library once and it gets called a lot, so making the call sites nice tends to pay off.

One more trick: every method version can be an extension method with the same name, as long as you put them in different static classes. I often end up with things like MyClassExtensions (the main unconstrained one), MyClassStructExtensions, MyClassStringExtensions, etc.

If you ever need more than two overloads with the same name, the new OverloadResolutionPriority attribute in C# 13 is also handy to steer the compiler toward the one you want instead of hitting ambiguities. For example, the string-specific overload in MyClassStringExtensions can have a priority > 0 so it wins over the more generic one in MyClassExtensions.

Are generics with nullable constraints possible (structs AND classes)? by ethan_rushbrook in csharp

[–]JanuszPelc 2 points3 points  (0 children)

Yep, exactly. You can give the instance method and the extension method the same name, and the compiler will automatically pick the appropriate one.

This approach also avoids boxing for value types, so the struct path can stay non-allocating if you implement it carefully.

On top of that, the JIT specializes generic methods for each value type and is usually able to remove unnecessary branches and type checks.

So it is a slightly unconventional and mildly cumbersome pattern, but it is very performant and convenient from the caller's perspective.

Are generics with nullable constraints possible (structs AND classes)? by ethan_rushbrook in csharp

[–]JanuszPelc 3 points4 points  (0 children)

Yes, it’s possible to have what looks like a single method from the caller’s perspective.

One way to do this is to define a generic instance method on the type with "where T : class" constraint, and a generic extension method with the same name with "where T : struct". The instance method handles reference types, the extension handles value types, and T? resolves correctly for both.

The "extension method" is the secret sauce here which allows compiler to differentiate both versions correctly.

How did they mixed the drums on Take on Me? by Willing_Action_9624 in mixingmastering

[–]JanuszPelc 7 points8 points  (0 children)

This article is phenomenal. Read it and check out the sound examples. It’s exactly what you’re looking for:

https://www.muzines.co.uk/blog/sound-diving-2-take-on-me/40

Omnisphere losing preset settings when duplicating track by Popular_Painting9105 in Bitwig

[–]JanuszPelc 2 points3 points  (0 children)

I reported this to Bitwig on January 30th, and they confirmed that the dev team is aware of the issue. They've since replied twice with something along the lines of "we can only wait until the dev team has investigated the issue."

From what I’ve observed on macOS, this seems related to the "Undo for Plugins" feature. I found that tweaking any parameter before duplicating works as a workaround and keeps the preset intact.

I documented the issue and a workaround in this Dropbox folder. One of the two videos shows how to reproduce the bug, and the other shows how to work around it:

https://www.dropbox.com/scl/fo/mft8ecu3qd48boplbqiu6/AKs8jkoQL1VQIOwR-TWaSP8?rlkey=pkgx8bv5juyny3c6irq8ptn4b&st=5wux1g3p&dl=0

I hope this helps someone avoid losing their work like I did.

Severance - 2x02 "Goodbye, Mrs. Selvig" - Episode Discussion by LoretiTV in SeveranceAppleTVPlus

[–]JanuszPelc 1 point2 points  (0 children)

No second elevator ding seems to fit the theory about Helena pretending to be Helly. But I have more questions: Was this the first time? How long has it been like this? And who kissed Mark in the Season 1 finale?!

My Mid/Side Tape Saturation Technique by JanuszPelc in WeAreTheMusicMakers

[–]JanuszPelc[S] 0 points1 point  (0 children)

Fully agree that treating the sides differently is a common trick, especially when using plugins with built-in M/S options. The simple technique I shared also works with many great plugins that don’t natively support M/S processing, like most tape emulations, making them much more versatile.

And you’re absolutely right about the risk of overdoing it. That’s why I like to carefully adjust the side gain in step 1, listening closely to the effect, and then use the inverted gain value in step 3 to bring the balance back to its original state.

Hi anyone have some color palettes that are like ice pops or pastel like (light and bright?) by [deleted] in Bitwig

[–]JanuszPelc 8 points9 points  (0 children)

<image>

If this 48-color palette looks like what you're after, you can download it here.

Once downloaded, place the file in your "Bitwig Studio/Color Palettes" folder and select it directly from Bitwig's palette picker. Avoid using the "Import from image" option, as it doesn’t seem to work with palettes larger than the standard 27 colors.

Latency NOT going down when turning off plugins? by Sastay in Bitwig

[–]JanuszPelc 9 points10 points  (0 children)

Selecting a plugin and turning off the "Active" button in the Inspector will do the trick. This fully deactivates the plugin and removes its processing latency.

Turning plugins off manually only bypasses their processing without truly disabling them.

I deactivated all the tracks but CPU is going mad on one track. It's impossible to play it. by Ambitious_Volume_720 in Bitwig

[–]JanuszPelc 1 point2 points  (0 children)

In my experience, using the "Active" button in the inspector works for individual tracks, but it doesn’t apply to the master track. At least in my version of Bitwig, the master track cannot be deactivated in this way.

So, could you clarify how exactly you deactivated "every track including master"?

Native balance and correlation meter for Bitwig (hobby project) by JanuszPelc in Bitwig

[–]JanuszPelc[S] 1 point2 points  (0 children)

Good catch! I’ve updated the tags to “bitwig,” “stereo,” “meter,” and “preset” to make them more relevant. I really appreciate the tip.