Case Study 2: Game Character Attribute System

A preview of Crypts of Pascalia: character creation with attribute validation (IF for range checks, CASE for character class selection).


The Scenario

Tomas Vieira has been thinking about Crypts of Pascalia, the text adventure game that we will build progressively throughout this book. Before there can be a dungeon to explore, there must be a character to explore it with. Tomas wants to build a character creation system — the first screen a player sees when they start a new game.

A character in Crypts of Pascalia has: - A name (any non-empty string) - A class (Warrior, Mage, Rogue, or Cleric) - Four attributes: Strength, Intelligence, Dexterity, Constitution — each ranging from 3 to 18

The class determines the character's starting attribute bonuses, and the system validates that all inputs are within legal ranges.

The Design

The character creation flow is:

  1. Enter name — validate it is not empty.
  2. Choose class — present a numbered menu, use case to process the selection.
  3. Assign base attributes — each class gets different starting values.
  4. Distribute bonus points — the player gets 5 extra points to allocate. Validate that each allocation does not push an attribute above 18.
  5. Display the final character sheet.

This design uses every decision construct from Chapter 5: - if..then for simple validations (non-empty name, positive points remaining) - if..then..else for two-way decisions (accept or reject an allocation) - Nested if for compound validation (attribute in range AND enough points remaining) - case for class selection and attribute allocation target - Compound Boolean expressions for complex validation rules

The Implementation

program CharacterCreator;
{ Crypts of Pascalia — Character Creation System }
{ Demonstrates: CASE for class selection, IF for validation, }
{               compound Booleans, nested decisions }

const
  MIN_ATTR       = 3;
  MAX_ATTR       = 18;
  BONUS_POINTS   = 5;
  MAX_NAME_LEN   = 20;

var
  charName:       String;
  classChoice:    Integer;
  className:      String;

  strength:       Integer;
  intelligence:   Integer;
  dexterity:      Integer;
  constitution:   Integer;

  bonusRemaining: Integer;
  attrChoice:     Integer;
  pointsToAdd:    Integer;
  currentValue:   Integer;
  confirmed:      Char;

begin
  WriteLn('==========================================');
  WriteLn('    CRYPTS OF PASCALIA');
  WriteLn('    Character Creation');
  WriteLn('==========================================');
  WriteLn;

  { --- Step 1: Enter Name --- }
  Write('Enter your character''s name: ');
  ReadLn(charName);

  if charName = '' then
  begin
    WriteLn('A hero must have a name! Defaulting to "Adventurer".');
    charName := 'Adventurer';
  end
  else if Length(charName) > MAX_NAME_LEN then
  begin
    WriteLn('Name too long — truncating to ', MAX_NAME_LEN, ' characters.');
    charName := Copy(charName, 1, MAX_NAME_LEN);
  end;

  WriteLn;
  WriteLn('Welcome, ', charName, '!');
  WriteLn;

  { --- Step 2: Choose Class --- }
  WriteLn('Choose your class:');
  WriteLn('  1. Warrior  — Strong and tough, masters of melee combat');
  WriteLn('  2. Mage     — Brilliant and perceptive, wielders of arcane power');
  WriteLn('  3. Rogue    — Quick and cunning, experts in stealth and traps');
  WriteLn('  4. Cleric   — Wise and resilient, channels of divine healing');
  WriteLn;
  Write('Enter class (1-4): ');
  ReadLn(classChoice);
  WriteLn;

  { CASE for class selection and base attribute assignment }
  case classChoice of
    1: begin
         className    := 'Warrior';
         strength     := 14;
         intelligence := 8;
         dexterity    := 10;
         constitution := 13;
       end;
    2: begin
         className    := 'Mage';
         strength     := 7;
         intelligence := 15;
         dexterity    := 10;
         constitution := 8;
       end;
    3: begin
         className    := 'Rogue';
         strength     := 9;
         intelligence := 10;
         dexterity    := 15;
         constitution := 8;
       end;
    4: begin
         className    := 'Cleric';
         strength     := 10;
         intelligence := 12;
         dexterity    := 8;
         constitution := 14;
       end;
  else
    begin
      WriteLn('Invalid class selection. Defaulting to Warrior.');
      className    := 'Warrior';
      strength     := 14;
      intelligence := 8;
      dexterity    := 10;
      constitution := 13;
    end;
  end;

  WriteLn(charName, ' the ', className, '!');
  WriteLn;
  WriteLn('Base attributes:');
  WriteLn('  STR: ', strength:2, '   INT: ', intelligence:2);
  WriteLn('  DEX: ', dexterity:2, '   CON: ', constitution:2);
  WriteLn;

  { --- Step 3: Distribute Bonus Points --- }
  bonusRemaining := BONUS_POINTS;

  WriteLn('You have ', bonusRemaining, ' bonus points to distribute.');
  WriteLn;

  { Loop until all bonus points are spent or the player is done }
  repeat
    WriteLn('Bonus points remaining: ', bonusRemaining);
    WriteLn('  1. Strength     (', strength, ')');
    WriteLn('  2. Intelligence (', intelligence, ')');
    WriteLn('  3. Dexterity    (', dexterity, ')');
    WriteLn('  4. Constitution (', constitution, ')');
    WriteLn('  0. Done (keep remaining points unspent)');
    WriteLn;
    Write('Allocate to which attribute? ');
    ReadLn(attrChoice);

    if attrChoice = 0 then
    begin
      WriteLn('Keeping ', bonusRemaining, ' points unspent.');
      bonusRemaining := 0;  { This will end the loop }
    end
    else if (attrChoice < 1) or (attrChoice > 4) then
    begin
      WriteLn('Invalid choice. Please enter 0-4.');
    end
    else
    begin
      Write('How many points to add? (1-', bonusRemaining, '): ');
      ReadLn(pointsToAdd);

      { Validate points to add }
      if (pointsToAdd < 1) or (pointsToAdd > bonusRemaining) then
      begin
        WriteLn('Invalid amount. You have ', bonusRemaining, ' points available.');
      end
      else
      begin
        { Determine current value of selected attribute }
        case attrChoice of
          1: currentValue := strength;
          2: currentValue := intelligence;
          3: currentValue := dexterity;
          4: currentValue := constitution;
        end;

        { Check if adding points would exceed maximum }
        if currentValue + pointsToAdd > MAX_ATTR then
        begin
          WriteLn('Cannot exceed ', MAX_ATTR, '! Current value is ',
                  currentValue, ', max addition is ', MAX_ATTR - currentValue, '.');
        end
        else
        begin
          { Apply the bonus — CASE to select which attribute to modify }
          case attrChoice of
            1: begin
                 strength := strength + pointsToAdd;
                 WriteLn('Strength increased to ', strength, '.');
               end;
            2: begin
                 intelligence := intelligence + pointsToAdd;
                 WriteLn('Intelligence increased to ', intelligence, '.');
               end;
            3: begin
                 dexterity := dexterity + pointsToAdd;
                 WriteLn('Dexterity increased to ', dexterity, '.');
               end;
            4: begin
                 constitution := constitution + pointsToAdd;
                 WriteLn('Constitution increased to ', constitution, '.');
               end;
          end;
          bonusRemaining := bonusRemaining - pointsToAdd;
        end;
      end;
    end;
    WriteLn;
  until bonusRemaining = 0;

  { --- Step 4: Display Final Character Sheet --- }
  WriteLn;
  WriteLn('==========================================');
  WriteLn('    CHARACTER SHEET');
  WriteLn('==========================================');
  WriteLn('  Name:    ', charName);
  WriteLn('  Class:   ', className);
  WriteLn('------------------------------------------');
  WriteLn('  STR: ', strength:2, '   INT: ', intelligence:2);
  WriteLn('  DEX: ', dexterity:2, '   CON: ', constitution:2);
  WriteLn('------------------------------------------');

  { Calculate and display a simple combat summary }
  WriteLn;
  WriteLn('  Combat Summary:');

  { Attack bonus based on class }
  case classChoice of
    1: WriteLn('    Attack bonus: +', (strength - 10) div 2,
               ' (Strength-based)');
    2: WriteLn('    Attack bonus: +', (intelligence - 10) div 2,
               ' (Intelligence-based)');
    3: WriteLn('    Attack bonus: +', (dexterity - 10) div 2,
               ' (Dexterity-based)');
    4: WriteLn('    Attack bonus: +', (intelligence - 10) div 2,
               ' (Intelligence-based)');
  end;

  WriteLn('    Hit points:   ', 8 + (constitution - 10) div 2);

  { Special ability hint based on class }
  WriteLn;
  Write('  Special: ');
  case classChoice of
    1: WriteLn('Power Strike — double STR bonus on next attack');
    2: WriteLn('Fireball — INT-based area damage');
    3: WriteLn('Backstab — triple DEX bonus from stealth');
    4: WriteLn('Heal — restore CON/2 hit points');
  end;

  WriteLn;
  WriteLn('==========================================');

  { Confirmation }
  Write('Accept this character? (Y/N): ');
  ReadLn(confirmed);

  if (confirmed = 'Y') or (confirmed = 'y') then
    WriteLn('Character saved! Prepare to enter the Crypts...')
  else
    WriteLn('Character discarded. Run the program again to try a new build.');

  WriteLn;
  WriteLn('[ End of Character Creation — Chapter 5 Preview ]');
  WriteLn('[ In Chapter 6, we add a game loop. ]');
  WriteLn('[ In Chapter 11, we store this in a record. ]');
end.

Sample Session

==========================================
    CRYPTS OF PASCALIA
    Character Creation
==========================================

Enter your character's name: Elara

Welcome, Elara!

Choose your class:
  1. Warrior  — Strong and tough, masters of melee combat
  2. Mage     — Brilliant and perceptive, wielders of arcane power
  3. Rogue    — Quick and cunning, experts in stealth and traps
  4. Cleric   — Wise and resilient, channels of divine healing

