Camicia in Coffeescript: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Learn Game Logic in CoffeeScript: The Camicia Simulation from Zero to Hero

Simulating the Camicia card game in CoffeeScript involves managing player decks, a central pile, and turn-based logic. Key challenges include handling face card 'debts,' detecting infinite loops by tracking game states, and correctly transferring cards between players until one player runs out of cards to win.

One rainy afternoon, you sit at the kitchen table playing cards with your grandmother. The game is her take on Camicia. At first, it feels like just another friendly match: cards slapped down, laughter across the table, the occasional victorious grin from Nonna. But as the game stretches on, something strange happens. The same cards keep cycling back. You play card after card, yet the end never seems to come.

You start to wonder. Will this game ever finish? Or could we keep playing this exact same sequence forever? This is more than just a card game; it's a fascinating programming puzzle. It’s a challenge that tests your ability to manage state, handle complex rules, and, most importantly, detect infinite loops.

This article will guide you through solving this very problem using CoffeeScript. We'll dissect the logic from the exclusive kodikra.com CoffeeScript learning path, transforming abstract game rules into concrete, functional code. You'll not only build a game simulation but also master the critical skill of state management and cycle detection, a concept applicable across countless software engineering domains.


What is the Camicia Card Game Simulation?

The Camicia simulation is a programming challenge designed to model a two-player card game with a unique set of rules. Unlike games of pure chance, Camicia's outcome is entirely deterministic once the decks are dealt. The goal of the exercise is not to build a user interface, but to create a powerful engine that can play the game automatically and determine the winner or declare if the game enters an infinite loop.

This task, drawn from the kodikra module on advanced algorithms, forces you to think like a computer, tracking every single change in the game's state to predict its ultimate conclusion.

The Core Rules of the Game

To build the simulation, we must first internalize the rules of the game. Every piece of logic we write will be a direct translation of these principles.

  • The Setup: The standard 52-card deck is split between two players, Player A and Player B. The cards are face down, and the leftmost card in their initial hand represents the top of their deck.
  • Turns: Player A always starts the game. Players take turns playing the top card from their deck onto a central pile.
  • -
  • Playing a Card: On a player's turn, they draw the top card from their deck and place it on the pile.
  • Face Cards (The "Debt" System): This is the core mechanic. When a player plays a face card (Jack, Queen, King, or Ace), they create a "debt." The opponent must then pay this debt by playing a specific number of cards:
    • Jack (J): Opponent must play 1 card.
    • Queen (Q): Opponent must play 2 cards.
    • King (K): Opponent must play 3 cards.
    • Ace (A): Opponent must play 4 cards.
  • Paying the Debt: While paying a debt, if the opponent plays a non-face card, the turn continues until the debt is paid. If they play another face card, the debt is immediately canceled, and the first player now owes a new debt based on the new face card. The turn switches.
  • Winning a Trick: A player wins the entire pile of cards if their opponent finishes paying a debt without playing a face card. The winner collects all cards from the pile and adds them to the bottom of their own deck. The winner of the trick then starts the next round.
  • Winning the Game: A player wins the entire game when their opponent has no cards left to play.
  • The Infinite Loop: If the exact same game state (Player A's deck, Player B's deck, whose turn it is, and any active debt) repeats, the game is in an infinite loop and will never end. Our simulation must detect this.

Why Simulate This Game in CoffeeScript?

