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:
- Enter name — validate it is not empty.
- Choose class — present a numbered menu, use
caseto process the selection. - Assign base attributes — each class gets different starting values.
- Distribute bonus points — the player gets 5 extra points to allocate. Validate that each allocation does not push an attribute above 18.
- 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:
-
Dispatcher — the class selection
caseassigns multiple variables and runs a block of code. This is the traditional menu-dispatch pattern. -
Lookup — the
currentValueassignment usescaseto 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 withattributes[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
recordtype. - Chapter 12 replaces the class integer with an enumeration type.
- Chapter 16 redesigns the character as a
TCharacterclass with methods.
Discussion Questions
-
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.
-
CASE vs. IF. The class selection uses
casewhile the name validation usesif. Could the class selection be written as anif..else ifchain? Could the name validation be written as acase? For each, explain which is more appropriate and why. -
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.)
-
The readability trade-off. The attribute bonus section has significant nesting: the
repeatloop contains anif..else ifchain, which contains anotherif..elsefor validation, which contains acase. Count the maximum nesting depth. At what depth does code become unacceptable? How would you restructure this? -
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.").