Devlog 1

See full code here.

Scope

This repo is designed to answer at least the following questions:

  1. Can a computer play black jack profitably WITHOUT adjusting bet size, if it keeps perfect track of cards?
  2. If so, is there a set of variables that can be easily tracked to play profitably?

I will also write this in Kotlin, because I'm trying to learn the language.

Because this may be a large project, I plan to have a few milestones: 1. A black jack playing program. 2. Add counters for each card remaining in the shoe (total distribution). 3. Add strategy card and autoplay. 4. Update strategy card and EV as we go. Allow autoplay until strategy card changes. 5. Build traditional high-low card count and display.

Then switch to a backend program. 5. Simulate perfect strategy to see if long-term EV is positive. 6. Build a training set with high-low card counts and associated distributions. Use decision tree algorithm to decide rule-based strategy cards. Compute long-term EV. 7. Repeat with othe variables (counts).

We'll establish some black jack rules: - Pays 3:2 on black jack - Can double any hand - Dealer hits on soft 17 - Can split any face cards - Dealer+player black jacks are considered a push - Black jacks on splits are considered regular 21s

One way I will deviate from reality in a non-material way is: If the dealer has a black jack, the player can still hit or stand, but they will lose regardless of their outcome. This is done for programmming ease.

Hello, world

It turns out that java applets haven't been used for about a decade, so I created a swing app, using ChatGPT for a Hello, World.

I created a Glyph pattern for drawing:

open class Glyph(val x: Int, val y: Int) {
    val children: MutableList<Glyph> = mutableListOf()

    fun addChild(child: Glyph) {
        children.add(child)
    }

    // Method to draw the glyph and its children
    fun draw(frame: JFrame, off_x: Int = 0, off_y: Int = 0) {
        draw_this(frame, off_x + x, off_y + y)
        for (child in children) {
            child.draw(frame, off_x + x, off_y + y)
        }
    }

    // Draw method for children with adjusted positions
    open fun draw_this(frame: JFrame, off_x: Int, off_y: Int) {}
}

This allows me to abstract components easily. For example, I have a Card class with 20x20 cards, and I can make a Hand class like this:

class Hand(x: Int, y: Int) : Glyph(x, y) {
    fun addCard(char: String) {
        children.add(Card(25*children.size, 0, char))
    }
}

After adding a few glyphs, this looks like this:

1-1

To organize the UI together with the backend components, I'm gonna try a Model-View-Controller pattern. I haven't used this pattern before, but I do know that it's usually a little messy to handle both UI and logically components.

This should look roughly like:

view = View()  // Contains glyphs
model = Model()  // Contains shoe
controller = Controller(view, model)
view.add_callbacks(["hit", "stand", ...], controller.dispatcher)

This shows an example of how a Hit would operate:

1-2

A few things to note about design principles here:

  1. At every point, Model gets to know all state. For example, end-of-hand may not be needed for model, but model still gets to know.
  2. State can only be set by controller. Model could have set end-of-hand after drawing a card, but controller communicates this to ensure alignment.
  3. Controller is dumb. For example, it had to ask Model if there was a bust. Model should handle anything logic related.
  4. Updates from Controller should be the same for Model and View. For example, end-of-hand does little for the Model, but the View needs to disable some buttons and write a status message. This is the responsibility of the View. If we find ourselves writing different updates for the Model and the View, it implies that the Controller is doing too much.

Notice that "Draw card" in the above diagram violates principle 2. We should think of "draw card" as a helper function that does, "get card" and "set card" in one step. This is allowable, but to keep things readable, we'll always put "Update" in such calls. So that snippet of code would look like:

Example 1

card = model.drawCardUpdateHand()
view.updateHand(card)

Later we'll want to include the distribution in the View. We'll do this like:

Example 2

card, distribution = model.drawCardUpdateDistribution()
view.updateDistribution(distribution)
model.updateHand(card)
view.updateHand(card)

A few more design principles introduced here:

  1. Updates are called explicitly.
  2. Internal state to Model (state that doesn't need to synchronize to View) doesn't need to be updated explicitly. In example 1, we didn't mention that we were updating the disribution, even though we were.
  3. Only one update per function call. Example 2 could have included drawCardUpdateHandUpdateDistribution, but we'll avoid this.
  4. State should never be read separately. Example 2 could be achieved with a drawCardUpdateHand, then later read distribution = model.getDistribution() followed by view.updateDistribution(distribution). If we ever find ourselves doing this, then we've violated the contract that Controller should be in charge of setting state. (Of course, the Controller can have a helper function to do a few of these steps together.)

However, rules were made to be broken. I found myself calling, view.displayStatus(model.dealerShowing(), model.humanShowing()).

Next steps

Adding a distribution of cards, a status box, and a profit-tracker were all pretty easy with the design patterns I decided.

1-3

I wanna get a fully functional black jack game before moving forward. Some steps I need to do next are:

As I proceed I find myself fudging more of the above principles: