Welcome to this lab where you will explore how computers represent numbers using a fun card-guessing game! Through this lab, you will gain an understanding of how binary numbers are used to represent decimal numbers in computers.
Card Guessing Game¶
Rules¶
Table 1:Deck of cards
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | J | Q | K | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Diamond | |||||||||||||
Club | |||||||||||||
Heart | |||||||||||||
Spade |
Table 1 shows a deck of 52 cards:
- Each card is in one of the four suits: Diamond ♦, Club ♣, Heart ♥, and Spade ♠.
- Each card has a value ranging from 1 to 13. For simplicity, the original card values A, J, Q, and K are converted to the numerical values 1, 11, 12, and 13 respectively.
Figure 1:Card guessing game
Rules
As depicted in Figure 1, the card-guessing game involves the following steps:
- The dealer selects a card without revealing it to the player.
- The player’s goal is to correctly guess the chosen card.
- To make an informed guess, the player can ask up to six yes/no questions.
- The dealer must answer each question truthfully and immediately.
Example 1 (Valid questions)
For instance, the player may ask:
- Is the suit club?
- Is the card diamond 1?
- Is the value at least 10?
Example 2 (Invalid questions)
However, the player cannot ask:
- What is the value?
- What is the suit?
because the answers are not yes/no.
One strategy is to ask whether the randomly drawn card is a specific card, e.g.,
Is it the Diamond 1?
If the answer is yes, the player can confidently guess that the card is Diamond 1 and win the game. However, if the answer is no, the player can continue to check another card, e.g.,
Is it the Diamond 2?
and so on...
A pseudocode that summarizes the above strategy (or algorithm) is shown below.
# YOUR CODE HERE
raise NotImplementedError
Run the following visible tests to check whether the chance
variable meets the following criteria:
- It is greater than or equal to 0.
- It is less than or equal to 1.
# tests
assert chance >= 0
assert chance <= 1
Passing the visible tests does not guarantee that the answer is correct. The actual correctness will be evaluated using a hidden test to be injected into the following cell.
# hidden tests
Virtual Cards¶
Instead of drawing a physical card, the dealer can use programming to generate a virtual deck and draw a random[1] virtual card from it. Run the code below to get the required tools.
from collections import namedtuple # for naming the cards
from random import choice # for randomly drawing cards
You’ll learn the syntax in a future lecture, but even now, it is clear that the code imports libraries for creating named tuples and making random choices, thanks to the design of Python being a high-level programming language.
To create a deck
of cards, run the following cell.
suits = ("Diamond", "Club", "Heart", "Spade")
values = range(1, 14)
Card = namedtuple("Card", ["value", "suit"])
deck = [Card(value, suit) for value in values for suit in suits]
deck
The code above uses composite data types like tuples and lists, which will be formally introduced later in the course. They are handy for storing and manipulating collections of values.
Now, you can use the following program to play the game with your friends:
choice(deck)
To draw cards repeatedly, use Ctrl-Enter instead of Shift-Enter to run the code, so the same code cell stay selected after the execution. Note that function choice
is said to be an impure function as it can return different values for repeated runs with the same input argument, namely deck
.
Can you fix the code below so that it can return 5 cards randomly drawn with replacement from the deck of cards?
1 2 3
def draw_five_cards_randomly(): card1 = card2 = card3 = card4 = card5 = choice(deck) return card1, card2, card3, card4, card5
def draw_five_cards_randomly():
# YOUR CODE HERE
raise NotImplementedError
return card1, card2, card3, card4, card5
# tests
import random
random.seed(1302) # to make pseudo-random sequences reproducible
assert (lambda cards: any(cards[i] != cards[4] for i in range(4)))(
draw_five_cards_randomly()
)
# hidden tests
Caution
While one may pass the visible test with an answer like
def draw_five_cards_randomly():
card1 = card2 = card3 = card4 = card5 = choice(deck)
card1 = 0
return card1, card2, card3, card4, card5
Who knows whether the hidden test would catch that card1
is not even a card in the deck
? The hidden test can also be modified since it is not hard coded in the notebook.
Virtual Player¶
Given you have randomly drawn a card,
- run the following code cell, and
- answer the 6 questions raised by the player honestly by entering
1
for yes and0
for no.
def is_yes(question):
"""Get an answer to a yes/no question."""
return "1" == input(question + " 1:yes [0:no] ")
suit_idx = number = 0
if is_yes("Is the suite either heart or spade?"):
suit_idx += 2
if is_yes("Is the suite either club or spade?"):
suit_idx += 1
for i in range(3, -1, -1):
number += 2**i * is_yes(f"Is the number {number+2**i} or above?")
print(f"The card is {suits[suit_idx]} {number}")
The code above provides an overview of various programming syntaxes to be covered in the course. Although you don’t need to be familiar with them now, it’s good to have a basic understanding:
- The program first defines a function called
is_yes
that checks whether the answer to a given question is yes. - To compute the guess, the program modifies the values of the variables
suit_idx
andnumber
using- conditional
if
statements and - a
for
loop, along with - different operations such as
- chained assignment
=
, - augmented assignment
+=
, - equality comparison
==
, and - exponentiation
**
.
- chained assignment
- conditional
Hope you are convinced that the virtual player has a winning strategy. Indeed, the answers to 6 questions not only resolve the uncertainty of a random draw from 6 cards, but they can also resolve a random draw from over 52 cards. Fix the following function number_of_cards
so that it returns the maximum number of cards from which a random draw can be resolved by the answers to a number of questions specified by an integer assigned to the variable number_of_questions
.
1 2 3
def number_of_cards(number_of_questions): ans = number_of_questions return ans
You may assume one can add new values/suits so that a deck may have an arbitrarily number of distinct cards.
def number_of_cards(number_of_questions):
# YOUR CODE HERE
raise NotImplementedError
return ans
# tests
assert number_of_cards(number_of_questions=6) > len(deck)
# hidden tests
The value of number_of_questions
can be very large in the hidden test.
Binary Number System¶
Representing non-negative integers¶
The following table gives the binary representions of unsigned decimal integers from 0 to 7.
Table 2:Encoding positive integers
Binary | Decimal |
---|---|
000 | 0 |
001 | 1 |
010 | 2 |
011 | 3 |
100 | 4 |
101 | 5 |
110 | 6 |
111 | 7 |
Observe that the binary representations of 4, 5, 6, and 7 all have 1 in the leftmost (most significant) bit. Consider that bit as the answer to the following yes/no question when converting an integer n
to binary:
Is the integer 4 or above, i.e., ?
Now, to convert the entire binary sequence to decimal, we can think of the conversion as a guessing game where
- the binary sequence is a sequence of yes (1) or no (0) answers to certain yes/no questions, and
- the informed guess is the integer represented by the binary sequence.
The following code attempts to convert integers between 0 and 7 to their binary strings.
def decimal_to_binary(n):
def get_bit(cond):
return "1" if cond else "0"
return get_bit(eval(Q1)) + get_bit(eval(Q2)) + get_bit(eval(Q3))
# Define the questions
Q1, Q2, Q3 = "n >= 4", "n >= 2", "n >= 1"
# Print the conversions decimal (binary)
print(*[f"{i} ({decimal_to_binary(i)})" for i in range(8)], sep="\n")
Although the conversions for 0, 1, 3, and 7 are correct, the conversions for 2, 4, 5, and 6 are incorrect. Fix the issue by assigning the correct strings to Q2 and Q3. Similar to Q1, each of the strings should be a valid Python expression that can be evaluated with n
being the integer to convert to a binary string.
Hint
Consider the modulo operator %
. See also:
# Assign strings to Q2 and Q3
# YOUR CODE HERE
raise NotImplementedError
Q2, Q3
# test
assert decimal_to_binary(2) == '010'
assert decimal_to_binary(4) == '100'
assert decimal_to_binary(5) == '101'
assert decimal_to_binary(6) == '110'
# hidden tests
Representing negative numbers¶
The following also uses 3 bits to represent 8 integers but half of them are negative.
Table 3:Encoding positive integers
Binary | Decimal |
---|---|
000 | 0 |
001 | 1 |
010 | 2 |
011 | 3 |
100 | -4 |
101 | -3 |
110 | -2 |
111 | -1 |
- The numbers 0, 1, 2, and 3 have the same binary representations as before, but
- the binary representations for 4, 5, 6, and 7 are now used for -4, -3, -2, and -1.
What is the benefit of the above representation?
To subtract a positive binary number from another positive binary number , i.e.,
it seems we can instead perform the binary addition
of a positive binary number and a negative binary number .
For example,
It seem to work as well if there are bits carried forward, e.g., in binary is
which translate to -2 in decimal as desired.
There is a subtlety when computing using binary addition because
which overflows to 4 bits, a seemingly invalid binary representation. Fortunately, there is a simple fix so binary addition can still apply to the above case. Come up with such a fix that also works for other cases such as , , , etc.
Hint
See two’s complement represenation. Note that some overflow is invalid such as and . Why? It is important for the computer to detect whether an overflow is valid or not. How?
YOUR ANSWER HERE
Logic Gates¶
A computer is built from transistors that operates on binary states on/off, which is also represented as bits 1/0, or the boolean value true/false. Play with the following simulator to see some examples.
- Click the logic simulator header above.
- Select 1-Bit Full Adder.
- Change the input and observe the output.
Table 4 gives the input and output relationship of a simpler adder, called the half adder, which computes the addition of two input bits and as
where and are the output bits, and is the concatenation of two bits and .
Table 4:Adder.
A | B | C∘S |
---|---|---|
0 | 0 | 00 |
0 | 1 | 01 |
1 | 0 | 01 |
1 | 1 | 10 |
The operation can be built using transistors, called logic gates, that can carry out basic logic operations such as
- : which returns 1 if and only if both and are 1, and 0 otherwise.
- : which returns 1 if and only if either and are 1 but not both.
The input and output relationships are listed below:
from pandas import DataFrame
DataFrame(
[[A, B, A & B, A ^ B] for A in (0, 1) for B in (0, 1)],
columns=["A", "B", "A AND B", "A XOR B"],
)
The logical functions are computed using bitwise operators &
and ^
, and the result is stored as a DataFrame
object, which is a useful tool for data analysis to be explained later in the course.
What is the logic in computing and from and ? Use the logic gates AND and XOR only.
YOUR ANSWER HERE
If you play minecraft, you can build the following devices based on how computers are built from logic gates:
An adder for binary addition
A calculator for calculus
Glossary¶
- algorithm
- An algorithm is a set of instructions that a computer program follows to solve a problem or complete a task. It is a step-by-step approach designed to be efficient and accurate.
- bitwise operator
- Bitwise operators are used to manipulate individual bits of binary numbers. They allow for the manipulation of data at a low level, such as setting or clearing specific bits, shifting bits left or right, or performing logical operations on bits.
- composite data type
- A composite data type groups related data into a single unit so that they can be easily stored and manipulated together.
- conditional
- A conditional statement is used to perform different actions based on different conditions. It allows the program to make decisions and execute specific code blocks based on the evaluation of a given condition.
- iteration
- An iteration is a repeated execution until a condition is met. It allows for the efficient repetition of tasks without the need for writing the same code multiple times.
- library
- A library is a collection of pre-written code that developers can use to perform common tasks. It contains functions, classes, and other reusable code that can be integrated into a larger software application, saving time and effort.
- pseudocode
- Pseudocode is a high-level description of a program or algorithm, using natural language and code-like syntax to express logic without being bound to a specific programming language. It is used during planning and design phases of software development.
The truth is, the random generation process will look random but is not truly random. You will learn about this pseudorandomness later in the course.