The End of an Era!

Dual Snake

Today, with heavy hearts but immense gratitude, we announce the end of service for Dual Snake’s online features. It has been 7 years and 7 months since our beloved two-headed snake was released on January 31st, 2018. It has been an incredible journey, and we thank you all for being a part of it.

Over the years, our server has been racking up costs, and the now-unsupported software it was running on finally gave out. While this marks the end of its online life, we’ve worked to ensure the spirit of the game lives on. We want to wholeheartedly thank every single person who has played, created, and competed in Dual Snake.

Immortalizing the Snake

All online services have now been migrated into permanent offline features. We made sure nothing was lost.

  • The Level Editor now saves levels directly to your disk! You can find them in <DRIVE>:\Users\<YOUR_USER>\AppData\Roaming\Dual Snake\userdata\levels. You can still share levels with friends! Just have them place the files in this folder, and they’ll be ready to play!
  • All community-created levels that were ever uploaded are now bundled with the game. You can access this massive library of content from the Custom menu (formerly Online).
  • All-time high scores have been immortalized. We have bundled the final leaderboards with the game after carefully removing impossible and cheated scores. The world records are now set in stone!
  • New high scores are saved locally. You can still challenge yourself and your friends, competing against the immortalized world records on all main, side, and custom levels.

Dual Snake’s Legacy and Our Future

We’ve moved on from Dual Snake, but we couldn’t be more proud of what it became. It taught us so much and was the backbone of the experience that allowed us to create and launch our next game, Primateria. Our lovely two-headed snake even made the journey with us and is present in the game with a brand-new art style!


You can check out Primateria here:

And the adventure doesn’t stop there. We are now hard at work on its sequel, Primateria: Reburn. We’re pouring everything we’ve learned into this new chapter.

You can follow our progress and wishlist it here:

We are very proud of our journey and incredibly thankful to all of you for being there. We hope you’ll continue with us on our new adventures!

Much love <3
Yfrit.

Primateria Reburn is Coming!

Hello Primateria Community!

In our last announcement, we promised that more exciting news was coming, and today we’re thrilled to share exactly what that is. We’ve also got a fresh set of updates for Primateria itself, so read on for the details!


Card Changes and Balancing Updates

  • Soul Devouring Crab
    The effect now reads “From mana” instead of “On fusion,” providing more flexibility on when to use this ability. It also opens up new synergies with cards like Adventurer’s Toolkit, so deck builders rejoice!
  • New Rules for Disabled Cards
    At the start of a player’s turn, any of their disabled cards will become bound instead. This change is intended to weaken strategies that fully lock down a player by occupying all their zones.
    (Note: Disabled cards will still become enabled/unbound at the end of their owner’s turn, as before.)
  • Enhancements, fixes, and other small changes
    Many bugs were fixed—especially in Daily Deck. Thank you to everyone who reported them!

New Artwork: Convoy Bison

We’re thrilled to reveal the brand-new Convoy Bison artwork! This design breathes fresh life into one of our beloved behemoths. Keep an eye out for it in your next match and let us know what you think!


Primateria Reburn is Coming!

Over the past year, we’ve worked tirelessly on Primateria—rolling out major updates, adding new content, and listening closely to your feedback. We’ve learned so much along the way and remain deeply grateful for your incredible support.

Today, we’re finally ready to unveil our next big project:

“A bold new TCG adventure fusing single-player exploration with a vibrant social hub—build your deck, explore shops, collect cosmetics, challenge fearsome AI and real players alike, and leave your mark on the Primateria world!”

Overview

Primateria Reburn reimagines the essence of Primateria, blending deck collection, trading, and challenging battles with enhanced community features that encourage social interaction. Whether you enjoy mastering single-player campaigns or going head-to-head with fellow players, Reburn offers the best of both worlds.

  • Explore Shops & Collect More Than Just Packs
    Visit multiple in-game shops to find exclusive card packs, powerful items, and rare cosmetic collectibles.
    Customize your avatar, deck box, and board to truly make your mark.
  • Primateria Battles: Single-Player & PvP
    Single-Player: Tackle advanced AI opponents and experience the evolution of the popular symmetric boss battles. Each encounter is deeper, harder, and full of surprises.
    PvP: Challenge friends or match against other players to test your newest combos and strategies. Climb the ladder or create custom lobbies for friendly skirmishes.
  • Social Hub
    Hang Out & Interact: Join a dynamic social space populated by both players and AI characters. Discuss deck builds, seek advice, or simply relax and share your latest pack pulls.
    Trades & Drafts: Trade cards with others to complete your collection or host thrilling draft events where each choice can lead to your next great synergy.
    Tournaments & Events: Compete in organized tournaments—both casual and high-stakes—and participate in seasonal events that keep the meta fresh and exciting.

Game Concept

As a direct sequel to the original Primateria, Reburn keeps the core card-battling system you love but expands into a fully-realized TCG world with new features:

By steadily building your collection, showcasing your style with unique cosmetics, and immersing yourself in a lively community, Primateria Reburn promises a richer, more connected card-battling journey than ever before.

Early Development Sneak Peek

We’re still early in development, so expect many things to change. In the meantime, here’s a small taste of what’s in store. Please note that all visuals below are placeholders, early drafts, or experiments as we shape Primateria Reburn:



We’ll have more to share as development continues—stay tuned!


Final Words

As always, your feedback and enthusiasm keep Primateria evolving. While we’ll continue to maintain Primateria (the original), our primary focus will shift toward Primateria Reburn. We’re incredibly proud of what the first game achieved and grateful to everyone who played and supported it through countless matches, events, and updates.

Servers will remain open, and the feedback, Daily Deck entries, and analytics from the original Primateria will help inform and shape Reburn. We can’t wait for you to explore this new social frontier of Primateria and share your thoughts on the evolving TCG experience!

If you’d like to connect with us, Join Our Discord, follow us on Instagram @yfritgames, or find us on Bluesky. You can also reach out via our website’s contact form at www.yfrit.com or by email at [email protected].

Thank you for your ongoing support,
Yfrit Team

How we made a card game without screens at Yfrit Games

“A card game without screens? What do you mean? Did you code with your monitor turned off or something?”

Well, of course not. What I mean is the following:

Imagine you are programming a game, and just finished implementing that new feature, and now you want to test it to see if it’s working properly. You then run the game and play the new feature. That’s the screen I am talking about: the screen in which you run the game.

I worked on Primateria, the card game that Yfrit Games is currently developing, for six months before having a single screen to run the game. But why?

Why I did this to myself

You see, we don’t have any artists on our team. So when we started making this game, we wanted to construct it in such a way that we would be able to easily change the visuals of the game, without having to do big changes to the code.

The best way we found to do that was separating completely the logical parts (rules, flow of the game, etc) and the visual parts (appearance of cards, animations, player inputs, etc). So how did we do it?

The MVC Architecture

The main thing behind all of this is something called the Model-View-Controller architecture. It’s a pretty common pattern used in Software Engineering, and there are many ways to interpret it. I will now explain how exactly it worked in our project.

Models represent the logical state of the game. Examples of models would be classes like the Card, that contains all the logical properties of a card, like level and elements. Or the GameState, a class that stores the locations of every card in the game (note that I am talking about locations like “deck” or “hand”, not x/y screen coordinates, since that would be a visual thing).

Controllers implement the flow of the game, manipulating the models to do so. An example would be the MechanicExecutor, that implements all the base mechanics of the game. If we take the attack mechanic for example, the MechanicExecutor implements it by manipulating a GameState, changing the locations of cards when the attack occurs.

Views represent the state of the game visually, by accessing/reading the models, but never modifying them directly. They are also responsible for detecting player inputs and communicating them to the controllers. Here would be things like the visuals of a card, with an image and screen x/y coordinates.

The following image shows how the models, views and controllers communicate with each other in the game:

  • The controllers read/access, but also manipulate/modify the state of the models.
  • The models communicate to the views when they are changed.
  • When a change occurs, the views read the state of the models to update the visual state correctly.
  • When a player action is necessary (e.g. choosing a card to attack), the controllers will make a request to the views and wait for a response.
  • When the views receive a request, they will prompt the player and wait for an action, and then respond to the controllers.

