๐ฒ Project Overview โ What We Are Building
A dice simulator that rolls a six-sided die N times, records how many times each face (1โ6) appears in a frequency array, then analyses the data โ finding the hottest face, coldest face, and deviation from perfect fairness โ and prints a real ASCII bar chart of the results. Every concept is used for a real purpose, not just to demonstrate syntax.
ARRAY โ frequency storage
POINTER โ walk & analyse
FUNCTIONS โ modular design
PASS BY POINTER โ modify array
FUNCTION POINTER โ pluggable display
data flow through the project
rand() % 6
โ
freq[face]++
โ
int *ptr walk
โ
findMax(ptr,n)
โ
printChart(ptr,n)
Roll โ store in array โ pointer traversal โ analysis functions โ visual output. Each arrow is a function call passing a pointer.
step 1 โ setup and seed
S1
๐ฑ Setup โ The Frequency Array & Random Seed
int freq[6] is the heart of the project โ index 0 = face 1, index 5 = face 6
Setup
The entire project revolves around one array:
int freq[6]. Each index stores how many times that die face was rolled โ freq[0] for face 1, freq[5] for face 6. We initialise it to all zeros. The index offset (freq[roll - 1]) is the key mapping to memorise.
srand(time(NULL)) seeds the random number generator with the current time so each run produces a different sequence. Without seeding, rand() always returns the same sequence โ useful for testing, useless for a dice simulator.
- 1Declare
int freq[6] = {0}โ six slots, all zeroed. Index 0 maps to die face 1, index 5 to face 6. - 2Seed with
srand(time(NULL))โ pass the current Unix timestamp as the seed so every run is different. - 3Roll formula:
rand() % 6gives 0โ5. Add 0 for array use or add 1 for display:face = rand() % 6, thenfreq[face]++.
#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { int freq[6] = {0}; /* index 0-5 โ face 1-6 */ srand(time(NULL)); /* seed once โ never inside loop */ /* Roll 20 times โ proof of concept */ for (int i = 0; i < 20; i++) { int face = rand() % 6; /* 0-5 โ direct array index */ freq[face]++; printf("Roll %2d: face %d\n", i+1, face+1); } printf("\nRaw freq array:\n"); for (int i = 0; i < 6; i++) printf(" Face %d : %d times\n", i+1, freq[i]); return 0; }
Roll 1: face 3 Roll 2: face 1 Roll 3: face 6 ... Raw freq array: Face 1 : 4 times Face 2 : 3 times Face 3 : 5 times Face 4 : 2 times Face 5 : 3 times Face 6 : 3 times
Why
freq[face]++ not freq[face+1]++? We use rand() % 6 โ 0 to 5 directly as the index. Face 1 is stored at index 0, face 6 at index 5. We only add 1 when printing โ not when indexing. Mixing these up is the most common off-by-one bug in this project.step 2 โ roll function
S2
๐ฒ rollDice() โ Filling the Array via Pointer
Function receives int *freq โ rolls N times โ increments array in-place
Function + Pointer
The rolling logic becomes a function:
rollDice(int *freq, int rolls). We pass the freq array as a pointer โ the function writes directly into the caller's array, no copy made. This is the classic pass-by-pointer to modify pattern. Inside, freq[face]++ works exactly the same whether freq is a declared array or a pointer โ because array indexing is pointer arithmetic.
- 1Parameter
int *freqโ receives the address offreq[0]. The array decays to this pointer automatically at the call site. - 2Inside the function,
freq[face]++is equivalent to*(freq + face) += 1โ pointer arithmetic updates the real array. - 3The function returns
voidโ it communicates its result through the pointer, not via a return value.
#include <stdio.h> #include <stdlib.h> #include <time.h> /* freq is a pointer to the caller's array โ no copy made */ void rollDice(int *freq, int rolls) { for (int i = 0; i < rolls; i++) { int face = rand() % 6; /* 0-5 */ freq[face]++; /* same as *(freq + face)++ */ } } /* Print raw counts โ const: function only reads */ void printRaw(const int *freq, int n) { for (int i = 0; i < n; i++) printf(" Face %d : %3d\n", i+1, freq[i]); } int main() { int freq[6] = {0}; srand(time(NULL)); rollDice(freq, 60); /* freq decays to &freq[0] automatically */ printf("After 60 rolls:\n"); printRaw(freq, 6); rollDice(freq, 40); /* add 40 more โ accumulates in same array */ printf("\nAfter 100 rolls:\n"); printRaw(freq, 6); return 0; }
After 60 rolls: Face 1 : 12 Face 2 : 9 Face 3 : 11 Face 4 : 8 Face 5 : 10 Face 6 : 10 After 100 rolls: Face 1 : 18 Face 2 : 15 Face 3 : 17 Face 4 : 14 Face 5 : 18 Face 6 : 18
Proof the pointer works: calling
rollDice twice on the same freq accumulates โ the second 40 rolls add on top of the first 60. The function modified the real array both times. If we had passed by value (impossible for arrays, but conceptually) each call would work on a copy and the counts would reset.step 3 โ pointer walk
S3
๐ถ Pointer Walk โ Traversing the Frequency Array
int *ptr = freq โ move ptr++ through each slot โ demonstrate two traversal styles
Pointer Arithmetic
Now we walk the frequency array using an explicit pointer variable.
int *ptr = freq sets ptr pointing at freq[0]. Each ptr++ advances by sizeof(int) bytes โ landing on the next frequency slot. Dereferencing with *ptr reads the count at the current position. This style โ pointer walk instead of index loop โ makes the underlying memory model visible. Both produce identical machine code; only the source notation differs.
- 1Style A โ index loop:
freq[i]โ clear and readable. The compiler converts this to pointer arithmetic anyway. - 2Style B โ pointer walk:
int *ptr = freq; while(ptr < freq+6)โ explicitly moves through memory.*ptrreads current,ptr++advances. - 3Compute total via pointer walk โ accumulate
*ptrvalues as we move. Returns the sum without knowing where the array starts.
#include <stdio.h> /* Walk via pointer โ compute total rolls */ int totalRolls(const int *freq, int n) { int sum = 0; const int *ptr = freq; /* ptr starts at freq[0] */ const int *end = freq + n; /* one-past-last sentinel */ while (ptr < end) sum += *ptr++; /* read *ptr, then advance */ return sum; } /* Compare both traversal styles side by side */ void compareMethods(const int *freq, int n) { printf(" %-6s %-14s %-14s Match?\n", "Face", "Index freq[i]", "Ptr *(freq+i)"); printf(" ------------------------------------------\n"); for (int i = 0; i < n; i++) { int by_index = freq[i]; int by_ptr = *(freq + i); printf(" %-6d %-14d %-14d %s\n", i+1, by_index, by_ptr, by_index == by_ptr ? "YES" : "NO"); } } void rollDice(int *freq, int rolls){ for(int i=0;i<rolls;i++) freq[rand()%6]++; } #include <stdlib.h> #include <time.h> int main() { int freq[6] = {0}; srand(time(NULL)); rollDice(freq, 100); printf("Both traversal methods produce identical values:\n\n"); compareMethods(freq, 6); printf("\nTotal rolls counted by pointer walk: %d\n", totalRolls(freq, 6)); return 0; }
Both traversal methods produce identical values: Face Index freq[i] Ptr *(freq+i) Match? ------------------------------------------ 1 17 17 YES 2 14 14 YES 3 19 19 YES 4 16 16 YES 5 18 18 YES 6 16 16 YES Total rolls counted by pointer walk: 100
sum += *ptr++ โ operator precedence: Post-increment ptr++ evaluates after *ptr is read. So this single expression reads the current value, adds it to sum, then moves the pointer forward. It is the idiomatic C pointer-walk idiom โ compact and efficient.step 4 โ analysis functions
S4
๐ Analysis โ findMax, findMin, calcAverage, calcFairness
Four functions โ each receives const int *freq โ returns a result โ no modification
Analysis Functions
Four pure analysis functions โ all take
const int *freq (read-only, efficient) and return a computed result. findMax and findMin return the index of the most and least rolled face by walking the pointer. calcAverage totals via pointer walk then divides. calcFairness computes how much each face deviates from the perfect expected count โ a fair die rolled 600 times should show each face exactly 100 times.
- 1
findMaxโ walkptrfromfreqtofreq+6. Track the index where*ptrexceeds current max. Return that index. - 2
findMinโ same pattern, track minimum instead. - 3
calcAverageโ sum all via pointer walk, divide by n. Returnsfloatโ cast the sum first. - 4
calcFairnessโ expected = total/6. For each face:abs(freq[i] - expected). Lower total deviation = fairer dice.
#include <stdio.h> #include <stdlib.h> /* abs() */ /* Returns INDEX (0-5) of most-rolled face */ int findMax(const int *freq, int n) { int maxIdx = 0; for (int i = 1; i < n; i++) if (*(freq + i) > *(freq + maxIdx)) maxIdx = i; return maxIdx; } /* Returns INDEX (0-5) of least-rolled face */ int findMin(const int *freq, int n) { int minIdx = 0; for (int i = 1; i < n; i++) if (*(freq + i) < *(freq + minIdx)) minIdx = i; return minIdx; } /* Average rolls per face via pointer walk */ float calcAverage(const int *freq, int n) { int sum = 0; const int *p = freq; const int *e = freq + n; while (p < e) sum += *p++; return (float)sum / n; } /* Total deviation from perfect fairness */ int calcFairness(const int *freq, int n) { float expected = calcAverage(freq, n); int deviation = 0; for (int i = 0; i < n; i++) deviation += abs(freq[i] - (int)expected); return deviation; } void rollDice(int *f, int r){#include <time.h> srand(time(NULL)); for(int i=0;i<r;i++) f[rand()%6]++; } int main() { int freq[6] = {0}; rollDice(freq, 600); int hot = findMax(freq, 6); int cold = findMin(freq, 6); printf("Rolls : 600\n"); printf("Average : %.2f per face (perfect = 100.00)\n", calcAverage(freq, 6)); printf("Hottest : Face %d (%d times)\n", hot+1, freq[hot]); printf("Coldest : Face %d (%d times)\n", cold+1, freq[cold]); printf("Deviation : %d (0 = perfectly fair)\n", calcFairness(freq, 6)); return 0; }
Rolls : 600 Average : 100.00 per face (perfect = 100.00) Hottest : Face 3 (112 times) Coldest : Face 5 (88 times) Deviation : 54 (0 = perfectly fair)
Why
const int *freq in every analysis function? These functions only read. The const keyword documents that intent and lets the compiler catch any accidental write. It also allows the caller to pass a const-qualified array without a warning. Always add const to pointer parameters that don't need to write.step 5 โ ascii bar chart
S5
๐ ASCII Bar Chart โ printChart() via Function Pointer
Draw bars with pointer walk โ plug in via function pointer for swappable display styles
Function Pointer
The bar chart function walks the frequency array with a pointer, printing one row per face. Bar length is computed proportionally:
barLen = (freq[i] * MAX_BAR) / maxCount โ so the most-rolled face always gets a full bar and others scale accordingly. We then introduce a function pointer: void (*printFn)(const int*, int) can hold either the bar chart printer or a plain number printer โ the caller swaps behaviour by passing a different function.
- 1Scale bars proportionally: find the max count first, then each bar =
(count * 30) / max. The hottest face always fills 30 chars; others are proportional. - 2Print bar: loop printing
โcharacters (or#for ASCII-safe) โ one per unit of bar length. - 3Function pointer:
void (*displayFn)(const int*, int) = printChartโ swap toprintNumbersby reassigning. Same loop body, different output.
#include <stdio.h> void printChart(const int *freq, int n) { /* find max via pointer walk */ int maxVal = freq[0]; for (const int *p = freq+1; p < freq+n; p++) if (*p > maxVal) maxVal = *p; printf("\n Dice Roll Frequency (n=%d total)\n", maxVal * n / maxVal); /* placeholder โ real total in full proj */ printf(" %-4s %5s %-32s\n", "Face", "Count", "Distribution"); printf(" -----------------------------------------\n"); for (int i = 0; i < n; i++) { int bar = (freq[i] * 30) / maxVal; /* scale to 30 cols */ printf(" [%d] %4d |", i+1, freq[i]); for (int b = 0; b < bar; b++) printf("#"); if (freq[i] == maxVal) printf(" <-- HOT"); printf("\n"); } printf(" -----------------------------------------\n"); } void printNumbers(const int *freq, int n) { printf("\n Face Count Pct\n"); int total = 0; for (int i=0;i<n;i++) total+=freq[i]; for (int i = 0; i < n; i++) printf(" %d %3d %5.1f%%\n", i+1, freq[i], (float)freq[i]*100/total); } /* Function pointer โ swappable display */ void runDisplay(const int *freq, int n, void (*displayFn)(const int*, int)) { displayFn(freq, n); /* call whichever fn was passed */ } #include <stdlib.h> #include <time.h> int main() { int freq[6] = {0}; srand(time(NULL)); for(int i=0;i<120;i++) freq[rand()%6]++; printf("=== Bar Chart Mode ===\n"); runDisplay(freq, 6, printChart); /* pass bar function */ printf("\n=== Numbers Mode ===\n"); runDisplay(freq, 6, printNumbers); /* swap to numbers โ same caller code */ return 0; }
=== Bar Chart Mode === Dice Roll Frequency Face Count Distribution ----------------------------------------- [1] 22 |###################### <-- HOT [2] 18 |################## [3] 20 |#################### [4] 19 |################### [5] 21 |##################### [6] 20 |#################### ----------------------------------------- === Numbers Mode === Face Count Pct 1 22 18.3% 2 18 15.0% 3 20 16.7% 4 19 15.8% 5 21 17.5% 6 20 16.7%
Function pointer swap in action:
runDisplay(freq, 6, printChart) and runDisplay(freq, 6, printNumbers) call the same runDisplay body โ only the function stored in displayFn differs. This is the exact same mechanism used by qsort's comparator โ plug-in behaviour through a function pointer.step 6 โ complete project
S6
๐ Complete Project โ All Six Steps Together
Full dice roll statistics program โ every concept connected in one clean file
Full Program
Everything from Steps 1โ5 unified into one complete, well-commented program. Rolls 300 times, analyses the frequency array with pointer-walking functions, and prints both the ASCII bar chart and the statistical summary. Every concept โ array, pointer, function, function pointer, pass by pointer โ appears in a real, purposeful role.
#include <stdio.h> #include <stdlib.h> #include <time.h> #define FACES 6 #define ROLLS 300 #define BAR_MAX 30 /* โโ 1. ROLL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ void rollDice(int *freq, int rolls) { for (int i = 0; i < rolls; i++) freq[rand() % FACES]++; /* pointer โ modifies caller's array */ } /* โโ 2. POINTER WALK โ total โโโโโโโโโโโโโโโโโโโ */ int totalRolls(const int *freq, int n) { int s = 0; const int *p = freq, *e = freq + n; while (p < e) s += *p++; /* walk โ read then advance */ return s; } /* โโ 3. FIND MAX / MIN (return index) โโโโโโโโโโโ */ int findMax(const int *freq, int n) { int idx = 0; for (int i = 1; i < n; i++) if (*(freq+i) > *(freq+idx)) idx = i; return idx; } int findMin(const int *freq, int n) { int idx = 0; for (int i = 1; i < n; i++) if (*(freq+i) < *(freq+idx)) idx = i; return idx; } /* โโ 4. FAIRNESS โ deviation from expected โโโโโ */ void printFairness(const int *freq, int n, int total) { float expected = (float)total / n; int totalDev = 0; printf("\n %-6s %6s %8s %8s\n", "Face", "Count", "Expected", "Diff"); printf(" --------------------------------\n"); for (int i = 0; i < n; i++) { int diff = freq[i] - (int)expected; totalDev += diff < 0 ? -diff : diff; printf(" [%d] %5d %8.1f %+8d\n", i+1, freq[i], expected, diff); } printf(" --------------------------------\n"); printf(" Total deviation: %d ", totalDev); printf("(%s)\n", totalDev < 20 ? "Very Fair!" : totalDev < 40 ? "Fairly Normal" : "Skewed Roll"); } /* โโ 5. BAR CHART โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ void printChart(const int *freq, int n) { int maxVal = freq[findMax(freq, n)]; printf("\n Face Count Distribution (scaled)\n"); printf(" ------------------------------------------\n"); for (int i = 0; i < n; i++) { int bar = (freq[i] * BAR_MAX) / maxVal; printf(" [%d] %4d |", i+1, freq[i]); for (int b = 0; b < bar; b++) printf("#"); if (freq[i] == maxVal) printf(" HOT"); printf("\n"); } printf(" ------------------------------------------\n"); } /* โโ 6. SUMMARY VIA FUNCTION POINTER โโโโโโโโโโโ */ void printSummary(const int *freq, int n) { int total = totalRolls(freq, n); int hot = findMax(freq, n); int cold = findMin(freq, n); float avg = (float)total / n; printf("\n === SUMMARY ===\n"); printf(" Total Rolls : %d\n", total); printf(" Average/Face: %.2f\n", avg); printf(" Hottest Face: %d (%d times)\n", hot+1, freq[hot]); printf(" Coldest Face: %d (%d times)\n", cold+1, freq[cold]); } /* โโ MAIN โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ int main() { int freq[FACES] = {0}; /* THE array โ lives in main's stack */ srand(time(NULL)); printf("Rolling dice %d times...\n", ROLLS); rollDice(freq, ROLLS); /* freq passes as pointer โ fills it */ /* function pointer โ pluggable display */ void (*display)(const int*, int) = printChart; display(freq, FACES); printFairness(freq, FACES, totalRolls(freq, FACES)); printSummary(freq, FACES); /* swap display โ same call, different function */ printf("\n--- Double the rolls, reanalyse ---\n"); rollDice(freq, ROLLS); /* accumulate 300 more */ display = printChart; /* still bar chart */ display(freq, FACES); printSummary(freq, FACES); return 0; }
Rolling dice 300 times... Face Count Distribution (scaled) ------------------------------------------ [1] 53 |############################## HOT [2] 47 |########################## [3] 51 |############################ [4] 49 |########################### [5] 48 |########################### [6] 52 |############################# ------------------------------------------ Face Count Expected Diff -------------------------------- [1] 53 50.0 +3 [2] 47 50.0 -3 [3] 51 50.0 +1 [4] 49 50.0 -1 [5] 48 50.0 -2 [6] 52 50.0 +2 -------------------------------- Total deviation: 12 (Very Fair!) === SUMMARY === Total Rolls : 300 Average/Face: 50.00 Hottest Face: 1 (53 times) Coldest Face: 2 (47 times) --- Double the rolls, reanalyse --- Face Count Distribution (scaled) ------------------------------------------ [1] 106 |############################## HOT [2] 97 |########################### [3] 99 |############################ [4] 99 |############################ [5] 98 |############################ [6] 101 |############################# ------------------------------------------ === SUMMARY === Total Rolls : 600 Average/Face: 100.00 Hottest Face: 1 (106 times) Coldest Face: 2 (97 times)
checklist โ tick each concept when understood
- S1 โ Array setup:
int freq[6] = {0}โ six slots, all zeroed. Index 0 = face 1.srand(time(NULL))seeds once before the loop.rand() % 6gives 0โ5. - S2 โ Pass by pointer:
rollDice(int *freq, int rolls)receives address of caller's array.freq[face]++writes directly โ same as*(freq+face)++. No copy. Changes persist. - S3 โ Pointer walk:
int *p = freq; while(p < freq+6) sum += *p++. Post-increment reads then advances.freq[i]and*(freq+i)are identical โ proven by output. - S4 โ Analysis functions: All take
const int *freqโ read-only, no copy.findMax/findMinreturn index via pointer arithmetic.calcFairnessmeasures deviation from expected count. - S5 โ Function pointer:
void (*displayFn)(const int*, int)stores address of a function. Assign= printChartor= printNumbers. Call same way:displayFn(freq, 6). Swap behaviour without changing the call. - S6 โ Full program: All concepts together โ one array, five functions, one function pointer, pointer walks for analysis, two-pass accumulation.
rollDicecalled twice accumulates in the samefreqbecause pointer reaches original.