CoffeeScript, with its motto "It's just JavaScript," provides a highly expressive and clean syntax that can make complex logic more readable. While modern JavaScript (ES6+) has adopted many features that made CoffeeScript popular, this simulation remains a perfect exercise for understanding its strengths.

  • Clarity and Conciseness: CoffeeScript's significant whitespace and lack of curly braces can make nested logic, like the turn-based debt system, easier to follow visually.
  • Functional Programming Style: The language encourages a more functional approach. We can see this in the use of `map` and helper functions, which leads to cleaner, more maintainable code.
  • State Management Practice: The core of this challenge is managing state. CoffeeScript provides all the necessary tools (arrays for decks, objects or classes for structure, and access to JavaScript's `Set` for state tracking) in a neat package.
  • Algorithmic Thinking: The problem itself is language-agnostic. Solving it in CoffeeScript builds fundamental algorithmic skills—specifically, cycle detection in a state machine—that are transferable to any programming language, from Python to Rust.

By tackling this problem, you are not just learning CoffeeScript syntax; you are learning how to model and solve complex, stateful problems, a cornerstone of software development. You can explore more such challenges in our comprehensive CoffeeScript learning path.


How Does the Simulation Logic Work?

The entire simulation hinges on a main game loop. Inside this loop, we process one move at a time, update the game's state, and check for a win condition or an infinite loop. Let's break down the logical flow step by step.

Step 1: Initialization and Data Structure Setup

Before the game can begin, we need to set up the board. This involves:

  1. Converting Decks: The input decks are strings of characters (e.g., 'A', 'K', '5'). We need to convert these into a more useful format. The face cards are mapped to their debt values (J=1, Q=2, K=3, A=4), while number cards can be mapped to 0. This gives us two arrays of integers representing each player's hand.
  2. Game State Variables: We initialize variables to track every component of the game:
    • handA & handB: Arrays representing the cards in each player's deck.
    • turn: A string (e.g., 'A') indicating the current player.
    • pile: An array to hold the cards played in the current trick.
    • currentDebt: An integer, initially 0, tracking any active debt.
    • seen: A JavaScript Set object. This is the most critical data structure for detecting infinite loops. We will store a unique signature of every game state we encounter in it.

Step 2: The Main Game Loop and State Tracking

The simulation runs inside a while true loop, which will only terminate upon a win, a loss, or the detection of an infinite cycle. At the very beginning of each iteration, we perform our most important check.

State Hashing: We create a unique string representation, or "hash," of the current game state. This string must capture everything needed to define the game's situation uniquely. A robust hash includes:

  • Player A's entire deck, in order.
  • Player B's entire deck, in order.
  • The player whose turn it is.
  • The value of currentDebt.

A sample state hash might look like: "4,7,2|1,3,9|A|0".

Cycle Detection: We check if this hash already exists in our seen Set.

  • If it does, we have been in this exact state before. The game is in an infinite loop. We terminate and report it.
  • If it doesn't, we add the hash to the seen Set and proceed with the turn.

ASCII Diagram: The Main Game Loop Flow

This diagram illustrates the high-level logic of each iteration within the game loop.

    ● Start of Turn
    │
    ▼
  ┌──────────────────────────┐
  │  Generate Current State  │
  │ (Decks + Turn + Debt)    │
  └───────────┬──────────────┘
              │
              ▼
    ◆ State already seen? ─ Yes ─⟶ 🛑 Infinite Loop!
    │         
    No
    │
    ▼
  ┌──────────────────────────┐
  │   Add State to `seen`    │
  └───────────┬──────────────┘
              │
              ▼
    ◆ Is there a debt?
   ╱                  ╲
  Yes                  No
  │                    │
  ▼                    ▼
[Handle Debt Payment] [Play Normal Card]
  │                    │
  └─────────┬──────────┘
            │
            ▼
  ┌──────────────────────────┐
  │    Check for Winner      │
  │ (Opponent has no cards?) │
  └───────────┬──────────────┘
              │
              ▼
    ◆ Game Over? ──────── Yes ─⟶ 🎉 Player Wins!
    │
    No
    │
    ▼
  ● End of Turn (Loop)

Step 3: Handling Turns and Debt Mechanics

The logic inside the loop branches based on whether there is an active debt.

If currentDebt is 0 (Normal Play):

  1. The current player takes the top card from their deck (shift() from the array).
  2. If they have no card to play, they lose, and the other player wins.
  3. The card is added to the pile.
  4. We check if the played card is a face card. If so, we set currentDebt to its value (1-4) and switch the turn to the other player.
  5. If it's not a face card, the turn simply switches to the other player.

If currentDebt > 0 (Debt Payment):

  1. The current player (the one paying the debt) plays a card.
  2. If they have no card, they lose.
  3. The card is added to the pile.
  4. Crucial Check: Is the played card a face card?
    • Yes: The old debt is cleared. The turn switches back to the other player, who now owes a new debt based on this new face card.
    • No: The debt count decreases by one (currentDebt--). The turn remains with the same player to continue paying the debt.
  5. Debt Fully Paid: If currentDebt becomes 0 after a non-face card is played, the trick is over. The player who *set* the debt (the other player) wins the pile. They collect all cards and add them to the bottom of their deck. Their turn begins for the next round.

ASCII Diagram: Debt Payment Sub-Logic

This flow focuses specifically on the logic when a player must pay a debt.

    ● Start of Debt Payment Turn
    │
    ▼
  ┌──────────────────┐
  │   Play top card  │
  └─────────┬────────┘
            │
            ▼
    ◆ Card is a face card?
   ╱                    ╲
  Yes                    No
  │                      │
  ▼                      ▼
┌──────────────────┐   ┌──────────────────┐
│ Clear old debt   │   │ Decrement debt   │
│ Set new debt     │   │ (currentDebt--)  │
│ Switch turn back │   └─────────┬────────┘
└──────────────────┘             │
                                 ▼
                         ◆ Debt fully paid? (== 0)
                        ╱                   ╲
                       Yes                   No
                       │                     │
                       ▼                     ▼
               ┌──────────────────┐   [Continue Payment Turn]
               │ Opponent wins    │
               │ the pile.        │
               │ Their turn starts.│
               └──────────────────┘

Where the Logic Lives: A Detailed Code Walkthrough

Now let's dissect the complete CoffeeScript solution from the kodikra.com curriculum. We'll examine how the concepts above are translated into clean, functional code.

The Initial Solution Code


class Camicia
  @simulateGame: (playerA, playerB) ->

    getCardValue = (card) ->
      switch card
        when 'J' then 1
        when 'Q' then 2
        when 'K' then 3
        when 'A' then 4
        else 0

    handA = playerA.map getCardValue
    handB = playerB.map getCardValue

    turn = 'A'
    pile = []
    seen = new Set()
    
    currentDebt = 0
    debtor = null
    cardsToPay = 0

    while true
      # State hashing and cycle detection
      round = "#{handA.join(',')}|#{handB.join(',')}|#{turn}|#{currentDebt}"
      if seen.has(round)
        return { winner: null, reason: 'Infinite game detected' }
      seen.add(round)

      # Determine current player's hand
      currentHand = if turn is 'A' then handA else handB
      otherHand = if turn is 'A' then handB else handA

      # Check for win condition before playing
      if currentHand.length is 0
        winner = if turn is 'A' then 'B' else 'A'
        return { winner: winner, reason: 'Player out of cards' }
      
      cardPlayed = currentHand.shift()
      pile.push cardPlayed

      if currentDebt > 0
        # Paying off a debt
        cardsToPay--
        if cardPlayed > 0 # A face card was played
          turn = if turn is 'A' then 'B' else 'A' # Turn flips back
          currentDebt = cardPlayed
          cardsToPay = cardPlayed
          debtor = turn
        else if cardsToPay is 0 # Debt paid off
          winnerOfTrick = if turn is 'A' then 'B' else 'A'
          if winnerOfTrick is 'A'
            handA.push(pile...)
          else
            handB.push(pile...)
          
          pile = []
          currentDebt = 0
          debtor = null
          turn = winnerOfTrick
      else
        # Normal play
        if cardPlayed > 0 # Face card played, debt is created
          currentDebt = cardPlayed
          cardsToPay = cardPlayed
          turn = if turn is 'A' then 'B' else 'A' # Turn flips
          debtor = turn
        else
          turn = if turn is 'A' then 'B' else 'A' # Turn flips

Note: The provided solution has been slightly refined for clarity and correctness in this walkthrough, particularly in state tracking and debt management logic.

Code Breakdown

1. Class and Static Method


class Camicia
  @simulateGame: (playerA, playerB) ->

The solution is wrapped in a Camicia class. The @ symbol in CoffeeScript defines a static method on the class itself, meaning we can call it directly via Camicia.simulateGame() without needing to create an instance of the class. This is a clean way to encapsulate the game logic.

2. Helper Function and Deck Initialization


    getCardValue = (card) ->
      switch card
        when 'J' then 1
        when 'Q' then 2
        when 'K' then 3
        when 'A' then 4
        else 0

    handA = playerA.map getCardValue
    handB = playerB.map getCardValue
  • getCardValue: A local helper function that neatly converts card characters into their integer debt values. The switch statement is a clean and readable way to handle these mappings.
  • handA = playerA.map getCardValue: This is beautiful, functional CoffeeScript. The map function iterates over each card in the input array (e.g., ['A', '5', 'K']) and applies the getCardValue function to it, creating a new array of integers (e.g., [4, 0, 3]).

3. State Variable Initialization


    turn = 'A'
    pile = []
    seen = new Set()
    
    currentDebt = 0
    debtor = null
    cardsToPay = 0

Here we set up our game state. turn starts with 'A'. pile and seen are empty. We also initialize our debt-related variables. currentDebt holds the *type* of debt (1-4), while cardsToPay is the countdown timer. Keeping them separate can clarify the logic.

4. The Game Loop and Infinite Game Detection


    while true
      round = "#{handA.join(',')}|#{handB.join(',')}|#{turn}|#{currentDebt}"
      if seen.has(round)
        return { winner: null, reason: 'Infinite game detected' }
      seen.add(round)
  • The loop begins.
  • round = "...": This is the crucial state hashing. We create the unique string using CoffeeScript's string interpolation "#{...}". Crucially, we include currentDebt in the state. A game with the same cards but a different debt status is a different state.
  • if seen.has(round): We check the Set for this string. Because Set lookups are extremely fast (average O(1) time complexity), this is a very efficient way to detect a cycle.
  • seen.add(round): If the state is new, we record it for future checks.

5. Player Turn and Win Condition Check


      currentHand = if turn is 'A' then handA else handB

      if currentHand.length is 0
        winner = if turn is 'A' then 'B' else 'A'
        return { winner: winner, reason: 'Player out of cards' }
      
      cardPlayed = currentHand.shift()
      pile.push cardPlayed

Before any action, we check if the current player *can* play. If their hand is empty at the start of their turn, they have lost. If they can play, we remove the top card using shift() (which modifies the array in place) and add it to the pile.

6. The Core Logic: Debt vs. Normal Play


      if currentDebt > 0
        # ... debt payment logic ...
      else
        # ... normal play logic ...

The code now branches. The logic inside these blocks is a direct implementation of the rules we discussed earlier.

  • Debt Logic (if currentDebt > 0): The code decrements cardsToPay. It checks if the cardPlayed is a face card (> 0). If so, it resets the debt and flips the turn. If not, and if cardsToPay hits zero, it awards the pile to the winner of the trick and resets for the next round.
  • Normal Logic (else): The code checks if the cardPlayed creates a new debt. If so, it sets currentDebt and cardsToPay, then flips the turn. If not, it's a simple number card, and the turn just flips to the other player.

This walkthrough demonstrates how a complex set of rules can be broken down into manageable logical blocks, with CoffeeScript's syntax helping to keep the implementation clean and readable.


When Does a Game Become Infinite?

An infinite game in Camicia isn't random; it's a mathematical certainty once a state repeats. A "state" is a complete snapshot of the game at a single moment. As we defined earlier, this includes:

  • The exact order of cards in Player A's deck.
  • The exact order of cards in Player B's deck.
  • Whose turn it is to play.
  • The current debt value (if any).

Since there are no external random factors (like shuffling or new cards being introduced), if the game ever returns to a previously seen state, the sequence of plays that led from the first occurrence to the second will inevitably repeat, forever. This is known as a cycle in a finite state machine.

Our simulation's `seen` Set is our memory. By storing a hash of every state encountered, we can instantly recognize when we've been here before, allowing us to confidently declare the game as infinite without having to simulate it endlessly.

Pros and Cons of State Hashing

This method is powerful, but it's worth understanding its trade-offs.

Pros Cons
Guaranteed Detection: This method is foolproof. If a cycle exists, it will be found. Memory Usage: For extremely long but finite games, the seen Set could grow very large, consuming significant memory.
High Performance: Using a Set for lookups is incredibly fast, making the per-turn check negligible in terms of performance impact. State Complexity: The effectiveness depends entirely on creating a correct and complete state hash. Forgetting a variable (like currentDebt) would lead to incorrect results.
Simplicity of Implementation: The logic is straightforward: generate a string, check if it's in the set, add it if not. Not Suitable for All Games: This technique only works for deterministic games with a finite number of states. It wouldn't work for games with randomness (like dice rolls) or an infinite state space.

Frequently Asked Questions (FAQ)

What is the role of the Set in this simulation?
The Set object is used as a high-performance cache to store the "hashes" of all previously encountered game states. Its primary role is to detect infinite loops. By checking if the current state's hash is already in the Set, we can instantly determine if the game has entered a cycle.
Why is currentDebt so important to include in the game state hash?
Because two game states can have identical decks and the same player to move, but be in completely different situations. For example, in one state, Player B might be about to play normally. In another, they might be required to play 3 cards to pay off a King's debt. Omitting currentDebt from the hash would incorrectly treat these two different states as the same, potentially missing an infinite loop or declaring one where none exists.
Can this CoffeeScript code be run in a modern browser?
Yes, absolutely. CoffeeScript is not an interpreted language; it's a transpiled one. You would use a CoffeeScript compiler (like the official `coffee` command-line tool) to convert this .coffee file into a standard JavaScript .js file. That resulting JavaScript file can be run in any modern browser or Node.js environment without any issues.
How could I optimize this code for performance?
For this specific problem, the current solution is already quite efficient. The main performance bottleneck in similar simulations is often the cycle detection, which is handled optimally here with a Set. Further micro-optimizations, like avoiding array re-concatenation, are possible but would likely have a negligible impact compared to the O(1) state lookup, which is the most significant performance factor.
What happens if both players run out of cards at the same time?
Based on the game's rules, this is an impossible scenario. Cards are only ever moved from one player's hand to the pile, and from the pile to the other player's hand. The total number of cards in the game (52) remains constant. A player only loses when it's their turn to play but their hand is empty, which means the other player must still have all 52 cards.
Is CoffeeScript still a relevant language to learn?
While modern JavaScript (ES6 and beyond) has incorporated many of the features that made CoffeeScript popular (like arrow functions and classes), CoffeeScript still offers a uniquely clean and minimal syntax. It's valuable to learn for maintaining existing codebases built with it and as an excellent tool for understanding the core concepts of transpilation and how high-level syntax can be compiled down to JavaScript.
How does this logic apply to other programming problems?
The core concept of modeling a problem as a series of states and transitions is fundamental to computer science. This is known as a Finite State Machine (FSM). The cycle detection technique is used in various fields, including network protocol analysis (detecting packet loops), compilers (analyzing code flow), and AI in games (preventing repetitive NPC behavior).

Conclusion: From Game Rules to Algorithmic Mastery

We've journeyed from a simple card game at a kitchen table to a robust, algorithmic simulation in CoffeeScript. By translating the rules of Camicia into code, we didn't just build a game engine; we mastered several critical programming concepts. You learned how to manage complex, ever-changing state, how to implement intricate, rule-based logic, and most importantly, how to solve the universal problem of cycle detection using state hashing.

The solution presented, a highlight of the kodikra.com curriculum, showcases how CoffeeScript's elegant syntax can make sophisticated logic more approachable. The skills you've honed here—modeling problems, managing state, and thinking algorithmically—are the bedrock of a successful software engineering career and will serve you well in any language or framework you choose to learn next.

Ready for the next challenge? Continue your journey and explore our full CoffeeScript Learning Path to tackle even more advanced problems and solidify your expertise.

Disclaimer: The code in this article is written using modern CoffeeScript conventions. Please ensure your development environment is set up with a recent version of the CoffeeScript compiler to transpile it to compatible ES6+ JavaScript.


Published by Kodikra — Your trusted Coffeescript learning resource.