Enter class (1-4): 3

Elara the Rogue!

Base attributes:
  STR:  9   INT: 10
  DEX: 15   CON:  8

You have 5 bonus points to distribute.

Bonus points remaining: 5
  1. Strength     (9)
  2. Intelligence (10)
  3. Dexterity    (15)
  4. Constitution (8)
  0. Done (keep remaining points unspent)

Allocate to which attribute? 3
How many points to add? (1-5): 3
Dexterity increased to 18.

Bonus points remaining: 2
  1. Strength     (9)
  2. Intelligence (10)
  3. Dexterity    (18)
  4. Constitution (8)
  0. Done (keep remaining points unspent)

Allocate to which attribute? 3
How many points to add? (1-2): 1
Cannot exceed 18! Current value is 18, max addition is 0.

Bonus points remaining: 2
  1. Strength     (9)
  2. Intelligence (10)
  3. Dexterity    (18)
  4. Constitution (8)
  0. Done (keep remaining points unspent)

Allocate to which attribute? 4
How many points to add? (1-2): 2
Constitution increased to 10.

==========================================
    CHARACTER SHEET
==========================================
  Name:    Elara
  Class:   Rogue
------------------------------------------
  STR:  9   INT: 10
  DEX: 18   CON: 10
------------------------------------------

  Combat Summary:
    Attack bonus: +4 (Dexterity-based)
    Hit points:   8

  Special: Backstab — triple DEX bonus from stealth

==========================================
Accept this character? (Y/N): Y
Character saved! Prepare to enter the Crypts...

[ End of Character Creation — Chapter 5 Preview ]
[ In Chapter 6, we add a game loop. ]
[ In Chapter 11, we store this in a record. ]

Analysis

Decision Constructs Used

Construct Purpose Example
if..then Validate name not empty if charName = '' then
if..then..else if Handle name too long vs. normal Name length validation
case..of..else Class selection with default fallback Class choice 1-4
Nested if inside else Validate points before applying bonus Points validation chain
case (no else) Attribute selection for reading currentValue lookup
case (no else) Attribute selection for writing Applying bonus points
Compound Boolean (or) Confirm with Y or y (confirmed = 'Y') or (confirmed = 'y')
Compound Boolean (or) Invalid choice range check (attrChoice < 1) or (attrChoice > 4)

CASE as a Dispatcher vs. CASE as a Lookup

This program uses case in two distinct roles:

  1. Dispatcher — the class selection case assigns multiple variables and runs a block of code. This is the traditional menu-dispatch pattern.

  2. Lookup — the currentValue assignment uses case to read the value of one of four variables. This is a lookup pattern, almost like indexing into a set of variables. (In Chapter 9, when we learn arrays, we can replace this with attributes[attrChoice] — much cleaner.)

The Repetition Problem (Again)

Like the tax calculator, this program has repetitive code: the attribute selection case appears three times (lookup, validation, assignment). Tomas recognizes this is inelegant, but without procedures (Chapter 7) or arrays (Chapter 9), there is no way to eliminate it. The repetition serves as motivation for learning those tools.

What Comes Next

This character creation system will grow across the book:

  • Chapter 6 wraps the creation in a retry loop.
  • Chapter 7 extracts procedures: DisplayAttributes, ValidatePoints, ApplyBonus.
  • Chapter 9 replaces four attribute variables with an array.
  • Chapter 11 stores the entire character in a record type.
  • Chapter 12 replaces the class integer with an enumeration type.
  • Chapter 16 redesigns the character as a TCharacter class with methods.

Discussion Questions

  1. Input Validation. The program handles empty names and invalid class choices gracefully. What other edge cases should be validated? Consider: very long names, names with special characters, entering a letter where a number is expected.

  2. CASE vs. IF. The class selection uses case while the name validation uses if. Could the class selection be written as an if..else if chain? Could the name validation be written as a case? For each, explain which is more appropriate and why.

  3. Default Values. When the class selection fails, the program defaults to Warrior rather than asking again. Is this good design? What would be a better approach? (Hint: Chapter 6 introduces loops.)

  4. The readability trade-off. The attribute bonus section has significant nesting: the repeat loop contains an if..else if chain, which contains another if..else for validation, which contains a case. Count the maximum nesting depth. At what depth does code become unacceptable? How would you restructure this?

  5. Fairness Check. Are the four classes balanced? Sum the base attributes for each class (e.g., Warrior: 14+8+10+13 = 45). Are the totals equal? Should they be? What game design principle does this relate to?

Extension Challenge

Add a fifth class, "Ranger," with balanced attributes (STR: 12, INT: 10, DEX: 12, CON: 11). Then add a class-specific validation rule: Mages cannot have STR above 12, Warriors cannot have INT above 14, and Rogues cannot have CON above 14. Display a warning if the player tries to violate these constraints, but allow them to proceed ("Your Mage is surprisingly buff — unconventional, but allowed.").