Automated Tests

That’s a cute architecture and all, but how did I know that everything worked, without a screen to test the game? Well, the nice part about that architecture is that I could easily replace the Views with something else entirely. That’s where automated tests come into play.

The main idea behind automated tests is writing code to test the code. Here is how my workflow would normally go. I would begin by writing a test to the model (is this case, the GameState):

gameState:moveCard(card, "hand")
local location = gameState:getCardLocation(card)
assert.are_equal(location, "hand")

This test basically:

  • Calls the GameState method that is supposed to move a card to another location;
  • Calls the GameState method to get the location of the card;
  • Checks if the location was updated correctly.

After that, I would implement the method being tested (GameState.moveCard in this case), and then see if the test passed. If it did, then the code was working properly. Otherwise, something was wrong and I had to investigate and fix it.

With the model working properly, the next step would be going one level above, and writing the tests for the controller. Here, I wanted to test the MechanicExecutor.attack method, to check if the targeted card was moved to the damage zone after the attack:

mechanicExecutor:attack(
    {
        attackers = {attacker1, attacker2},
        target = target
    }
)
local location = gameState:getCardLocation(target)
assert.are_equal(location, "damage")

Inside the attack method, I would use the recently implemented GameState.moveCard to move the card to the damage zone:

function MechanicExecutor:attack(params)
    -- ...
    -- other things that happen during an attack
    -- ...
    
    gameState:moveCard(params.target, "damage")
end

The next level is where things get interesting.

The Event System

So far, I only talked about how I implemented the pure logical parts of the game using tests, but there was no need for player input so far. So how would I implement something like the logic of letting the player choose the target for an attack?

As I mentioned in the MVP section, the communication between the Controllers and Views happens through requests. In the context of this game, a request is a way to ask a question, and then wait for a response, without caring who is responding. To implement that, I used Yfrit’ s Event System.

So, let’s go to an example. In the code, the class that would call the MechanicExecutor (the controller we were testing earlier) is called Phases, and it controls everything that happens in each phase of the game. For the battle phase, it would have a method Phases.battle, that would do the following things:

  • Request for a battle phase action, that could be either attacking or ending the phase;
  • If the action was to end the phase, the phase would just end.
  • If the action was an attack, it would also contain extra parameters, which in this case would be a list of attackers and an attack target. The Phases would then perform the attack by calling MechanicExecutor.attack (the controller method we tested earlier).

The implementation of this method looks something like this:

local action, actionParams =
           Event.request("battlePhaseAction")
if action == "endPhase" then
    -- ...
    -- code that ends the battle phase
    -- ...
elseif action == "attack" then
    mechanicExecutor:attack({
        attackers = actionParams.attackers,
        target = actionParams.target
    })
end

In a normal game flow, a View would respond that request, according to what the player clicked on the screen. But in a test environment, we don’t want to depend on the player, so what do we do? Well, that’s simple.

Instead of the player, I would make the test code itself respond to the request, replacing the role of the View. So I wrote a test that looked like this:

Event.listenRequest("battlePhaseAction", function()
    local action = "attack"
    local actionParams = {
        attackers = {attacker1, attacker2},
        target = target
    }
    return action, actionParams
end)

phases:attack()

local location = gameState:getCardLocation(target)
assert.are_equal(location, "damage")

This test basically:

  • Prepares a responder for the battlePhaseAction request, that will respond with an “attack” action, and the proper attackers and target;
  • Calls the Phases.attack method, that internally makes the request, which is automatically responded by the previously prepared responder;
  • Checks if the target card was moved to the damage zone as expected.

And this way, I managed to test the entire attack mechanic. By using that same process, we were able to implement and test all the base mechanics of the game without having to implement a single View/Screen.

Conclusion

So this is how we at Yfrit Games used MVC, automated tests and request communication to separate the logical and visual parts of our game completely.

After all the base mechanics of the game were already working, we finally started actually implementing the first screens. And you know what? It actually worked pretty well. We didn’t need to make any big changes to the previous code, and even managed to keep the logical and visual parts in separated repositories.

So what did you think? Would you use this approach in one of your projects? What would you do differently?