10.2c — Decks: Shuffle, Draw & Traverse
The starter's Shuffle() is EMPTY — check your copy, and
count the methods while you are there, because there are two of them.
That emptiness is a gift: the single most exam-famous algorithm of the
year, left in the seat where you sit. Today you implement the shuffle,
wire the draw the starter never had, and traverse a hand to render it.
The starter hands you a card game whose Shuffle is empty twice over, whose deck can never reach a hand, and whose hands are therefore always empty lists. "How do I shuffle a deck and draw from it?" is not one problem. It is five: two methods sharing a name and answering to different list types, an algorithm that has to be exactly right or the game is quietly rigged, a swap written in the one order that does not lose a card, a move between two lists that has to survive an empty deck, and a loop that turns list positions into screen positions.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Notice the same method name is written twice, for two different list types
Shufflehas two overloads in the starter — one takingList<Card_data>, the deck of ScriptableObject assets from 10.2b, and one takingList<Card>, the components on screen. C# picks between them from the type of the argument at the call site, andDeal()decides which one it reaches by what it passes.Read: the twoShufflesignatures inAssets/Scripts/GameManager.cs, the two calls to them insideDeal(), and theList<Card_data>/List<Card>split in the field list at the top of that file — the same six Lists you counted in 10.2a. - write this partRead: the
RANDOM(a, b)row of the exam reference sheet, which prints the C# translation and warns that Unity is exclusive at the top for ints — and pattern 4, "Swap (the shuffle's spine)", in the Lists & Arrays reference, which names Fisher-Yates and says what makes it fair. - Swap the two slots through a temp, in the order that does not lose a cardThree statements, and the order is the whole of it: copy
list[i]intotempfirst, then writelist[r]intolist[i], then writetempintolist[r]. The AP pseudocode writes exactly the same three lines —temp <- aList[i],aList[i] <- aList[r],aList[r] <- temp.Read: pattern 4, "Swap (the shuffle's spine)", in the Lists & Arrays reference — the AP and C# columns side by side — and the same page's warning about which statement has to come first. - Move the top card out of the deck and into the hand, and refuse when there is noneRead
deck[0], hand the data to a card component,Addthat component to the hand, andRemoveAt(0)from the deck — in that order, because removing first changes what index 0 even is. The guard in front of all of it is the boundary case:deck[0]on an empty list is the crash 10.2a's socratic asked you to imagine at draw #31.Read: theAPPEND(aList, item)andREMOVE(aList, i)rows of the Lists & Arrays reference, that page's "Bug 2 — the bounds", and the socratic in 10.2a that asked what breaks at draw #31 if you never remove. - Traverse the hand by index to place every cardA
forloop from 0 toCount - 1, and the loop counter IS the screen position:imultiplies the spacing. The hand literally spreads because of a counter. Aforeachvisits the same cards and can do nothing with their places, because it never has ani.Read: theFOR i FROM 1 TO LENGTHrow of the Lists & Arrays reference, and patterns 1 and 2 on the same page — traverse to total, and traverse with a filter — which are the same loop with a different body.
Three of those five parts are things you have already typed. Reading the file to see which overload a call reaches is the two-minute read that opens every jam day; the three swap statements are pattern 4 from 10.2a's lists; and moving a card from one list to another is RemoveAt plus Add, which is the AP's REMOVE plus APPEND and which 10.2a already made you translate on sight. What is left is the random slot and the empty-deck guard — and the random slot is the reason this is a guided build rather than a full walkthrough. It is one line, and it carries three separate things at once: AP counts list slots from 1 and C# from 0, the exam's RANDOM(a, b) is inclusive at both ends and Unity's int version is not, and the word Random in this template names two different classes, so the call has to say which.
The test: could you look this part up on its own, right now, in one search? "Shuffle a deck" cannot — it is an algorithm, a list operation and a boundary case wearing one name. "How does C# Remove an item from a List" can, and you have already read the row that answers it.
Your turn
The shuffle is the one algorithm in this unit whose bug is invisible in a single play: a silently biased deck deals the same card first again and again and still looks random. How would you test for that — and what did 9.5 teach you about believing a claim about randomness?
Your hand is a List<Card> and your deck is a List<Card_data>. If you build the discard pile the starter already declares a field for, which overload does reshuffling it need, and what does your answer say about where a shuffle method belongs?
Fisher-Yates, in exam language. The AP pseudocode for a shuffle (know BOTH languages):
AP: C#:
FOR i FROM LENGTH(list) TO 2 for (int i = list.Count - 1; i > 0; i--)
r ← RANDOM(1, i) int r = UnityEngine.Random.Range(0, i + 1);
SWAP(list[i], list[r]) Card_data temp = list[i];
list[i] = list[r];
list[r] = temp;
Read it as a story: walk the list from the END; at each position, pick a random EARLIER-or-equal position; swap. Every card gets exactly one chance at every slot — that fairness (uniform distribution) is why this is THE textbook shuffle, not a vibe shuffle.
Two translations, and both bite. AP lists
are 1-indexed and C# lists are
0-indexed, so AP's slots LENGTH..2
are C#'s (Count-1)..1 — the same slots, written two
ways. And AP's RANDOM(a, b) includes BOTH ends
while Unity's Random.Range(a, b) for ints excludes
the top, which is why the C# column says i + 1.
Write Range(0, i) and the slot at i
can never be picked, the last card never moves, and the deck
still looks shuffled. The
exam reference sheet's
RANDOM row prints this same AP-to-C# difference; the
Lists & Arrays
reference prints the swap as pattern 4.
UnityEngine. out loud.
This template installs com.unity.mathematics —
not directly, but pinned in Packages/packages-lock.json
as a dependency of something else — and GameManager.cs
has using Unity.Mathematics; at the top. That
package defines Unity.Mathematics.Random;
UnityEngine defines Random too. So a
bare Random.Range(...) in that one file is
CS0104: 'Random' is an ambiguous reference and the
game will not build. Two legal fixes: fully qualify the call, or
delete the using Unity.Mathematics; line. The AP
exam never tests namespace resolution. Every real C# codebase
does.
Socratic
The lazy shuffle everyone writes first: repeatedly pick a random card and REMOVE it into a new list. It works — why is it worse than Fisher-Yates (cost per removal: the list shifts every element)? And why does the exam phrase "every possible order is equally likely" make the lazy version suspicious?
The question this answers“how do I shuffle a deck and draw from it?”
You are editing Assets/Scripts/GameManager.cs. This is the card game template's own script — the one with the two empty Shuffle methods and the working Deal(). You fill both overloads with Fisher-Yates, add the draw that carries a card from the deck to the hand, and add the loop that lays the hand out by index. Deal() gets rewired at the end. Exactly two lines are yours to write, and both of them are the new idea.
This build leaves 2 lines for you to write. Everything around them is given, so you always know the shape of what you are producing.
Step 1Read the caller, then count the methods it can reach
Two minutes of reading, no writing. Deal() is real working code, and it already tells you which list types this file deals in — read it before you touch anything.
void Deal()
{
Shuffle(player_deck);
Shuffle(ai_deck);
}
void Shuffle(List<Card_data> data_hand)
{
}
void Shuffle(List<Card> card_hand)- 1The starter's own method. It is already written, it already runs, and it already calls the method you are about to fill in.
- 3The first call.
player_deckis declared asList<Card_data>— the ScriptableObject assets from 10.2b — so C# resolves this call to the overload that takes aList<Card_data>and not the other one.C# chooses an overload from the argument's type, at the call site. That is the whole of overloading: one name, several signatures, and the compiler picks by what you pass. It means the two empty methods below are not duplicates — they are two different jobs sharing a word, and only one of them is reachable from this line. - 4The second call, and it is the same overload:
ai_deckis aList<Card_data>as well. So Deal() today reaches exactly one of the two empty methods. - 7Overload (a) — the deck, empty in the starter. This is the one Deal() calls.
- 10Overload (b) — the hand, also empty. Nothing in the starter calls it yet, because
player_handandai_handareList<Card>and nothing shuffles a hand today.Implement one and leave the other, and the other call is a silent no-op. No error, no warning, no message in the Console: the list comes back in exactly the order it went in. C# does not warn about an unused parameter, so an empty method body is a legal method that does nothing. That is the shape of this bug — aShuffle(player_hand)later in the unit that compiles, runs, and shuffles nothing, while the deck beside it works perfectly.
Step 2Fisher-Yates, walking from the end — and the one line you write
The algorithm the exam prints, in C#. Walk the list from the end; at each slot, pick a random earlier-or-equal slot; swap. One line is yours — the random pick — and it is the line where the AP and C# genuinely disagree.
void Shuffle(List<Card_data> data_hand)
{
for (int i = data_hand.Count - 1; i > 0; i--)
{
int r = ______________________________________;
Card_data temp = data_hand[i];
data_hand[i] = data_hand[r];
data_hand[r] = temp;
}
}- 1Overload (a) again, now with a body. The parameter is the list the caller passed, so this method never needs to know it is
player_deckrather thanai_deck. - 3The loop, and it runs backwards:
istarts at the last valid index (Count - 1) and stops when it reaches1. Read it next to the AP'sFOR i FROM LENGTH(list) TO 2— AP's slot 1 is C#'s slot 0, so AP's rangeLENGTH..2is C#'s(Count-1)..1, and the two loops visit the same slots in the same order.Why from the end, and why stop before 0. Working backwards means the slots behindiare still unshuffled and the slots ahead of it are already settled, which is what makes every ordering equally likely. And the loop stops at1, not0: when one card is left there is nothing to swap it with, because a single card is already in the only slot it can occupy. Swapping slot 0 with slot 0 would be a no-op — the AP writes that same stopping point asTO 2in 1-indexed slots. - Your turn5Your line — the whole right-hand side. It picks the slot to swap with: a random index from the start of the list up to and including
i. The declaration on the left is fixed —ris the name the three lines below read, so do not rename it — and everything from the=onward is yours to work out.Two numbers and a class name go into it. The first number is the bottom of the range: the first slot in the list, and C# counts slots from 0 while the AP counts from 1. The second is the top, written asiplus one — because the exam'sRANDOM(a, b)includes both ends and Unity's int version does not, which theRANDOM(a, b)row of the exam reference sheet spells out. The class name is the one theusinglines at the top of GameManager.cs have made ambiguous:Randomis defined by bothUnityEngineandUnity.Mathematicsin this template, so the call has to say which one it means. TheRANDOM(a, b)row and theLENGTH(aList)row of the Lists & Arrays reference give you the indexing rule.UnityEngine.Random.Range(0, i + 1)— and both halves of that matter. Thei + 1is the max-exclusive correction: Unity's int overload never returns its upper bound, soRange(0, i)returns0..i-1and the slot aticould never be its own partner on this pass. The last card in the list would then never move at all — a shuffle that is wrong in a way that still looks shuffled. TheUnityEngine.is the second half.com.unity.mathematicsis installed in this template (transitively — it is not inPackages/manifest.json, butPackages/packages-lock.jsonpins it at depth 2), and it definesUnity.Mathematics.Random;UnityEnginedefinesRandomtoo. A bareRandom.Range(...)in this file is thereforeerror CS0104: 'Random' is an ambiguous referenceand the script does not build. Qualifying the call is the fix that survives somebody adding thatusingback; deletingusing Unity.Mathematics;from the top of the file is the other legal fix, and it is one line you would have to explain to the next person. - 7The swap begins by copying the card at
isomewhere safe. This is pattern 4 from the Lists & Arrays reference, in this file's own names — and it isCard_data, because that is what this list holds.The temp is not optional and the order is not optional. Overwritedata_hand[i]before you have saved it and the card that was there is gone from the list entirely — the two statements below then write the same card into both slots. - 8The random slot's card moves into slot
i. Read it as the AP'saList[i] <- aList[r]. - 9And the saved card goes into the random slot. Read it as
aList[r] <- temp. The pair isSWAP(list[i], list[r])spread over three statements, exactly as the reference sheet prints it. - 11Closes overload (a), which now shuffles. Overload (b) is still an empty body — step 3 fills it in.
Step 3The same six lines for the other overload
Nothing new happens here, and that is the point: overload (b) is the loop and the swap again, with Card in place of Card_data. Type it out rather than copying, and notice that not one of the six lines changes shape.
void Shuffle(List<Card> card_hand)
{
for (int i = card_hand.Count - 1; i > 0; i--)
{
int r = UnityEngine.Random.Range(0, i + 1);
Card temp = card_hand[i];
card_hand[i] = card_hand[r];
card_hand[r] = temp;
}
}- 1Overload (b) — the hand.
List<Card>is the components, not the assets: the thing that is on screen.Both overloads are needed because the starter has two kinds of list, and it is not an accident. The deck isList<Card_data>because a deck is data you have not played yet; the hand isList<Card>because a hand is something you can see and drag. The moment a later lesson reshuffles a hand — or a discard pile back into a deck — it will call this overload, and until you fill it in that call does nothing at all. It will not tell you. - 3The identical loop. The only difference from overload (a) is the parameter name, which is the one thing that has to change:
data_handandcard_handare separate lists of separate types. - 5The line you wrote in step 2, unchanged — and here it is printed, because the shape is worth seeing twice. If your version in step 2 compiles and this one does not, the two are not the same line.A generic
Shuffle<T>would remove this duplication, and this is not the lesson for it. Generics are real C# and the exam never asks for one; two short methods are the honest version at this point in the year. - 6The swap again, and the only other word that changes:
Card, notCard_data. If you wroteCard_datahere the file will not compile, and the error will point at this line rather than at the list it came from.
Step 4Draw: carry the top card from the deck into the hand
The starter has no draw at all — which is why its hands stay empty. Four statements and one guard, and the guard is the second line you write.
void Draw(List<Card_data> deck, List<Card> hand, Vector3 where)
{
if (________________________________) return;
GameObject new_card = Instantiate(blank_card, where, Quaternion.identity, canvas.transform);
new_card.GetComponent<Card>().data = deck[0];
hand.Add(new_card.GetComponent<Card>());
deck.RemoveAt(0);
}- 1Three parameters, and every one of them is load-bearing.
deckis theList<Card_data>to draw from,handis theList<Card>to draw into, andwhereis the screen position the new card appears at. The method takes them as parameters instead of reaching forplayer_deckandplayer_handbecause the starter has two of each, and a draw that only worked for the player would have to be written twice. - Your turn3Your line — the guard, and the whole condition inside the parentheses. It answers one question: when is there nothing left to draw? The
if, thereturn;and the shape are given; what you write is the condition between the parentheses.Two pieces go inside. The first is the C# half of theLENGTH(aList)row in the Lists & Arrays reference — the property that answers "how many items", read off thedeckparameter this method was handed. The second is the question you are asking of it, and the same page's "Bug 2 — the bounds" is about reading an index a list does not have: this line is how you refuse before you get there.=is the wrong operator here and the file will not build with it — one=assigns, and you are asking.deck[0]on an empty list is not a card and not a null — it throws. Which exception it throws is worth reading off your own Console once: the guard clause is the entire difference between a jam build and a jam crash, and the same four lines are why a real card game can survive a player who discards everything and then draws. It also keeps the shuffle honest: the deck is empty exactly when every card has been drawn, which is the state 10.2a asked you to imagine at draw #31. - 5The starter's own Instantiate line, moved here from
Deal()verbatim — the blank card prefab, cloned atwhere, no rotation, parented to the canvas.Deal()no longer needs it, because it will call this method instead.This is why the card has to be created here and not later.Card.Start()readsdata.card_nameand the rest on the first frame after the object exists, so a visual that is instantiated and left with an emptydatathrows aNullReferenceExceptioninCard.Start()— from a line in a different file, which is why it is worth knowing about before you see it. - 6Hand the drawn card its data.
deck[0]is the top of the deck, and the top of a shuffled deck is a random card — which is the whole reason the shuffle had to be right.The hand holdsCard, so the new visual has to be one. ACard_datais an asset on disk; aCardis a component on a GameObject.GetComponent<Card>()is how you get from the clone you just made to the component that will be in the list. - 7The card joins the hand. This is the AP's
APPEND(hand, topCard)— and it is the line the starter never had, which is whyplayer_hand.Countis 0 in the template as shipped. - 8And the deck gives it up. This is the AP's
REMOVE(deck, 1)— index 1 in exam-speak, because the exam counts list slots from 1 and C# counts from 0.Remove LAST, never first.RemoveAt(0)shifts every remaining card down one slot, so doing it before the three lines above would leavedeck[0]pointing at the second card — you would draw the second card and quietly lose the first. The order in this method is not style; it is the difference between a draw and a bug.
Step 5Traverse the hand by index to lay it out
The hand is a list, so showing it means visiting every element — and because the loop counter is also the position, this is the loop that has to be an index loop rather than a foreach.
void LayoutHand(List<Card> hand, Vector3 origin)
{
for (int i = 0; i < hand.Count; i++)
{
hand[i].transform.position = origin + new Vector3(i * 160f, 0f, 0f);
}
}
// ... and when you only need each card, not its place:
int total = 0;
foreach (Card c in hand)
{
total += c.data.cost;
}- 1A separate method, so that laying the hand out is one named thing that can be called again whenever the hand changes. Same shape as
Draw: take the list as a parameter, do not reach for a field. - 3The index loop — the
FOR i FROM 1 TO LENGTHrow of the Lists & Arrays reference, written the C# way. It runs forward and includes the last card, which is the opposite of the shuffle's loop, and it is safe here because nothing is being removed.Why this one cannot be aforeach. The loop counteriis the only thing that knows which position this card occupies, and the next line needs exactly that. Aforeach (Card c in hand)visits the same cards in the same order and never tells you where any of them is — the loop you met in 8.3, and the right tool when the body only cares about the element. - 5Position comes straight out of the counter: card 0 sits at
origin, card 1 one step to the right, card 2 two steps. The 160 is a number somebody chose — the card's width plus a gap — which is why it is a literal here and a real knob in a polished game.This is the line that makes the hand spread, and it is the same "position is the index" idea the exam's index-loop questions are really testing. Change 160 and the fan tightens; make it negative and the hand runs off the other way. Nothing about the list changed. - 9The other traversal, for contrast. This is the shape the AP calls
FOR EACH item IN aList. - 10An accumulator, seeded at zero — pattern 1 in the Lists & Arrays reference, traverse to total.
- 11A
foreachover the hand. No index anywhere, because this loop has nothing to say about positions. - 13Notice the
.data:cis aCardcomponent, and the cost lives on theCard_dataasset it points at.Card.csalso copies the value into its owncostfield inStart(), soc.costwould work too — but the asset is the source of truth, and reading it there cannot go stale.This is the traverse-with-a-filter question from the exam, in your own game. "Sum the cost of every card in hand" is this loop with a+in it; "sum the cost of the cards you can afford" is the same loop with anifinside — pattern 2 on that reference page. Write both in AP pseudocode with these names before you write them in C#.
Step 6Rewire Deal() and prove the shuffle is real
The starter's Deal() did its own instantiating and never touched the hands. Now that Draw does that work, Deal() gets shorter — and the game gets a real hand for the first time.
void Deal()
{
Shuffle(player_deck);
Shuffle(ai_deck);
Draw(player_deck, player_hand, player_hand_location);
Draw(ai_deck, ai_hand, ai_hand_location);
player_hand[0].GetComponent<DraggableUI>().enabled = false;
}
// Prove it in the Console, then delete the proof:
// Prove the shuffle is not rigged:- 3Unchanged from the starter — and it matters that this runs before the draws.
Drawtakesdeck[0], so whatever card the shuffle left at the top is the card that gets dealt. - 4Unchanged. Both decks are shuffled before either is drawn from, and neither shuffle can reach the other deck, because each call passes one list.
- 6One card from the player's deck onto the player's hand, at the player's position. The two
Instantiateblocks the starter had here are gone —Drawdoes that work now, and it does it into the hand instead of into thin air. - 7The same line for the other side.
ai_hand_locationis computed inStart()as the player's position plus 350 up, and that line is untouched. - 9The starter's own line, and it still works:
player_hand[0]is now aCardcomponent, soGetComponent<DraggableUI>()finds the drag behaviour on the same GameObject. The player's drawn card is not draggable — for now.What actually changed here, and it is the biggest thing on this page. The starter'sDeal()instantiated a visual, assigned its data, and then dropped the reference — soplayer_handandai_handstayed empty no matter how many cards were on screen. Every loop over a hand therefore did nothing, silently.Drawputs the card into the list as well as onto the canvas, so from today the hand is a real list you can traverse, count and lay out. That is what makes any of the turn logic possible at all. - 12One run with a
Debug.Log(player_hand.Count + " in hand, " + player_deck.Count + " left in deck");at the end ofDeal()should print1 in hand, 29 left in deckif your deck is thirty cards. Then delete the log — a print that survives into the next lesson is noise you will read past. - 13Play the scene ten times and watch which card is dealt first. Ten identical first cards is not luck — it is the off-by-one in
Range(0, i)(see the note oni + 1in step 2). Ten different ones is a shuffle you can trust — though ten glances is not a count, and a real count is worth writing.
Check your work: the finished filetry every gap first — then open this to compare
Compare against your own file rather than copying it. Three things have to match and nothing else does: both Shuffle overloads are filled in, the random pick is UnityEngine.Random.Range(0, i + 1) with the max-exclusive + 1 and the qualified class name, and Draw reads deck[0] before it removes it, behind the empty-deck guard. Your parameter names, your spacing constant, your deck size and everything Deal() does after the two draws are yours. The gap line is the one that carries the whole lesson: it is the only place the AP pseudocode and C# genuinely disagree, in three ways at once.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/GameManager.cs
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager gm;
// The decks are Card_data (the 10.2b assets); the hands and the piles are
// Card (the components on screen). Two element types, so two Shuffle overloads.
public List<Card_data> player_deck = new List<Card_data>();
public List<Card_data> ai_deck = new List<Card_data>();
public List<Card> player_hand = new List<Card>();
public List<Card> ai_hand = new List<Card>();
public List<Card> player_discard_pile = new List<Card>();
public List<Card> ai_discard_pile = new List<Card>();
public Canvas canvas;
public GameObject blank_card;
public Vector3 player_hand_location;
public Vector3 ai_hand_location;
private void Awake()
{
if (gm != null && gm != this)
{
Destroy(gameObject);
}
else
{
gm = this;
DontDestroyOnLoad(this.gameObject);
}
}
void Start()
{
canvas = FindAnyObjectByType<Canvas>();
ai_hand_location = player_hand_location + new Vector3(0, 350, 0);
Deal();
}
void Deal()
{
// Shuffle both decks BEFORE anything is drawn: Draw takes deck[0],
// so whoever the shuffle left on top is who gets dealt.
Shuffle(player_deck);
Shuffle(ai_deck);
Draw(player_deck, player_hand, player_hand_location);
Draw(ai_deck, ai_hand, ai_hand_location);
// The starter's own line, now reading the hand Draw just filled.
player_hand[0].GetComponent<DraggableUI>().enabled = false;
}
// Fisher-Yates, overload (a) — the decks. Walk from the end; at each slot
// pick a random earlier-or-equal slot; swap through a temp.
// AP: FOR i FROM LENGTH TO 2 / r <- RANDOM(1, i) / SWAP(list[i], list[r])
void Shuffle(List<Card_data> data_hand)
{
for (int i = data_hand.Count - 1; i > 0; i--)
{
// Range is MAX-EXCLUSIVE for ints, so i + 1 makes the range 0..i —
// and UnityEngine. is required: Unity.Mathematics.Random is also
// in scope in this file (see the Concept Lab callout on the ambiguity).
int r = UnityEngine.Random.Range(0, i + 1);
Card_data temp = data_hand[i];
data_hand[i] = data_hand[r];
data_hand[r] = temp;
}
}
// The same six lines, for the hands and piles. Copy, do not guess: an empty
// body here is a silent no-op, and nothing in C# will warn you about it.
void Shuffle(List<Card> card_hand)
{
for (int i = card_hand.Count - 1; i > 0; i--)
{
int r = UnityEngine.Random.Range(0, i + 1);
Card temp = card_hand[i];
card_hand[i] = card_hand[r];
card_hand[r] = temp;
}
}
// Move the top of a deck into a hand, and put it on screen while you do it.
void Draw(List<Card_data> deck, List<Card> hand, Vector3 where)
{
// The boundary: deck[0] on an empty list is a crash, not a null.
if (deck.Count == 0) return;
GameObject new_card = Instantiate(blank_card, where, Quaternion.identity, canvas.transform);
// Read the top FIRST — RemoveAt(0) below would shift a different card
// into index 0.
new_card.GetComponent<Card>().data = deck[0];
hand.Add(new_card.GetComponent<Card>());
deck.RemoveAt(0);
}
// Position IS the index: the hand spreads because of a loop counter.
// This one cannot be a foreach — a foreach has no i.
void LayoutHand(List<Card> hand, Vector3 origin)
{
for (int i = 0; i < hand.Count; i++)
{
hand[i].transform.position = origin + new Vector3(i * 160f, 0f, 0f);
}
}
void AI_Turn()
{
// The next lesson's problem.
}
}
// The two loops side by side, for reference:
//
// index loop — you NEED i for positions:
// for (int i = 0; i < hand.Count; i++) { Layout(hand[i], i); }
//
// foreach — you only need each card:
// int total = 0;
// foreach (Card c in hand) { total += c.data.cost; }Build it, then break it on purpose. The walkthrough above leaves you a working shuffle, a working draw and a hand that finally holds something. Break each of those three and watch what the Console says — every bug in this method is one somebody in this room will actually write.
Checkpoint — before moving on, you must be able to:
- Implement Fisher-Yates in C# and match it line by line to AP pseudocode
- Explain why the C# range is
(0, i + 1)and not(0, i) - Say why
UnityEngine.Randomhas to be spelled out in this file - Implement BOTH
Shuffleoverloads, and say what breaks if you skip one - Draw safely with the empty-deck guard, reading the top card before removing it
- Render a hand by index loop (position = index), and say why a foreach cannot
- Explain remove-while-traversing and its backward fix
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- Lists & Arrays — shuffles, traversals, the works
- Exam Reference Sheet — RANDOM, SWAP, traversals
- 10.2a — Card Jam Kickoff — the starter's six Lists, and the AP-to-C# table these methods use
- 10.2b — ScriptableObjects — the card assets the deck is made of
- How do I…? index — find this lesson again by the question you were asking
Saved on this device only — the Dashboard tracks your progress.