mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 07:10:42 -08:00
Refactor to allow testing side effects
By externalizing the source of i/o and randomness for shuffling, we can inject non-interactive and deterministic behavior during unit tests.
This commit is contained in:
@@ -1,240 +1,34 @@
|
|||||||
import java.util.ArrayList;
|
import java.io.EOFException;
|
||||||
import java.util.Arrays;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.io.InputStreamReader;
|
||||||
import java.util.List;
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
public class Blackjack {
|
public class Blackjack {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.out.println("BLACK JACK");
|
// Intuitively it might seem like the main program logic should be right
|
||||||
System.out.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n");
|
// here in 'main' and that we should just use System.in and System.out
|
||||||
if(promptBoolean("DO YOU WANT INSTRUCTIONS? ")){
|
// directly whenever we need them. However, by externalizing the source
|
||||||
System.out.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE");
|
// of input/output data (and the ordering of the cards via a custom
|
||||||
System.out.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE");
|
// shuffle function), we can write non-interactive and deterministic
|
||||||
System.out.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE");
|
// tests of the code. See UserIoTest as an example.
|
||||||
System.out.println("DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE");
|
try (Reader in = new InputStreamReader(System.in)) {
|
||||||
System.out.println("FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE");
|
Writer out = new OutputStreamWriter(System.out);
|
||||||
System.out.println("PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS");
|
UserIo userIo = new UserIo(in, out);
|
||||||
System.out.println("STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/',");
|
Deck deck = new Deck(cards -> {
|
||||||
System.out.println("INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE");
|
userIo.println("RESHUFFLING");
|
||||||
System.out.println("INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR");
|
Collections.shuffle(cards);
|
||||||
System.out.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING");
|
return cards;
|
||||||
System.out.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR");
|
});
|
||||||
System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'.");
|
Game game = new Game(deck, userIo);
|
||||||
}
|
game.run();
|
||||||
|
} catch (Exception e) {
|
||||||
int nPlayers = 0;
|
// This allows us to elegantly handle CTRL+D / CTRL+Z by throwing an exception.
|
||||||
while(nPlayers < 1 || nPlayers > 7) {
|
System.out.println(e.getMessage());
|
||||||
nPlayers = promptInt("NUMBER OF PLAYERS");
|
System.exit(1);
|
||||||
}
|
|
||||||
|
|
||||||
Deck deck = new Deck();
|
|
||||||
System.out.println("RESHUFFLING");
|
|
||||||
deck.shuffle();
|
|
||||||
|
|
||||||
List<Player> players = new ArrayList<>();
|
|
||||||
for(int i = 0; i < nPlayers; i++) {
|
|
||||||
players.add(new Player(i + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
while(true) {
|
|
||||||
int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses.
|
|
||||||
while(!betsAreValid(bets)){
|
|
||||||
System.out.println("BETS:");
|
|
||||||
for(int i = 0; i < nPlayers; i++) {
|
|
||||||
// Note that the bet for player "1" is at index "0" in the bets
|
|
||||||
// array and take care to avoid off-by-one errors.
|
|
||||||
bets[i] = promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop
|
|
||||||
players.get(i).setCurrentBet(bets[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(Player player : players){
|
|
||||||
player.dealCard(deck.deal());
|
|
||||||
player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Consider adding a Dealer class to track the dealer's hand and running total.
|
|
||||||
// Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored.
|
|
||||||
LinkedList<Card> dealerHand = new LinkedList<>();
|
|
||||||
Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on
|
|
||||||
dealer.dealCard(deck.deal());
|
|
||||||
// TODO deal two cards to the dealer
|
|
||||||
|
|
||||||
// TODO handle 'insurance' if the dealer's card is an Ace.
|
|
||||||
|
|
||||||
printInitialDeal(players, dealer);
|
|
||||||
|
|
||||||
for(Player player : players){
|
|
||||||
play(player, deck);
|
|
||||||
}
|
|
||||||
|
|
||||||
// only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards)
|
|
||||||
// otherwise, just print the dealer's concealed card
|
|
||||||
dealerHand = playDealer(dealerHand, deck);
|
|
||||||
|
|
||||||
evaluateRound(players, dealerHand);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printInitialDeal(List<Player> players, Player dealer) {
|
|
||||||
// Prints the initial deal in the following format:
|
|
||||||
/*
|
|
||||||
PLAYER 1 2 DEALER
|
|
||||||
7 10 4
|
|
||||||
2 A
|
|
||||||
*/
|
|
||||||
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
output.append("PLAYERS ");
|
|
||||||
for (Player player : players) {
|
|
||||||
output.append(player.getPlayerNumber() + "\t");
|
|
||||||
}
|
|
||||||
output.append("DEALER\n");
|
|
||||||
//Loop through two rows of cards
|
|
||||||
for (int j = 0; j < 2; j++) {
|
|
||||||
output.append("\t");
|
|
||||||
for (Player player : players) {
|
|
||||||
output.append(player.getHand().get(j).toString()).append("\t");
|
|
||||||
}
|
|
||||||
if(j == 0 ){
|
|
||||||
output.append(dealer.getHand().get(j).toString());
|
|
||||||
}
|
|
||||||
output.append("\n");
|
|
||||||
}
|
|
||||||
System.out.print(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays the players turn. Prompts the user to hit (H), stay (S), or if
|
|
||||||
* appropriate, split (/) or double down (D), and then performs those
|
|
||||||
* actions. On a hit, prints "RECEIVED A [x] HIT? "
|
|
||||||
*
|
|
||||||
* @param player
|
|
||||||
* @param deck
|
|
||||||
*/
|
|
||||||
private static void play(Player player, Deck deck) {
|
|
||||||
// TODO implement play(player, deck)
|
|
||||||
// If the player hits, deal another card. If the player stays, return. If the player busts, return.
|
|
||||||
// delegate to evaluateHand(hand) to determine whether the player busted.
|
|
||||||
// Use promptBoolean and promptInt as examples to start with for prompting actions
|
|
||||||
// initially prompt with "PLAYER [x] ?" where x is the player number and accept H, S, D, or /
|
|
||||||
// after hitting, prompt "RECEIVED A [c] HIT? " where c is the card received and only accept H or S
|
|
||||||
// handle splitting and doubling down, or feel free to skip implementing
|
|
||||||
// split/double down for now, but leave a todo if that is unfinished
|
|
||||||
// after the first pass.
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int evaluateHand(LinkedList<Card> hand){
|
|
||||||
// TODO implement evaluateHand
|
|
||||||
// 'int' is maybe the wrong return type. We need to indicate a bust and somehow communicate the ambiguity of aces.
|
|
||||||
// OR maybe we stick with 'int' and use -1 for a bust and otherwise determine the value of aces that gives the highest non-bust score.
|
|
||||||
// but note that we also need a distinction between a natural Blackjack (21 in only 2 cards) and a 21 with more than 2 cards (the natural blackjack wins)
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example:
|
|
||||||
*
|
|
||||||
* DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11
|
|
||||||
* DRAWS 10 ---TOTAL IS 21
|
|
||||||
*
|
|
||||||
* TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code.
|
|
||||||
*
|
|
||||||
* @param dealerHand
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private static LinkedList<Card> playDealer(LinkedList<Card> dealerHand, Deck deck) {
|
|
||||||
// TODO implement playDealer
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates the result of the round, prints the results, and updates player/dealer totals.
|
|
||||||
* @param players
|
|
||||||
* @param dealerHand
|
|
||||||
*/
|
|
||||||
private static void evaluateRound(List<Player> players, LinkedList<Card> dealerHand) {
|
|
||||||
// TODO implement evaluateRound
|
|
||||||
// print something like:
|
|
||||||
/*
|
|
||||||
PLAYER 1 LOSES 100 TOTAL=-100
|
|
||||||
PLAYER 2 WINS 150 TOTAL= 150
|
|
||||||
DEALER'S TOTAL= 200
|
|
||||||
*/
|
|
||||||
// this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total.
|
|
||||||
// currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.)
|
|
||||||
// remember to handle a "PUSH" when the dealer ties and the bet is returned.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user for a "Yes" or "No" answer.
|
|
||||||
* @param prompt The prompt to display to the user on STDOUT.
|
|
||||||
* @return false if the user enters a value beginning with "N" or "n"; true otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean promptBoolean(String prompt) {
|
|
||||||
System.out.print(prompt);
|
|
||||||
|
|
||||||
// Other ways to read input are
|
|
||||||
// new BufferedReader(new InputStreamReader(System.in)).readLine();
|
|
||||||
// and new Scanner(System.in)
|
|
||||||
// But those are less expressive and care must be taken to close the
|
|
||||||
// Reader or Scanner resource.
|
|
||||||
String input = System.console().readLine();
|
|
||||||
if(input == null) {
|
|
||||||
// readLine returns null on CTRL-D or CTRL-Z
|
|
||||||
// this is how the original basic handled that.
|
|
||||||
System.out.println("!END OF INPUT");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(input.toLowerCase().startsWith("n")) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user for an integer. As in Vintage Basic, "the optional
|
|
||||||
* prompt string is followed by a question mark and a space." and if the
|
|
||||||
* input is non-numeric, "an error will be generated and the user will be
|
|
||||||
* re-prompted.""
|
|
||||||
*
|
|
||||||
* @param prompt The prompt to display to the user.
|
|
||||||
* @return the number given by the user.
|
|
||||||
*/
|
|
||||||
public static int promptInt(String prompt) {
|
|
||||||
System.out.print(prompt + "? ");
|
|
||||||
|
|
||||||
while(true) {
|
|
||||||
String input = System.console().readLine();
|
|
||||||
if(input == null) {
|
|
||||||
// readLine returns null on CTRL-D or CTRL-Z
|
|
||||||
// this is how the original basic handled that.
|
|
||||||
System.out.println("!END OF INPUT");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(input);
|
|
||||||
} catch(NumberFormatException e) {
|
|
||||||
// Input was not numeric.
|
|
||||||
System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE");
|
|
||||||
System.out.print("? ");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that all bets are between 1 and 500 (inclusive).
|
|
||||||
*
|
|
||||||
* @param bets The array of bets for each player.
|
|
||||||
* @return true if all bets are valid, false otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean betsAreValid(int[] bets) {
|
|
||||||
return Arrays.stream(bets)
|
|
||||||
.allMatch(bet -> bet >= 1 && bet <= 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,50 @@
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class Deck {
|
public class Deck {
|
||||||
|
|
||||||
private LinkedList<Card> cards;
|
private LinkedList<Card> cards;
|
||||||
|
private Function<LinkedList<Card>, LinkedList<Card>> shuffleAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the game deck with the given number of standard decks.
|
* Initialize the game deck with the given number of standard decks.
|
||||||
* e.g. if you want to play with 2 decks, then {@code new Decks(2)} will
|
* e.g. if you want to play with 2 decks, then {@code new Decks(2)} will
|
||||||
* initialize 'cards' with 2 copies of a standard 52 card deck.
|
* initialize 'cards' with 2 copies of a standard 52 card deck.
|
||||||
*
|
*
|
||||||
* @param nDecks
|
* @param shuffleAlgorithm A function that takes the initial sorted card
|
||||||
|
* list and returns a shuffled list ready to deal.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public Deck() {
|
public Deck(Function<LinkedList<Card>, LinkedList<Card>> shuffleAlgorithm) {
|
||||||
cards = new LinkedList<>();
|
this.shuffleAlgorithm = shuffleAlgorithm;
|
||||||
for(Card.Suit suit : Card.Suit.values()) {
|
|
||||||
for(int value = 1; value < 14; value++) {
|
|
||||||
cards.add(new Card(value, suit));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deals one card from the deck, removing it from this object's state.
|
* Deals one card from the deck, removing it from this object's state. If
|
||||||
|
* the deck is empty, it will be reshuffled before dealing a new card.
|
||||||
|
*
|
||||||
* @return The card that was dealt.
|
* @return The card that was dealt.
|
||||||
*/
|
*/
|
||||||
public Card deal() {
|
public Card deal() {
|
||||||
// TODO implement Deck.deal() - new Card(10, Card.Suit.CLUBS) added temporarily
|
if(cards == null || cards.isEmpty()) {
|
||||||
return new Card(10, Card.Suit.CLUBS);
|
reshuffle();
|
||||||
|
}
|
||||||
|
return cards.pollFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle the cards in this deck.
|
* Shuffle the cards in this deck using the shuffleAlgorithm.
|
||||||
*/
|
*/
|
||||||
public void shuffle() {
|
public void reshuffle() {
|
||||||
// TODO implement Deck.shuffle()
|
LinkedList<Card> newCards = new LinkedList<>();
|
||||||
// Probably just call Collections.shuffle(cards);
|
for(Card.Suit suit : Card.Suit.values()) {
|
||||||
|
for(int value = 1; value < 14; value++) {
|
||||||
|
newCards.add(new Card(value, suit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cards = this.shuffleAlgorithm.apply(newCards);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
190
10_Blackjack/java/src/Game.java
Normal file
190
10_Blackjack/java/src/Game.java
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
|
||||||
|
public class Game {
|
||||||
|
|
||||||
|
private Deck deck;
|
||||||
|
private UserIo userIo;
|
||||||
|
|
||||||
|
public Game(Deck deck, UserIo userIo) {
|
||||||
|
this.deck = deck;
|
||||||
|
this.userIo = userIo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
userIo.println("BLACK JACK", 31);
|
||||||
|
userIo.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n", 15);
|
||||||
|
if(userIo.promptBoolean("DO YOU WANT INSTRUCTIONS")){
|
||||||
|
userIo.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE");
|
||||||
|
userIo.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE");
|
||||||
|
userIo.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE");
|
||||||
|
userIo.println("DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE");
|
||||||
|
userIo.println("FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE");
|
||||||
|
userIo.println("PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS");
|
||||||
|
userIo.println("STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/',");
|
||||||
|
userIo.println("INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE");
|
||||||
|
userIo.println("INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR");
|
||||||
|
userIo.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING");
|
||||||
|
userIo.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR");
|
||||||
|
userIo.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int nPlayers = 0;
|
||||||
|
while(nPlayers < 1 || nPlayers > 7) {
|
||||||
|
nPlayers = userIo.promptInt("NUMBER OF PLAYERS");
|
||||||
|
}
|
||||||
|
|
||||||
|
deck.reshuffle();
|
||||||
|
|
||||||
|
List<Player> players = new ArrayList<>();
|
||||||
|
for(int i = 0; i < nPlayers; i++) {
|
||||||
|
players.add(new Player(i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses.
|
||||||
|
while(!betsAreValid(bets)){
|
||||||
|
userIo.println("BETS:");
|
||||||
|
for(int i = 0; i < nPlayers; i++) {
|
||||||
|
// Note that the bet for player "1" is at index "0" in the bets
|
||||||
|
// array and take care to avoid off-by-one errors.
|
||||||
|
bets[i] = userIo.promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop
|
||||||
|
players.get(i).setCurrentBet(bets[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Player player : players){
|
||||||
|
player.dealCard(deck.deal());
|
||||||
|
player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Consider adding a Dealer class to track the dealer's hand and running total.
|
||||||
|
// Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored.
|
||||||
|
LinkedList<Card> dealerHand = new LinkedList<>();
|
||||||
|
Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on
|
||||||
|
dealer.dealCard(deck.deal());
|
||||||
|
// TODO deal two cards to the dealer
|
||||||
|
|
||||||
|
// TODO handle 'insurance' if the dealer's card is an Ace.
|
||||||
|
|
||||||
|
printInitialDeal(players, dealer);
|
||||||
|
|
||||||
|
for(Player player : players){
|
||||||
|
play(player, deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards)
|
||||||
|
// otherwise, just print the dealer's concealed card
|
||||||
|
dealerHand = playDealer(dealerHand, deck);
|
||||||
|
|
||||||
|
evaluateRound(players, dealerHand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printInitialDeal(List<Player> players, Player dealer) {
|
||||||
|
// Prints the initial deal in the following format:
|
||||||
|
/*
|
||||||
|
PLAYER 1 2 DEALER
|
||||||
|
7 10 4
|
||||||
|
2 A
|
||||||
|
*/
|
||||||
|
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
output.append("PLAYERS ");
|
||||||
|
for (Player player : players) {
|
||||||
|
output.append(player.getPlayerNumber() + "\t");
|
||||||
|
}
|
||||||
|
output.append("DEALER\n");
|
||||||
|
//Loop through two rows of cards
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
output.append("\t");
|
||||||
|
for (Player player : players) {
|
||||||
|
output.append(player.getHand().get(j).toString()).append("\t");
|
||||||
|
}
|
||||||
|
if(j == 0 ){
|
||||||
|
output.append(dealer.getHand().get(j).toString());
|
||||||
|
}
|
||||||
|
output.append("\n");
|
||||||
|
}
|
||||||
|
System.out.print(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the players turn. Prompts the user to hit (H), stay (S), or if
|
||||||
|
* appropriate, split (/) or double down (D), and then performs those
|
||||||
|
* actions. On a hit, prints "RECEIVED A [x] HIT? "
|
||||||
|
*
|
||||||
|
* @param player
|
||||||
|
* @param deck
|
||||||
|
*/
|
||||||
|
private void play(Player player, Deck deck) {
|
||||||
|
// TODO implement play(player, deck)
|
||||||
|
// If the player hits, deal another card. If the player stays, return. If the player busts, return.
|
||||||
|
// delegate to evaluateHand(hand) to determine whether the player busted.
|
||||||
|
// Use promptBoolean and promptInt as examples to start with for prompting actions
|
||||||
|
// initially prompt with "PLAYER [x] ?" where x is the player number and accept H, S, D, or /
|
||||||
|
// after hitting, prompt "RECEIVED A [c] HIT? " where c is the card received and only accept H or S
|
||||||
|
// handle splitting and doubling down, or feel free to skip implementing
|
||||||
|
// split/double down for now, but leave a todo if that is unfinished
|
||||||
|
// after the first pass.
|
||||||
|
}
|
||||||
|
|
||||||
|
private int evaluateHand(LinkedList<Card> hand){
|
||||||
|
// TODO implement evaluateHand
|
||||||
|
// 'int' is maybe the wrong return type. We need to indicate a bust and somehow communicate the ambiguity of aces.
|
||||||
|
// OR maybe we stick with 'int' and use -1 for a bust and otherwise determine the value of aces that gives the highest non-bust score.
|
||||||
|
// but note that we also need a distinction between a natural Blackjack (21 in only 2 cards) and a 21 with more than 2 cards (the natural blackjack wins)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example:
|
||||||
|
*
|
||||||
|
* DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11
|
||||||
|
* DRAWS 10 ---TOTAL IS 21
|
||||||
|
*
|
||||||
|
* TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code.
|
||||||
|
*
|
||||||
|
* @param dealerHand
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private LinkedList<Card> playDealer(LinkedList<Card> dealerHand, Deck deck) {
|
||||||
|
// TODO implement playDealer
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the result of the round, prints the results, and updates player/dealer totals.
|
||||||
|
* @param players
|
||||||
|
* @param dealerHand
|
||||||
|
*/
|
||||||
|
private void evaluateRound(List<Player> players, LinkedList<Card> dealerHand) {
|
||||||
|
// TODO implement evaluateRound
|
||||||
|
// print something like:
|
||||||
|
/*
|
||||||
|
PLAYER 1 LOSES 100 TOTAL=-100
|
||||||
|
PLAYER 2 WINS 150 TOTAL= 150
|
||||||
|
DEALER'S TOTAL= 200
|
||||||
|
*/
|
||||||
|
// this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total.
|
||||||
|
// currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.)
|
||||||
|
// remember to handle a "PUSH" when the dealer ties and the bet is returned.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that all bets are between 1 and 500 (inclusive).
|
||||||
|
*
|
||||||
|
* @param bets The array of bets for each player.
|
||||||
|
* @return true if all bets are valid, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean betsAreValid(int[] bets) {
|
||||||
|
return Arrays.stream(bets)
|
||||||
|
.allMatch(bet -> bet >= 1 && bet <= 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
102
10_Blackjack/java/src/UserIo.java
Normal file
102
10_Blackjack/java/src/UserIo.java
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for printing output to the screen and reading input
|
||||||
|
* from the user. It must be initialized with a reader to get input data from
|
||||||
|
* and a writer to send output to. Typically these will wrap System.in and
|
||||||
|
* System.out respectively, but can be a StringReader and StringWriter when
|
||||||
|
* running in test code.
|
||||||
|
*/
|
||||||
|
public class UserIo {
|
||||||
|
|
||||||
|
private BufferedReader in;
|
||||||
|
private PrintWriter out;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the UserIo with the given reader/writer. The reader will be
|
||||||
|
* wrapped in a BufferedReader and so should <i>not</i> be a BufferedReader
|
||||||
|
* already (to avoid double buffering).
|
||||||
|
*
|
||||||
|
* @param in Typically an InputStreamReader wrapping System.in or a StringReader
|
||||||
|
* @param out Typically an OuputStreamWriter wrapping System.out or a StringWriter
|
||||||
|
*/
|
||||||
|
public UserIo(Reader in, Writer out) {
|
||||||
|
this.in = new BufferedReader(in);
|
||||||
|
this.out = new PrintWriter(out, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void println(String text) {
|
||||||
|
out.println(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void println(String text, int leftPad) {
|
||||||
|
IntStream.range(0, leftPad).forEach((i) -> out.print(' '));
|
||||||
|
out.println(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print(String text) {
|
||||||
|
out.print(text);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readLine() {
|
||||||
|
try {
|
||||||
|
String line = in.readLine();
|
||||||
|
if(line == null) {
|
||||||
|
throw new UncheckedIOException("!END OF INPUT", new EOFException());
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user for a "Yes" or "No" answer.
|
||||||
|
* @param prompt The prompt to display to the user on STDOUT.
|
||||||
|
* @return false if the user enters a value beginning with "N" or "n"; true otherwise.
|
||||||
|
*/
|
||||||
|
public boolean promptBoolean(String prompt) {
|
||||||
|
print(prompt + "? ");
|
||||||
|
|
||||||
|
String input = readLine();
|
||||||
|
|
||||||
|
if(input.toLowerCase().startsWith("n")) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user for an integer. As in Vintage Basic, "the optional
|
||||||
|
* prompt string is followed by a question mark and a space." and if the
|
||||||
|
* input is non-numeric, "an error will be generated and the user will be
|
||||||
|
* re-prompted.""
|
||||||
|
*
|
||||||
|
* @param prompt The prompt to display to the user.
|
||||||
|
* @return the number given by the user.
|
||||||
|
*/
|
||||||
|
public int promptInt(String prompt) {
|
||||||
|
print(prompt + "? ");
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
String input = readLine();
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(input);
|
||||||
|
} catch(NumberFormatException e) {
|
||||||
|
// Input was not numeric.
|
||||||
|
println("!NUMBER EXPECTED - RETRY INPUT LINE");
|
||||||
|
print("? ");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ public class DeckTest {
|
|||||||
@Test
|
@Test
|
||||||
void testInit() {
|
void testInit() {
|
||||||
// When
|
// When
|
||||||
Deck deck = new Deck();
|
Deck deck = new Deck((cards) -> cards);
|
||||||
|
deck.reshuffle();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
long nCards = deck.size();
|
long nCards = deck.size();
|
||||||
|
|||||||
Reference in New Issue
Block a user