Polymaker Sponsors Workshop 88 Makerspace

We’d like to welcome Workshop 88‘s newest sponsor: Polymaker!


We’d like to thank Polymaker for helping Workshop 88 makerspace achieve our mission of providing tools and technology access to makers. Watch for ingenious new Workshop 88 projects using Polymaker 3D printing filaments.

https://polymaker.com/
Polymaker is a developer and manufacturer of 3D printing materials committed to innovation, quality and sustainability. Its award-winning product portfolio has enabled numerous individuals and companies to better create and make.
US Store: https://us.polymaker.com/
Canada Store: https://ca.polymaker.com/

Why isn’t there a Thingiverse for CNC? Slice Everything!

Contents:

“Why isn’t there a Thingiverse for CNC?” is a question that has been asked for almost as long as Thingiverse has existed, and the answer may surprise you.  

I’m D. Scott Williamson, a software architect and developer as well as a CNC and 3D printing hobbyist. I’ve wondered about this many times myself, wishing I could share and find CNC projects with the same ease I can share and find 3D printing projects.

Some of my CNC projects I would have liked to have shared over the years: Furniture, standup, cocktail, and miniature coin-op cabinets, engraved signs, coasters, vacuum form molds, control panels and much more.

Here’s why, how we got here, and how we can fix it.

What is Thingiverse?

Thingiverse.com is a free global file sharing website for 3D printing where people can upload 3D printable files with descriptions, instructions, pictures and videos of their creations.  You can search for 3D printable files, preview them, and download them.  The 3D printable files contain 3D geometry, most often in the .stl file format. 

Search results for “Benchy” on Thingiverse

How to 3D print a file from Thingiverse

Go to Thingiverse.com, download a file, load the .stl into your slicer, click slice, preview, and send the gcode to your printer.  It takes a minimum of one click to convert a 3D model into gcode.

Benchy on Thingiverse

Benchy loaded and sliced in Prusa Slicer

Benchy loaded and sliced in SuperSlicer

Benchy loaded and sliced in Cura

Benchy loaded and sliced in IdeaMaker

How to CNC mill a downloaded file

Let’s start with some definitions.

  • CNC stands for “Computer Numerical Control” which refers to any machine controlled by computer, typically through a gcode program file. Automated milling machines are typically referred to as “CNC machines” but technically any computer controlled machine is a CNC machine, including 3D printers.
  • CAD stands for “Computer Aided Design” which refers to software used to design a part.
  • CAM stands for “Computer Aided Manufacturing” which refers to software used to translate a design into gcode. Slicer software is automated CAM for 3D printing.

There are many CNC file sharing websites (example lists here, and here), they typically share layered 2D dxf drawings and source files for dozens of CAD packages. There is no automatic CAM process to create gcode from these files, and there are few open source software options.

The CNC process varies by the CAM tool but always resembles the following: Download a file, load the 2D .dxf(s) into your CAM package, manually select shapes or faces, manually create a machine operation (e.g. drill, pocket, profile, engrave, 3D contour), and manually configure the machine operation (bit selection, depth of cut, cut speed, plunge speed, etc.) based on the machine capabilities and material properties. Repeat this process for every machine operation, create gcode, preview the result in a cut viewer (often third party software like CAMotics or Cutviewer Mill & Turn) and iterate as needed.  Load the gcode in your CNC controller software and machine the part.

If you’d like to change settings or cut a different material you need to revisit the settings in all impacted machine operations individually and manually re-configure the CAM.

CAM in CamBam and gcode visualization in CAMotics

There’s no “slice” button in CAM for CNC

There’s no Thingiverse for CNC because CAM is HARD! Shared .dxf files resemble drawings, not finished parts, and they require a lot of error prone manual work coupled with significant domain knowledge to generate the machine operations needed to create gcode.  Slicers are automated CAM tools for 3D printers which are technically CNC machines, that automatically convert 3D models into gcode using reusable parameters organized into machine, material, and job configurations.  The 3D models fully represent designer intent in their geometry.

It’s easier to 3D print the parts for an MPCNC than to use it.

How did we get here?

People have been shaping wood and stone for all of recorded history and have been using machines to shape metal at least since clock and watch makers used machines to cut metal gears and shape metal since the 1700’s. Numerical control of machines began in the 1940’s, gcode was invented at MIT in 1958 and standardized in 1963. CNC and CADCAM took off in the 80’s and fused filament deposition 3D printing was invented in 1988.  The consumer 3D printing market is valued at $3262 million USD (2022) compared to the desktop CNC Machines marked at $369.7 million USD(2023).  How did the consumer 3D printing market grow to nearly 9 times the size of the consumer CNC market when CNC had a 40 year head start?

I see two primary factors.

RepRap inspired, organized, and mobilized a global development community

RepRap has been an amazing force in the advancement of 3D printing technologies.  Founded in February 2004 by Adrian Bowyer at the University of Bath with the goal of making a self replicating machine to democratize manufacturing. RepRap inspired and organized a global community to iterate on all aspects of consumer 3D printing:

  • 3D printer design – Many 3D printer designs were and are explored including bed lifters, bed slingers, delta, scara, belt printers, and more experimental designs leading to many modern 3D printer companies today, not the least of which Prusa Printers founded by Joseph Prusa a long time active member of RepRap.
  • Electronics – RAMPS Arduino based controller
  • Firmware – Marlin, Sailfish, Repetier and more
  • Gcode senders – Pronterface etc. (later incorporated into slicers)
  • Software slicers – Skeinforge, ReplicatorG, Slic3r (and derivatives Prusa Slicer, Super Slicer, OrcaSlicer), Cura, and others.

This multidisciplinary community was able to rapidly experiment and advance the reliability and usability of 3D printers through iterative feedback and open source knowledge sharing, ultimately resulting in the ubiquity and ease of use modern 3D printers enjoy today.

The CADCAM software industry has neglected the consumer/hobbyist market

For literally hundreds of years, engineers have drafted detailed part designs on paper for skilled tradespeople to craft through a sequence of manual operations.  Standard practices and notations are used to ensure the communication of required dimensions and parameters for the efficient commercial production of parts relying on both the engineer and trades-person’s knowledge of engineering, material, and machining principles.  These conventions persist in the digital CADCAM systems of today.

There is little need for GD&T details for 3D printers or desktop CNC machines doing woodworking, PCB engraving, or just able to cut aluminum. What is the surface finish of a 3D printed part? What is the tolerance of a hole cut by a laser cutter?

Since industry is where the vast majority of commercial CADCAM software value is, there is little motivation to make workflows for hobbyists that are automated and focused on the capabilities of modest hobby CNC machines even though such tools have the potential to disruptively grow the consumer hobbyist CNC market.

Despite the proliferation of affordable consumer, desktop, hobbyist, and DIY CNC machines, there exists a significant knowledge and skill barrier to entry for CNC hobbyists; a barrier that has been overcome in free open source 3D printing slicer software.

An easy fix

Slicer software is already very capable CAM software. It loads, visualizes, and manipulates 3D models representing designer intent in the context of the work area of a machine. It uses preset parameters for machine, material, and job settings. It generates a sequence of machine operations, converts the machine operations to tool paths, and converts tool paths to gcode with post-processor specializations for 3D printer firmware flavors (SuperSlicer already supports post processing milling operations).

The opportunity is to leverage the existing capabilities and extend a slicer from purpose built 3D printing automated CAM to a general purpose automated CAM package capable of “slicing” a model to create gcode for any machine including 3D printers.

  1. Generalize existing slicer abstractions of printer, filament, and print parameters to machine, material, and job settings. 3D printers will become the first class of machines, filaments a class of materials, and print settings will be a job description. Additional machines, materials and jobs may be added, these abstractions enable the description of any job on any machine that converts gcode to action.
  2. Automatic creation of gcode from designer intent represented in shareable 3D files.
    1. 3D files (e.g. .stl, .obj, .step) that represent the finished part.
    2. 2D files (e.g. .dxf, .svg) that represent paths for machines that fundamentally operate in 2D (e.g. pen plotters, vinyl cutters/drag knife, EDM, engravers, hot wire cutters, needle cutters, laser cutters, plasma cutters, CNC for sheet material).

These concepts not only enable CNC mills but all types of machines that convert gcode to action including:

  • 3D printers (Cartesian, Bed lifters, Bed slingers, Belt printers, Deltas, CoreXY, SCARA, …)
  • CNC mills
  • Laser cutters/engravers
  • Pen plotters
  • Vinyl cutters (drag knives)
  • EDM machines (2 or 4 axes)
  • Hot wire foam cutters (2 or 4 axes)
  • Needle cutters (foamboard, closed cell foam)
  • Plasma cutters
  • Waterjet
  • Wire benders
  • CNC Lathes
  • CNC grinder

I have nothing against CADCAM software, it will always be needed for more advanced or commercial machining. You may wonder why I don’t advocate adding a “slice” button to CADCAM packages. This was my initial thought but most packages are closed source. It would be great if open source FreeCAD/ONDSEL incorporated fully automated CAM, but it currently lacks the simple workflow, interface, and parameter organization that already exists in slicers. Slicers have the needed program components organized in nearly the right way to enable a slice button for every CNC machine and being infinitely community extensible. A nice stretch goal would be to enable the slicer to export a FreeCAD project file from the source geometry, generated machine operations, and generated tool-path. This would provide easy gcode generation for hobbyists with a low barrier path from hobbyist to more advanced open source CADCAM.

Slice Everything!

I’m no Adrian Bowyer, I don’t know how to organize and inspire an online community, I am a software architect & developer with a background in graphics and computational geometry who sees how close open source slicers are to automated general purpose CAM systems. I’m also a 3D printing and CNC hobbyist who recognizes a real market pain point and opportunity. (FWIW I invented the magnetic removable steel 3D printing build plate: Hackaday, Workshop 88 Blog)

Community is critical to this type of project just as it has been in the amazing success of 3D printing. I particularly hope this message and its potential for positive disruption resonates in the RepRap community as it is perfectly aligned with their mission: “RepRap is about making self-replicating machines, and making them freely available for the benefit of everyone. We are using 3D printing to do this, but if you have other technologies that can copy themselves and that can be made freely available to all, then this is the place for you too.”

Goals:

  1. Help the developer community organize and plan to advance existing slicer design to be applicable to any job on any machine using any material that makes sense without impacting the existing 3D printing workflow, which enables…
  2. Automatic creation of gcode for any machine, material, and job to be as easy as slicing for 3D printing, which enables…
  3. Sharing of files in common formats that completely capture designer intent that can be easily converted to gcode for any compatible machine or material.
    Thingiverse for everything.

Hopefully some people will share this vision and help organize around it. Perhaps manufacturers of consumer CNC machines and kits, existing slicer developers, frustrated CNC hobbyists, or you.

Please share your thoughts with me, Scott Williamson at scottw@workshop88.com, subject “Slice Everything”.

I have specific technical ideas related to slicer advancement I will cover in a later post.

I will be happy to talk to anyone about it, see you at Midwest RepRap Festival 2024.

tl; dr (summary)

  • Thingiverse and similar sites are successful because there are interchangeable 3D geometry file formats and slicer software makes it simple to 3D print downloaded files
    • .stl (.obj, .stp) are generic file formats that contain the designer’s intent in geometry
    • Slicer software directly converts designer intent from geometry into gcode using parameters in preset categories
  • There is no “slice” button to create CNC gcode in CADCAM software whether it be open source, online, or commercial.
    • .dxf is the primary interchange format for CNC designs, dxf is layered 2D resembling drawings
    • CAM software does not have standardized shareable file formats for machine operations, machine capabilities or even tool (bit) descriptions
    • CAM software does not provide direct conversion of designer intent into gcode for a machine, rather requires manual creation of machine operations and configuration
    • CAM software does not organize presets in such a way as to facilitate a wide variety of machine configurations, job types, and materials
  • Slicer software already contains abstractions for CNC machine, material, and job though they currently target the narrow 3D printing application: printer, filament, print settings
  • Slicer software already contains planar geometry analysis of 3D objects required to create the vast majority of 3 axis CNC tool-paths for many machines
  • Slicer software already contains code for the creation of a sequence of machine operations that are converted into machine readable gcode with a post-processor to adapt to printer firmware
  • Slicer software (SuperSlicer recommended) should be be extended and organized such that:
    • Print settings are replaced with Job settings where 3D printing is a type of job
    • Filament settings are replaced with Material settings where 3D printing plastics are described for 3D printing jobs and additional materials may be added for other types of jobs
    • Printer settings are replaced with Machine settings where details can be captured for a variety of machines including which job types are applicable to which machines
    • Machine operations and parameters for CNC be added and algorithms be extended/created for CNC.
  • Slicer software is and will continue to be open source enabling the community to extend it to support whatever machines that turn gcode to action that exist or may be imagined.
    • Potential types of machines:
      • 3D printer (existing)
      • CNC mills
      • Laser cutter/engraver
      • Pen plotter
      • Vinyl cutter (drag knife)
      • EDM (2 axis, 4 axis)
      • Hot wire foam cutting (2 axis, 4 axis)
      • Needle cutter (foam cutter)
      • Plasma cutter
      • Waterjet
      • Wire bender
      • CNC lathe
      • CNC Grinder
  • One click Slicing for CNC and other machines lowers the barrier to entry for hobbyists/consumers, enables widespread sharing of designs, and has the potential to enable significant market growth
  • Please share your thoughts with me, Scott Williamson at scottw@workshop88.com, subject “Slice Everything”.

D. Scott Williamson
scottw@workshop88.com

Jacks or Better VIDEO POKER in 10 Lines of Atari BASIC

How do you cram Video Poker into 10 lines of 70’s era Atari BASIC?!
This all started Wednesday October 4th, 2023 when Y-Combinator Hacker News posted Show HN: Classic Video Poker(https://lfgslots.com/classicvideopoker/).

I like poker, especially 5 card games like Texas Hold Em’ and Classic Video Poker. I shared the link with a couple other veteran video game developers (Hi Brian & Kevin!), and conversation turned to how video poker should be programmed. This made me wonder if it could be coded in 10 lines of BASIC for the 2024 BASIC 10 Liner Contest .

The Rules

Jacks or Better VIDEO POKER is a 5 card draw poker game. The player places a bet, the deck is shuffled, and 5 cards are dealt. The player then selects which cards to hold and which to discard. Cards are discarded and replaced with new cards from the deck, the hand is evaluated, and if the player holds a winning hand they are rewarded with a payout based on the hand value and proportional to the initial bet. This process is repeated so long as the player has enough money to cover a bet, the player may add money to the game between hands.

The ranking and description of winning hands from highest to lowest:

  • Royal Flush Ten, Jack, Queen, King, Ace in the same suit
  • Straight Flush Five cards of consecutive values in the same suit
  • Four of a Kind Four cards of the same value
  • Full House Three of a kind and Two of a kind
  • Flush Any five cards in the same suit
  • Straight Any five cards of consecutive value, Ace may be low (A2345) or high (TJQKA) but may not wrap (not QKA23)
  • Three of a Kind Three cards of the same value
  • Two Pair Two pairs of cards of the same value
  • Jacks or better Two cards of the same value (“a pair”) that are Jack, Queen, King, or Ace

Cards do not need to be dealt in order in the hand (e.g. 7,8,4,6,5 is a straight equivalent to 4,5,6,7,8)

The Program

It was not easy. Here is the 2024 contest entry including the Atari floppy disk image and instructions: https://bunsen.itch.io/jacks-or-better-video-poker-atari-8-bit-by-d-scott-williamson

The 10 Line BASIC Program

1N0=0:N1=1:N2=N1+N1:N4=N2+2:N5=N4+N1:N9=N5+N4:N10=N5+N5:N13=N9+N4:N14=N13+N1:N15=N14+N1:N17=N15+N2:N19=N15+N4:N21=N19+N2:N25=N15+N10:N29=N25+N4:N119=119:N128=N119+N9:NKE=155:M=N25:B=N1:DIM D$(N119),HN$(165),H(N10+N10),H$(N10),T$(N128),C(N14),DC(N5)

2N7=N5+N2:N8=N7+N1:N16=N15+N1:F.I=N0 TO N19:REA.T:H(I)=T:N.I:O.#N1,N4,N0,"K":POK.82,N0:GR.N0:POK.752,1:SE.N2,N7,N2:SE.N1,N0,15:REA.D$:REA.T$:HN$=T$(N2):REA.T$:HN$(61)=T$(N2):?:F.I=N1 TO N15:?"        ";:N.I:POS.9,2:?"Ö É Ä Å Ï    Ð Ï Ë Å Ò"

3F.I=N9 TO N1 STEP -N1:T=N10-I:POS.N0,T+N4:?HN$(I*N15+N1,I*N15+N15):F.J=N1 TO N5:POS.N10+N1+N5*J,T+N4:?J*H(I+N10):N.J:N.I:?:?:?"Û±ÝÛ²ÝÛ³ÝÛ´ÝÛµÝ      Û­ÝÛ«Ý    Û¤Ý":POS.N0,N21:?,,,"  ÛÅÎÔÅÒÝ":?,,,"  Ä Å Á Ì";

4POS.N29,N19:?"Cash:$";M;" ":POS.N21,N19:?"Bet:$";B:GET#N1,K:B=B+(N1*(K=43)*(B<N5))-((K=45)*(B>1)):M=M+N25*(K=36):POS.N0,20:G.N4+(K=NKE)*(M>=B):D. 0,8,9,11,4,3,13,17,2,1, 0,1,2,3,4,6,9,25,50,250

5?HN$(150,164):?HN$(150,164):?HN$(N1,N15);HN$(N1,N15):F.I=N1 TO20STEP N2:J=INT(RND(N0)*52)*N2+N1:T$=D$(I):D$(I,I+N1)=D$(J):D.²€³€´€µ€¶€·€¸€¹€Ô€Ê€Ñ€Ë€Á€²à³à´àµà¶à·à¸à¹àÔàÊàÑàËàÁಐ³´µ¶·¸¹ÔÊÑËÁ²û³û´ûµû¶û·û¸û¹ûÔûÊûÑûËûÁûÁ²³´µ¶·¸¹ÔÊÑËÁ

6D$(J,J+N1)=T$:N.I:M=M-B:POS.N29,N19:?"Cash:$";M;" ":F.I=N1 TO N9 STEP N2:H$(I,I+N1)=D$(I):N.I: POS.N1,N19:F.I=N0 TO N5:DC(I)=N1:N.I:F.I=N1 TO N9 STEP N2: ?H$(I,I+N1);" ";:N.I:POS.N1,N17:D."               Jacks or BetterTwo Pair       Three of a Kind"

7F.I=N0 TO N4: DC(I)=DC(I)+(K=(I+49)):DC(I)=DC(I)-INT(DC(I)/N2)*N2:?CHR$(72-DC(I)*N4);"  ";:N.I: GET#N1,K: POS.N1,N17:G. N9-N2+(K=NKE): D."Straight       Flush          Full House     Four of a Kind Straight Flush Royal Flush                  "

8?HN$(N1,N13):F.I=N0 TO N4: J=I*N2+N1: T=J+DC(I)*N10:H$(J)=D$(T,T+N1):N.I:POS.N1,N19:F.I=N1 TO N9 STEP N2: ?H$(I,I+N1);" ";:N.I:?:F.I=N1 TO N14:C(I)=N0:N.I:R=N0: F.I=N1 TO N14:F.J=N1 TO N9 STEP N2:T=104+I:C(I)=C(I)+H$(J,J)=D$(T,T):N.J

9R=R+C(I)*C(I)*(I<N14): N.I:S=N0:F.I=N1 TO N10:T=N1:F.J=N0 TO N4:T=T*C(I+J):N.J:S=S+T:N.I:F=N1:F.I=N4 TO N10 STEP N2:F=F*H$(N2,N2)=H$(I,I):N.I:JB=N0:F.I=N10+N1 TO N14:JB=JB+(C(I)=N2):N.I:JB=JB*(R=N5+N2): R1=R+JB-S-F*2-(S*F*(C(N13))):HI=N0:W=N0:?:?

10F. I=N1 TO N10:HI=HI+I*(H(I)=R1):N.I:?" ";HN$(HI*N15+N1,HI*N15+N15);:T=H(HI+N10):F.I=N1 TO B:W=W+T:M=M+T:POS.N21,22:?"WIN $";W:POS.N29,N19:?"Cash:$";M:F.J=(T>N0)*(N16)TO N0 STEP-N4:SO.N1,N29,N10,J:SE.N2,N15-(J=N0)*N8,J+N2:N.J:SE.N2,N7,N2:N.I:G.N4

Program Overview

Line 1 Initialize constants, Dimension arrays

Line 2 Initialize constants, read hand ranks array from data, open keyboard device for key input, set left margin to 0, set graphics mode, hide the cursor, set colors, read deck of cards into D$ from data, read hand names from data into HN$, print white banner and VIDEO POKER title

Line 3 Display the winning hands and payout table, print legends for inputs

Line 4 Begin main game loop, pre-deal loop on line 4, print cash and bet, get input key in K, update bet B based on key if + or – pressed, update money (“Cash”) if $ pressed, GOTO 5 if ENTER pressed, otherwise GOTO 4 to continue pre-deal input loop, DATA statement with hand ranks and payouts

Line 5 Initial print of card bodies, clear previous hand and winning hand string from screen, shuffle the (first 10 cards of) deck D$, data containing initial deck of cards and card values for histogram

Line 6 Shuffle continued, subtract bet from cash to begin hand, update cash display, deal 5 cards by copying first 5 cards from the deck D$ to the hand H$, set 5 discard flags DC to 1, print cards in hand, position cursor to print hold flags, DATA containing hand name strings

Line 7 Discard input loop, loop through 5 cards, if a corresponding number key K is pressed toggle hold/discard flag in DC(I), print H or D based on DC(I), get key from keyboard in K, GOTO 7 loop until enter is pressed, DATA holding hand names

Line 8 Clear hold/discard characters, replace discarded cards with those from the deck, print cards in players hand, clear histogram C(), reset rank, loop over card values to calculate value counts of cards in the players hand to build a histogram

Line 9 Continue building histogram, and accumulate histogram values squared into initial rank R, detect straight, detect flush, detect jacks or better, combine rank, straight, flush, and jacks or better flags into a final rank R1 calculation, initialize the hand index and win to 0 in case the hand is not a winning hand

Line 10 Compare rank to winning hand ranks to get hand index HI, print hand name HN$(HI*15), lookup payout H(HI+10), payout loop once for each bet (1-5), award payout to win W and money M (“Cash”), print winnings and updated Cash balance, loop to cycle colors and ring bell, reset the screen color, GOTO 4 to return to the pre-deal state

The Program Expanded And Explained

Here each line number is multiplied by 100, command abbreviations are expanded and placed on separate lines with remarks.

100 N0=0        : REM CONSTANT VARIABLE 0 
101 N1=1        : REM CONSTANT VARIABLE 1
102 N2=N1+N1    : REM CONSTANT VARIABLE 2 
103 N4=N2+2     : REM CONSTANT VARIABLE 4 
104 N5=N4+N1    : REM CONSTANT VARIABLE 5 
105 N9=N5+N4    : REM CONSTANT VARIABLE 9 
106 N10=N5+N5   : REM CONSTANT VARIABLE 10 
107 N13=N9+N4   : REM CONSTANT VARIABLE 13 
108 N14=N13+N1  : REM CONSTANT VARIABLE 14 
109 N15=N14+N1  : REM CONSTANT VARIABLE 15 
110 N17=N15+N2  : REM CONSTANT VARIABLE 17 
111 N19=N15+N4  : REM CONSTANT VARIABLE 19 
112 N21=N19+N2  : REM CONSTANT VARIABLE 21 
113 N25=N15+N10 : REM CONSTANT VARIABLE 25 
114 N29=N25+N4  : REM CONSTANT VARIABLE 29 
115 N119=119    : REM CONSTANT VARIABLE 119 
116 N128=N119+N9: REM CONSTANT VARIABLE 128 
117 NKE=155     : REM CONSTANT VARIABLE 155 (KEYCODE FOR ENTER)
118 M=N25       : REM INITIALIZE MONEY TO 25 ($)
120 B=N1        : REM INITIALIZE BET TO 1 ($)
121 DIM D$(N119),HN$(165),H(N10+N10),H$(N10),T$(N128),C(N14),DC(N5) : REM DIMENSION VARIABLES
122 REM D$(1,52*2)  IS THE DECK OF CARDS, EACH CARD HAS A NUMBER AND A SUIT 
123 REM D$(105,119) ARE THE CARD NUMBERS FOR CALCULATING HISTOGRAMS A23456789TJKQA
124 REM HN$      IS THE HANDS NAME STRING, FOR DISPLAYING THE RATE TABLE AND HAND WHEN THE PLAYER WINS
125 REM H(20)    IS THE HAND VALUE TABLE, EACH OF THE 10 WINNING HANDS HAS A RANK AND A PAYOUT
126 REM H$(10)   IS THE PLAYERS 5 CARD HAND WHERE EVERY TWO CHARACTERS IS A CARD
127 REM T$(N128) IS A TEMPORARY STRING FOR READING STRINGS FROM DATA AND HOLDING STRINGS FOR CALCULATIONS
128 REM C(14)    HISTOGRAM OF CARD COUNTS IN PLAYERS HAND
129 REM DC(5)    DISCARD FLAG FOR DISCARDING OR HOLDING CARDS
 
200 N7=N5+N2    : REM CONSTANT VARIABLE 7 
201 N8=N7+N1    : REM CONSTANT VARIABLE 8 
202 N16=N15+N1  : REM CONSTANT VARIABLE 16 
203 FOR I=N0 TO N19 : REM LOOP TO READ HAND RANKS AND PAYOUTS FROM DATA INTO ARRAY H
204 READ T:?I,T            : REM READ DATA INTO TEMPORARY VARIABLE
205 H(I)=T            : REM COPY DATA INTO ARRAY (YOU CANNOT READ DATA DIRECTLY INTO AN ARRAY VARIABLE
206 NEXT I            : REM LOOP
207 OPEN #N1,N4,N0,"K": REM OPEN THE KEYBOARD FOR INPUT SO KEYSTROKES CAN BE READ WITHOUT REQUIRING ENTER KEY
208 POKE 82,N0        : REM SET LEFT MARGIN TO 0 FROM DEFAULT OF 2 TO USE ENTIRE SCREEN WIDTH
209 GRAPHICS N0       : REM GRAPHICS 0, CLEAR SCREEN AND SET ATARI 40 COLUMN 24 ROW TEXT SCREEN MODE
210 POKE 752,1        : REM HIDE CURSOR
211 SETCOLOR N2,N7,N2 : REM SET THE BACKGROUND COLOR TO DARK BLUE
212 SETCOLOR N1,N0,15 : REM SET THE TEXT COLOR TO BRIGHT WHITE
213 READ D$           : REM READ THE DECK OF CARDS STRING FROM DATA
214 READ T$           : REM READ PARTIAL HAND NAMES FROM DATA
215 HN$=T$(N2)        : REM COPY THE HAND NAMES STRING SKIPPING THE INITIAL DOUBLE QUOTE CHARACTER IN T$
216 READ T$           : REM READ MORE OF THE HAND NAMES FROM THE NEXT DATA STATEMENT
217 HN$(61)=T$(N2)    : REM COPY THE HAND NAMES TO THE END OF HN$ SKIPPING THE INITIAL DOUBLE QUOTE CHARACTER IN T$
218 PRINT             : REM PRINT A BLANK LINE 
219 FOR I=N1 TO N15   : REM LOOP TO PRINT WHITE BANNER AT THE TOP OF THE SCREEN 
220 PRINT "        "; : REM PRINT 8 INVERTED SPACES
221 NEXT I            : REM LOOP
222 POSITION 9,2                  : REM POSITION CURSOR IN MIDDLE OF THE BANNER
223 PRINT "Ö É Ä Å Ï    Ð Ï Ë Å Ò": REM PRINT INVERTED "V I D E O   P O K E R"

300 FOR I=N9 TO N1 STEP -N1       : REM LOOP TO DISPLAY THE WINNING HANDS AND PAYOUTS FOR BETS
301 T=N10-I                       : REM TEMPORARY VARIABLE T INDEXES THE ROW POSITION ON THE SCREEN IN DESCENDING ORDER
302 POSITION N0,T+N4              : REM POSITION THE CURSOR TO PRINT THE NAME OF THE HAND
303 PRINT HN$(I*N15+N1,I*N15+N15) : REM PRINT HAND NAME SUBSTRING OUT OF HN$ INDEXED BY I.  EACH NAME IS 15 CHARACTERS LONG
304 FOR J=N1 TO N5                : REM LOOP OVER THE BET AMOUNTS FROM 1 TO 5
305 POSITION N10+N1+N5*J,T+N4     : REM POSITION THE CURSOR IN THE RIGHT COLUMN AND ROW TO DISPLAY THE PAYOUT FOR HAND I AT BET LEVEL J
306 PRINT J*H(I+N10)              : REM PRINT PAYOUT FOR HAND I AT BET LEVEL J
307 NEXT J                        : REM LOOP OVER BET LEVELS
308 NEXT I                        : REM LOOP OVER HANDS
309 PRINT                         : REM PRINT A BLANK LINE
310 PRINT                         : REM PRINT A BLANK LINE
311 PRINT "Û±ÝÛ²ÝÛ³ÝÛ´ÝÛµÝ      Û­ÝÛ«Ý    Û¤Ý" : REM PRINT KEY LEGEND, INVERTED "[1][2][3][4][5]   [-][+]   [$]"
312 POSITION N0,N21               : REM POSITION CURSOR TO PRINT DEAL KEY LEGEND
313 PRINT ,,,"  ÛÅÎÔÅÒÝ"          : REM PRINT INVERTED "[ENTER]" TO THE RIGHT
314 PRINT ,,,"  Ä Å Á Ì";         : REM PRINT INVERTED "D E A L" TO THE RIGHT, SEMICOLON PREVENTS NEWLINE FROM SCROLLING THE SCREEN
    
400 POSITION N29,N19              : REM THIS IS THE BET AND CASH INPUT LOOP, POSITION THE CURSOR TO PRINT PLAYER CASH
401 PRINT "Cash:$";M;" "          : REM PRINT THE AMOUNT OF MONEY THE PLAYER HAS, TRAILING SPACE NEEDED TO CLEAR DIGITS AS PLAYER CASH DECREASES
402 POSITION N21,N19              : REM POSITION THE CURSOR TO PRINT THE PLAYER BET 
403 PRINT "Bet:$";B               : REM PRINT THE PLAYER BET LEVEL
404 GET#N1,K                      : REM READ A KEY FROM THE KEYBOARD INTO K
405 B=B+(N1*(K=43)*(B<N5))-((K=45)*(B>1)) : REM UPDATE BET, ADD 1 ONLY IF K=43 ("+") AND THE BET IS LESS THAN 5, SUBTRACT 1 IF K=45 ("-") AND B>1
406 M=M+N25*(K=36)                : REM UPDATE PLAYER MONEY, ADD 25 ONLY IF THE "$" KEY WAS PRESSED WHEN K=36
407 POSITION N0,20                : REM POSITION CURSOR TO DISPLAY CARDS
408 GOTO 100*(N4+(K=NKE)*(M>=B))  : REM GOTO N4+(K=NKE)*(M>=B), GOTO IF ENTER PRESSED AND MONEY>=BET GOTO 5 (DEAL), OTHERWISE GOTO 4 (LOOP)
409 DATA 0,8,9,11,4,3,13,17,2,1, 0,1,2,3,4,6,9,25,50,250
410 REM DATA ON LINE 409 IS 10 HAND RANKS FOLLOWED BY 10 CORRESPONDING HAND VALUES
    
500 PRINT HN$(150,164)              : REM PRINT FIVE PAIRS OF INVERTED SPACES TO LOOK LIKE CARDS FROM THE END OF THE HAND NAMES STRING
501 PRINT HN$(150,164)            : REM PRINT FIVE PAIRS OF INVERTED SPACES TO LOOK LIKE CARDS FROM THE END OF THE HAND NAMES STRING
502 PRINT HN$(N1,N15);HN$(N1,N15) : REM PRINT 15 BLANK SPACES TWICE TO CLEAR THE HAND AND "WIN$:X" STRINGS FROM THE SCREEN (THE FIRST HAND NAME IS BLANK, USED FOR ALL LOSING HANDS)
503 FOR I=N1 TO 20 STEP N2        : REM SHUFFLE THE DECK, ONLY THE FIRST 10 CARDS AS THEY ARE THE ONLY ONES THAT MAY BE USED IN EACH HAND
504 J=INT(RND(N0)*52)*N2+N1       : REM PICK ANOTHER RANDOM CARD IN THE DECK INDEXED BY J TO SWAP WITH CARD AT INDEX I
505 T$=D$(I)                      : REM COPY THE DECK AT I TO T$
506 D$(I,I+N1)=D$(J)              : REM COPY THE 2 CHARACTER CARD FROM LOCATION J IN THE DECK TO LOCATION I
507 DATA²€³€´€µ€¶€·€¸€¹€Ô€Ê€Ñ€Ë€Á€²à³à´àµà¶à·à¸à¹àÔàÊàÑàËàÁಐ³´µ¶·¸¹ÔÊÑËÁ²û³û´ûµû¶û·û¸û¹ûÔûÊûÑûËûÁûÁ²³´µ¶·¸¹ÔÊÑËÁ 
508 REM THE DATA IN LINE 507 IS THE INVERTED CHARACTERS OF THE DECK OF CARDS FOLLOWED BY VALUES OF CARDS (A123456789TJQKA) USED TO CALCULATE RANKING HISTOGRAM
    
600 D$(J,J+N1)=T$                 : REM COPY THE TWO CHARACTER CARD FROM POSITION I SAVED IN T$ TO LOCATION J
601 NEXT I                        : REM LOOP TO SHUFFLE NEXT CARD
602 M=M-B                         : REM SUBTRACT BET FROM MONEY
603 POSITION N29,N19              : REM POSITION CURSOR TO PRINT PLAYER CASH
604 PRINT "Cash:$";M;" "          : REM PRINT PLAYER CASH
605 FOR I=N1 TO N9 STEP N2        : REM LOOP TO DEAL 5 CARDS
606 H$(I,I+N1)=D$(I)              : REM COPY THE FIRST 5 CARDS FROM THE DECK INTO THE PLAYERS HAND 2 CHARACTERS AT A TIME 
607 NEXT I                        : REM LOOP DEAL
608 POSITION N1,N19               : REM POSITION CURSOR TO PRINT CARDS
609 FOR I=N0 TO N5                : REM LOOP TO SET ALL 5 DISCARD FLAGS TO 1
610 DC(I)=N1                      : REM SET DISCARD FLAG I TO 1
611 NEXT I                        : REM LOOP INITIALIZE DISCARD FLAGS
612 FOR I=N1 TO N9 STEP N2        : REM LOOP TO PRINT CARDS
613 PRINT H$(I,I+N1);" ";         : REM PRINT 2 CHARACTER CARD FROM THE HAND FOLLOWED BY A SPACE
614 NEXT I                        : REM LOOP TO PRINT CARDS
615 POSITION N1,N17               : REM POSITION CURSOR TO PRINT DISCARD/HOLD FLAGS
616 DATA"               Jacks or BetterTwo Pair       Three of a Kind" : REM FIRST DATA FOR HN$
    
700 FOR I=N0 TO N4                : REM DISCARD/HOLD INPUT LOOP, LOOP THROUGH 5 CARD DISCARD/HOLD VALUES
701 DC(I)=DC(I)+(K=(I+49))        : REM INCREMENT D(I) DISCARD/HOLD IF K=49+I, KEYS 1-5 ARE KEY CODES 49-53
702 DC(I)=DC(I)-INT(DC(I)/N2)*N2  : REM EFFECTIVELY MODULUS 2, LIMITS THE VALUES TO 1 OR 0
703 PRINT CHR$(72-DC(I)*N4);"  "; : REM PRINT "D" IF DC(I) IS 1 OR "H" IF IT IS NOT FOLLOWED BY TWO SPACES
704 NEXT I                        : REM LOOP THROUGH DISCARD ARRAY
705 GET #N1,K                     : REM GET KEY FROM KEYBOARD
706 POSITION N1,N17               : REM POSITION CURSOR TO PRINT DISCARD VALUES
707 GOTO 100*(N9-N2+(K=NKE))      : REM GOTO N9-N2+(K=NKE), IF ENTER PRESSED GOTO 800 OTHERWISE GOTO 700
708 DATA"Straight       Flush          Full House     Four of a Kind Straight Flush Royal Flush                  " : REM SECOND DATA FOR HN$

800 PRINT HN$(N1,N13)             : REM PRINT BLANK HAND NAME OVER DISCARD VALUES TO REMOVE THEM
801 FOR I=N0 TO N4                : REM LOOP THROUGH 5 CARDS TO DEAL NEW CARDS FOR DISCARDS
802 J=I*N2+N1                     : REM CALCULATE INDEX j OF CARD I IN THE HAND H$ (AND IN THE DECK D$ ON DEAL)
803 T=J+DC(I)*N10                 : REM CALCULATE INDEX OF THE CARD IN THE DECK, IF DISCARD IS 1, SELECT CORRESPONDING CARD FROM NEXT 5 CARDS IN THE DECK
804 H$(J)=D$(T,T+N1)              : REM COPY CARD FROM THE DECK TO THE HAND (EITHER HOLD OR DISCARD AND REDEAL CARD+5)
805 NEXT I                        : REM LOOP DEAL DISCARDS
806 POSITION N1,N19               : REM POSITION CURSOR TO PRINT CARDS
807 FOR I=N1 TO N9 STEP N2        : REM LOOP OVER CARDS TO PRINT CARDS
808 PRINT H$(I,I+N1);" ";         : REM PRINT 2 CHARACTER CARD FROM HAND AT INDEX I
809 NEXT I                        : REM LOOP
810 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING
811 FOR I=N1 TO N14               : REM LOOP OVER CARD VALUES TO CLEAR HISTOGRAM
812 C(I)=N0                       : REM SET HISTOGRAM VALUE C(I) TO ZERO
813 NEXT I                        : REM LOOP
814 R=N0                          : REM SET RANK TO 0
815 FOR I=N1 TO N14               : REM LOOP I OVER CARD VALUES TO CALCULATE HISTOGRAM IN C()
816 FOR J=N1 TO N9 STEP N2        : REM LOOP J OVER CARD INDEXES IN HAND
817 T=104+I                       : REM SET TEMPORARY VALUE TO 104+I,INDEX OF VALUE CHARACTER IN DECK STRING
818 C(I)=C(I)+H$(J,J)=D$(T,T)     : REM ADD 1 TO C(I) IF THE VALUE CHARACTER IN THE HAND FOR CARD J MATCHES THE VALUE CHARACTER FROM THE DECK STRING
819 NEXT J                        : REM LOOP NEXT CARD
    
900 R=R+C(I)*C(I)*(I<N14)         : REM RANK IS THE SUM OF THE CARD COUNTS SQUARED ONLY INCLUDING THE ACE ONCE (IT'S INCLUDED TWICE TO DETECT STRAIGHTS)
901 NEXT I                        : REM NEXT CARD VALUE, R CONTAINS RANK SUM OF SQUARES OF CARD COUNTS FOR THE HAND 
902 S=N0                          : REM SET STRAIGHT FLAG S TO ZERO
903 FOR I=N1 TO N10               : REM LOOP OVER LOWEST CARD FOR 10 POSSIBLE STRAIGHTS (A-5 THROUGH T-A)
904 T=N1                          : REM SET TEMP TO 1
905 FOR J=N0 TO N4                : REM LOOP THROUGH 5 CONSECUTIVE CARD COUNTS C(I+J) IN THE HISTOGRAM
906 T=T*C(I+J)                    : REM MULTIPLY T BY THE HISTOGRAM VALUE AT I+J
907 NEXT J                        : REM LOOP NEXT CARD
908 S=S+T                         : REM ADD T TO S, T CAN ONLY BE 1 IF THE HAND CONTAINS 5 CONSECUTIVE CARDS
909 NEXT I                        : REM LOOP NEXT POTENTIAL STRAIGHT
910 F=N1                          : REM SET FLUSH DETECTION TO 1
911 FOR I=N4 TO N10 STEP N2       : REM LOOP HAND INDEX I FROM THE SECOND CARD SUIT TO THE FIFTH CARD SUIT 
912 F=F*H$(N2,N2)=H$(I,I)         : REM MULTIPLY THE FLUSH FLAG BY ONE ONLY IF THE SUIT CHARACTER OF THE FIRST CARD MATCHES THE SUIT OF THE INDEXED CARD
913 NEXT I                        : REM NEXT CARD SUIT INDEX, WHEN DONE F WILL ONLY BE 1 IF ALL CARDS ARE THE SAME SUIT
914 JB=N0                         : REM SET JACKS OR BETTER FLAG TO 0
915 FOR I=N10+N1 TO N14           : REM LOOP INDEX I THROUGH THE CARD HISTOGRAMS FOR JACK (11) THROUGH HIGH ACE (14)
916 JB=JB+(C(I)=N2)               : REM ADD 1 TO JB ONLY IF THE HISTOGRAM FOR VALUE I IS EQUAL TO 2
917 NEXT I                        : REM LOOP THROUGH JACKS OR BETTER CARD COUNTS
918 JB=JB*(R=N5+N2)               : REM JB IS MULTIPLIED BY 1 ONLY IF THE RANK IS 7, OTHERWISE 0.  RANK 7 IS A SINGLE PAIR (2^2+3*1^2)
919 R1=R+JB-S-F*2-(S*F*(C(N13)))  : REM R1 IS THE FINAL COMPOSITE HAND RANK INCORPORATING FLUSH, STRAIGHT, JACKS OR BETTER FLAG, AND DETECTION OF ROYAL FLUSH (S*F*(C(N13)))
920 HI=N0                         : REM SET THE HAND INDEX HI TO ZERO
921 W=N0                          : REM WIN AMOUNT TO ZERO 
922 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING
923 PRINT                         : REM PRINT BLANK LINE IN PREPARATION FOR PRINTING HAND DESCRIPTION AND WINNING

1000 FOR I=N1 TO N10              : REM LOOP THROUGH THE POTENTIAL WINNING HANDS (INDEX 1 - 10)
1001 HI=HI+I*(H(I)=R1)            : REM ADD WINNING HAND INDEX I TO HI ONLY IF THE HAND RANK EQUALS THE HAND RANK AT THE INDEX H(I)
1002 NEXT I                       : REM LOOP NEXT WINNING HAND, WHEN THIS IS DONE HI WILL CONTAIN AN INDEX INTO HAND NAMES AND PAYOUTS 
1003 PRINT " ";HN$(HI*N15+N1,HI*N15+N15); : REM PRINT HAND NAME, THE NAME AT INDEX ZERO IS BLANK FOR NON-WINNING HANDS
1004 T=H(HI+N10)                  : REM COPY THE WINNING BASE PAYOUT TO T
1005 FOR I=N1 TO B                : REM LOOP FROM 1 TO BET, HIGHER BETS, MORE WINNING PIZAZZ
1006 W=W+T                        : REM WITH EACH ITERATION OF THE LOOP THE WIN INDICATOR IS AWARDED THE BASE PAYOUT
1007 M=M+T                        : REM WITH EACH ITERATION OF THE LOOP THE PLAYERS MONEY IS INCREASED BY T
1008 POSITION N21,22              : REM POSITION CURSOR TO PRINT PLAYER WINNINGS
1009 PRINT "WIN $";W              : REM PRINT WINNINGS
1010 POSITION N29,N19             : REM POSITION CURSOR TO PRINT PLAYER MONEY
1011 PRINT "Cash:$";M             : REM PRINT PLAYER MONEY
1012 FOR J=(T>N0)*(N16)TO N0 STEP-N4 : REM IF THE WINNING BASE PAYOUT IS NOT ZERO, LOOP FROM 16 TO 0 
1013 SOUND N1,N29,N10,J           : REM MAKE WINNING SOUND
1014 SETCOLOR N2,N15-(J=N0)*N8,J+N2 : REM SET SCREEN TO EXCITING WINNING COLOR
1015 NEXT J                       : REM END OF MULTIMEDIA REWARD LOOP (END OF FLASH AND DING)
1016 SETCOLOR N2,N7,N2            : REM RESTORE SCREEN COLOR
1017 NEXT I                       : REM LOOP BET AMOUNT TIMES
1018 GOTO 100*N4:REM GOTO N4      : GO TO BET, MONEY, AND DEAL INPUT LOOP LINE

Programming techniques

The deck of cards

The program manages a deck of 52 cards in a D$ where each card is represented by a value character (2-9,T,J,Q,K,A) and suit character (hearts, clubs, diamonds, spades). Generating cards randomly risks dealing duplicate cards and checking for duplicates is more work than managing a deck. The initial deck is read into D$ from data in line 2. The deck is shuffled in a loop spanning lines 5 and 6 by swapping each card with a random one in the deck. Initially, the program shuffled all 52 cards but since the deck is shuffled between each deal and each hand can only use a maximum of the first 10 cards (5 cards discarded and re-dealt) it is only necessary to shuffle the first 10 cards of the deck between hands reducing the delay considerably.

Ranking hands

This is the heart of this program, it accurately ranks a Jacks or Better Poker hand in about 1 line of code spanning 2 lines of this program. The ranking system must accurately rank hands according to many rules and be compact enough to fit in the 10 line program. IF … THEN logic would quickly consume too many lines so I needed to devise a ranking system. The hand ranks from highest to lowest are:

  • Royal Flush : Ten, Jack, Queen, King, Ace in the same suit
  • Straight Flush : Five cards in a row in the same suit
  • Four of a Kind : Four cards of the same value
  • Full House : Three of a kind and Two of a kind
  • Flush : Any five cards in the same suit
  • Straight : Any five cards in order, Ace may be low (A2345) or high (TJQKA) but may not wrap (not QKA23)
  • Three of a Kind : Three cards of the same value
  • Two Pair : Two pairs of any cards of the same value
  • Jacks or better : Two cards of the same value that are Jack, Queen, King, or Ace

Some combination of rules like a “Royal Straight” Ten, Jack, Queen, King, Ace are not legal Poker Hands and must not fall out of the hand ranking system.
To rank a hand several calculations are made and flags are set then combined to determine a numerical rank used to lookup hand values from a table.
Variables used in ranking:

  • R Initial Rank
  • R1 Final Rank
  • S Straight detection (1 if there are 5 sequential card values in the hand in any order)
  • F Flush detection (1 if all 5 cards in the hand are the same suit)
  • JB Jacks or Better detection (1 only if there is a single pair in the hand (rank 7) and the pair is Jack, Queen, King, or ACE (a 2 in the histogram for one of those values)
  • C(14) Counts (histogram) of cards A123456789TJQKA (includes A at both ends for straight detection)

Steps in the calculation

  • Count the number of cards of each value in the hand into array C to create the histogram (line 8-9). A is counted twice, once at each end for straight calculation later.
  • The sum of the squares of the histogram values (line 9) for the first 13 histogram values (so A is not included twice) is accumulated in the initial rank R. At this point the rank values have the following meanings:
    • 17 (4*4)+1 Four of a kind
    • 13 (3*3)+(2*2) Full house
    • 11 (3*3)+1+1 Three of a kind
    • 9 (2*2)+(2*2)+1 Two pair
    • 7 (2*2)+1+1+1 Pair
    • 5 1+1+1+1+1 Five different value cards
  • Search for straights (line 9): Use a nested loop 0-10 then 0-4 to multiply consecutive card counts in the histogram A straight will be 5 ones in a row so S can only be 1 if there are 5 cards in a row in any order in the hand.
  • Search for flush (line 9) by comparing the suit of the last 4 cards in the hand to the suit of the first and take the product of the comparisons. It can only be 1 if all cards are the same suit.
  • Search for Jacks or Better JB (line 9) loop through counts of Jack, Queen, King, and Ace and add 1 to JB if the count is 2, then keep the result only if the initial rank is 7 (single pair). JB is only a 1 if the hand contains a single pair of Jacks or better.
  • The final rank calculation (line 9) combines R with JB, S, F, and the count of Aces (C(13)) as follows R1=R+JB-S-F*2-(S*F*(C(N13))) resulting in the final hand ranking. Values have the following meanings:
    • 17 (4*4)+1 Four of a kind
    • 13 (3*3)+(2*2) Full house
    • 11 (3*3)+1+1 Three of a kind
    • 9 (2*2)+(2*2)+1 Two pair
    • 8 7+JB Jacks or better
    • 7 (2*2)+1+1+1 Pair (use jacks or better)
    • 5 1+1+1+1+1 Nothing
    • 4 5-S Straight
    • 3 5-2*F flush
    • 2 5-S-2*F Straight flush
    • 1 5-S-2*F-(S*F*(C(N13))) Royal flush
  • The winning hand values are stored in the table with the payouts. A loop scans all the potential winning hands to see if the rank matches one of the hands (line 10) and if so lookup the corresponding payout

Payout

If the player wins then the payout loop flashes the screen and rings the bell once for each dollar bet and adds the payout amount to the players total cash each time.

Audio Visuals

Just like in real gambling machines, wins are accompanied by ringing bells and flashing lights while losses pass silently leaving only reward and anticipation to drive continued play.

Input method

There are too many options to use the joystick and it would have been unintuitive and using BASIC’s INPUT requires pressing enter after every input and validating inputs so I decided to access the keyboard device to get blocking input on individual key-presses. The keyboard device is opened for input in line 2 O.#N1,N4,N0,"K" (OPEN #1,4,0,"K") then keys are retrieved in input loops in line 4 and 7 GET#N1,K (GET #1,K). The GET statement waits for a key and returns the ATASCII value in K which is used in conditionals in assignment and GOTO statements. Examples include pressing $ to add money to the player account M=M+N25*(K=36) which adds 25 if K= 36 the ATASCII code for $ and 0 otherwise or G.N4+(K=NKE)*(M>=B) which loops at line 4 until ENTER (ATASCII 155 assigned to NKE for number keyboard enter) is pressed and the player has enough money to cover their bet.

Use of conditional expressions as integers (booleans)

Atari BASIC has no ELSE, so everything to the right of THEN (on one of only 10 BASIC program lines) is only executed if the IF clause is true placing a hard limit on the amount and structure of logic possible in a short program. The result of a comparison (=,<,<=,>,>=,<>) is evaluated to 1 if true and 0 if false. This can be used to create conditional calculations in without using IF … THEN. This technique is used extensively in this program to handle input, make conditional calculations in hand ranking, and in GOTO statements to vector to intended lines based on conditions.

Use of variables for constants

From the Atari BASIC Manual: “Each time a constant (4,5,16,3.14159, etc.) is used, it takes 7 bytes. Defining a new variable requires 8 bytes plus the length of the variable name (in characters). But each time it is used after being defined, it takes only I byte, regardless of its length.
Lines that contain too many constants overflow the internal tokenization buffer and cannot use the maximum 253 characters per line permitted by the Atari BASIC input buffer length. Using variables for constants is much more compact, furthermore deriving constants from other constants is even more compact during initialization.

Modulus operator

The following modulus 2 calculation is used in line 5 to toggle holding cards:
DC(I)=DC(I)-INT(DC(I)/N2)*N2
Atari BASIC does not have a modulus operator, modulus(a,b) returns the remainder of a/b
Instead A-INT(A/B)*B is used, let’s look at it in parts.
INT(A/B) is the integer portion of A/B, aka truncated value of A/B, aka ceil(A/B)
INT(A/B)*B is the integer portion of A larger than
B A-INT(A/B)*B Subtracting the integer portion of A larger than B from A leaves the remainder or modulus.

Conclusion

I love these highly constrained programming challenges and I think Jacks or Better VIDEO POKER turned out great. It’s certainly been fun play-testing it! I think this style of technical programming is probably my favorite part of developing games. Working with Atari, especially Atari BASIC, is a special kind of nostalgia for me that always brings me back to my programming and literal adolescence. I hope you found this entertaining, wish me luck! Contest results to be announced April 6th 2024.

PCBWay Sponsors Workshop 88 Makerspace

We’d like to welcome Workshop 88‘s newest sponsor: PCBWay!

PCBWay provides PCB Manufacturing, assembly, CNC, 3D Printing services, and more.

We’d like to thank PCBWay for helping Workshop 88 makerspace achieve our mission of providing tools and technology access to makers. Watch for exciting new Workshop 88 projects using PCBWay products and services.

If you’d like to sponsor or support Workshop 88 makerspace, send us an email at info@workshop88.com

Revisiting the Art Institute of Chicago Project

I’d written previously about my project to display images of artworks owned by the Art Institute of Chicago (ARTIC) https://workshop88.com/oldblog/index.php/2023/01/14/the-adafruit-pyportal-and-modifying-adafruit-libraries/. The challenge was that, unlike the project that inspired it, the ARTIC’s REST API does not return the full URL path to the image files. I solved it by patching the source code for the PyPortal library. It was clunky and stopped me from using that PyPortal for any other project.

Aha!

Then, inspiration struck. I had an under utilized Raspberry Pi W and decided to implement a web server on it that could take the response from the ARTIC, massage it and forward it on to the PyPortal. So rather than talking directly to ARTIC, the PyPortal talks to the Raspi, which talks to ARTIC. I used Bottle to build the web server.

Enhance the original

To spice up this second version, I implemented a “Happy Birthday, <artist>” system. The PyPortal asks the Raspi for the URL of the nth image to be displayed. The Raspi looks up which artists have birthdays today and asks ARTIC if it owns any pieces by those artists. From that search results list, the Raspi constructs the URL to the nth image. It puts the image URL, the artist’s name, the artwork’s title, and the dimensions of the image into a dictionary structure and sends a JSON response back to the PyPortal. The PyPortal downloads the image and displays it on the screen.

New things to learn

For this project to work, Raspi had to compose a query for ARTIC that returned only pieces by the artists desired. It would be poor form to display Pearson’s works on Beardson’s birthday. The prior project didn’t require such high precision. “/artworks/search/?q=impressionism” was adequate to get a lot of very beautiful imagess. For greater precision, I was going to have to learn complex queries using Elasticsearch’s Query DSL. The engineering staff at the ARTIC was very responsive in providing support, but I still struggled. I eventually found an on-line course on Udemy.com for the complete Elasticsearch stack. Section 3 of the course was DSL. I signed up for a free trial and completed the training.

Learning DSL allowed me to develop the following process:

  1. PyPortal request to Raspi: Get the nth image’s URL
  2. Raspi table lookup: What artists were born today?
  3. Raspi request to ARTIC: Perform a query to get those artists’ ARTIC IDs
  4. Raspi to ARTIC: Perform a query to get the URL of the nth image by those artists
  5. Raspi to PyPortal: Massage ARTIC’s response and send the modified image URL to PyPortal
  6. PyPortal: Download and display the image
  7. Rinse/Repeat

Here is what the Raspi uses to find artists IDs:

criteria = {
 "query": {
  "bool": {
   "should": [
    {"match_phrase":{"title":     {"query":artist_name,"slop":1}}},
    {"match_phrase":{"alt_titles":{"query":artist_name,"slop":1}}},
    ], # should
   "minimum_should_match": 1,
  }, # bool
 }, # query
} # criteria

The query takes the artist’s name and tries to find that name in the title field. It also looks for that name in the alt_titles fields. The ‘slop:1’ specifies that ‘Louis Sullivan’ (the name in the DailyArtFixx.com birthday database) is a close enough match to ‘Louis H. Sullivan’ (the name used by the ARTIC).

The alt_titles field is necessary because the artist Maurice Quentin de La Tour (DailyArtFixx.com) has all of the following aliases:

      "Maurice Q. de Latour",
      "Maurice Quentin La Tour",
      "Maurice-Quentin de La Tour",
      "Maurice-Quentin de La Tour",
      "Maurice-Quentin de la Tour",
      "Maurice Quentin De La Tour",
      "Maurice Quentin Delatour",
      "Maurice Quentin de Latour",
      "Maurice Quentin de La Tour"

Misspellings are also a risk. Taddeo Zuccari is also known as Taddeo Zuccaro. Hopefully, the alt_titles will catch this. If it doesn’t, there is still a “fuzziness” factor that can be added to the query. Fuzziness can handle individual letter insertions, deletions and transpositions. The greater the fuzziness factor, the more errors it can tolerate — at the risk of including someone who should not be included. Combining fuzziness and slop makes for a very complex query and since alt_titles seems to be working, I only use 1 degree of slop and skip fuzziness.

If Raspi is successful finding artist_name, it extracts that artist’s unique ARTIC ID. I do the above query for each artist having a birthday today. I put all the found IDs into a list.

I then loop over all the IDs and build something like the following query to find all the ARTIC artworks. The filter clauses ensure that the ARTIC has an image of the artwork and that there is a title and artist name associated to it. In the following example, I use IDs to find all artworks by two artists — Maurice LaTour and Casper Friedrich.

criteria = {"query": 
   {"bool": {	
      "should": [
         {"match": {"artist_id": 34563}},
         {"match": {"artist_id": 34185}}],
      "minimum_should_match": 1,
      "filter": [
         {"exists": {"field" : "image_id"}},
         {"exists": {"field" : "artist_title"}}, 
         {"exists": {"field" : "title"}}, ], }, }, } 

Every time the PyPortal asks for the nth image, the Raspi sends back data from the nth image in this list.

While there are plenty of opportunities for refactoring and bullet proofing the code, I consider the project complete and a success. It is running continuously in my workshop — Happy Birthday, artists!

Final thoughts

The brains of the project were moved from the PyPortal into the Bottle web server. I can change the server and add any number of parallel query schemes — all the works in the Narcissa Niblack Thorne miniatures gallery, all artwork associated with the Caravaggio exhibit, images of cats, stained glass, architectural models, etc. What would you like to see?

Fireworks Deck Umbrella: Have a Safe and Happy Fourth of July

When I was first learning about microcontrollers and blinking lights, I’d seen a YouTube video by Dave Plummer, a retired system engineer at Microsoft (he wrote Task Manager). In that video, he creates what he called a Tiki Umbrella. It’s a deck umbrella tricked out with LED strips along the ribs that produce various animation effects.

I thought that would be a nice project to do for the 4th of July.

My deck umbrella has eight ribs — so, an LED strip for each rib. My original plan was to have a ninth strip running up the pole — to mimic the launch of the firework shell into the air before it explodes.

As with many of my projects, I procrastinated and ran short of time. July 4th was a hard stop date. I also lacked inventory on all the appropriate components. So the project that I implemented was not all that I envisioned.

Because the LED strips would be somewhat sheltered from the elements under the umbrella, I didn’t feel the need for IP65 or higher (IP stands for ingress protection). I settled on two roles of Inland brand, 5-meter, WS2812B, 60 LEDs/meter strips. Given the length of the umbrella ribs, this works out to 76 pixels per rib.

For the microcontroller, I used an RP2040 Scorpio, running CircuitPython and the Adafruit LED Animation library. Here’s the wiring diagram.

Rather than a breadboard for an outdoor project, I decided to swallow my fear of soldering and used a permanent proto-board as the diagram suggests. That’s about 50 solders. Face your fear and push through it, MACE! Surprisingly, it was much easier that I’d expected. Flux and good, Pb solder makes a big difference.

I’d done all the soldering on my workbench and tested the animation software as I went — solder a strip, test the software, rinse, repeat.

Once all the strips were working, it was time to attach it all to the umbrella. The problem is that the whole circuit was one huge, heavy, awkward-to-handle octopus. The protective gel covering the LEDs loved to stick to everything it came in contact with — the table, other strips, the tags I’d attached to identify each strand, the umbrella ribs, etc. I would have liked to have 10 extra hands during install.

A much better approach would have been to build components, attach them to the umbrella, and then connect them together. Rather than solder the LEDs to the proto-board, a pair of JST 3-pin connectors with long leads would have done nicely. That way, I could have positioned the microcontroller on the umbrella, then positioned a strip on a rib, then connected the two.

This would make repairs easier also. Sadly, the solder on one of the strips failed during install and had to be repaired. It was next to impossible to bring a soldering iron up to the top of the umbrella. Luckily, I was able to take down the strip (it was no longer connected to the proto-board), solder some STRANDED wire (for increased flexibility), add heat-shrink to the pad (for greater strain relief), and clip on a few Wago connectors. Then I just re-attached the strip to the rib, and snappend the proto-board’s dangling wires into the Wagos. That’s the way I wish I’d done ALL the strips, but I didn’t have any Wagos and wasn’t sure if I could get them by the 4th.

The microcontroller and proto-board had their own installation challenges. I thought the pole and ribs were plastic. It turns out they’re metal. That’s a big short-circuit risk. I got around the problem by taping the proto-board to a piece of styrofoam to insulate it from the pole.

The placement of the microcontroller at the top of the pole makes programming changes an issue too! Here’s a picture of me having to put a music stand on top of the patio table in order to put the laptop within reach of the microcontroller USB cable. This is how I added additional effects post-install.

One final shortcoming of my install — you can’t close the umbrella in a storm. And I never animated the pole.

So…let’s just call this a prototype — better luck next holiday!

Enjoy the show!

P.S. The hot, humid weather of the 4th caused the adhesive on the back of the LED strips to fail. The strips started to hang straight down putting a lot of stress on solder connections. I ended up fixing this by using clear, packing tape. I’d lay a piece onto the LED side of the strip and then fold it around the edges until it grabbed the umbrella rib. So far, so good.

Color coded hex driver hack tool-tip

Organize your hex drivers using color

I use metric hex drivers to work on drones, 3D Printers, CNC machines, and a lot of other projects. My hex drivers have cylindrical handles with the metric size printed on one side so most of the time the sizes are not visible. Here are some random pictures of my desktop during projects with the drivers circled.

Drivers

My drivers are the common 1.5mm, 2.0mm, 2.5mm, and 3.0mm sizes typically used for M2, M2.5, and M3 hardware. You can see that even when the drivers are oriented correctly the straps in my RaceDayQuads drone field kit cover the dimensions.

Color Codes

I thought it would be great to color code them using the grooves in the handles. I already know the resistor color code standard and thought it would be natural to use the same colors to represent driver sizes.

I decided to use the color coding for the two digit size but skip the third exponent band as I don’t have 10mm+ drivers. I use a fixed “a.b” encoding where “a” is millimeters, and “b” is tenths of a millimeter. For example, the 2.5mm driver would have a red stripe for 2 followed by a green stripe for 5.

Colorful Rubber Bands

I was going to use enamel paint to manually paint the driver handles, but that would be arduous and potentially messy when it occurred to me I could use rubber bands. There are a lot of colorful pony tail and craft rubber band kits on the internet but it took a lot of searching to find a rubber band kit that had all of the resistor color code colors, which include a full rainbow plus black, grey, white, and brown. Then I found the Funtopia 1500 Pcs Small Elastic Hair Ties with Organizer Box Colorful for Girls, Kids, Hair Elastics for Thin or Thick Hair (24 Colors). For less than $10 I can label drivers for the rest of my natural life.

It did not disappoint.

Results

Now I can clearly see which driver to select and get the right one every time even without my glasses.
(The unlabeled driver in my RaceDayQuads kit is a Phillips screwdriver)

It’s not what I made, it’s how I made it — Scripting Inkscape

Over the last couple of years, my wife and I have accumulated several small ceramic tiles (and other similarly shaped items). Rather than lying them flat, it’s good to prop them up on a stand. I made a stand by laser cutting out pieces from 1/8″ wood scraps, glueing the pieces together and painting the result. Here’s a finished stand propping up a tile mounted to a cork backing.

The stand is a rectangular piece that’s the same size as (or a little smaller than) the tile. I call this the PLATE. I decided to tilt the plate 15 degrees off vertical. Glued to the back of the plate are two triangles that hold the plate at 15 deg. These are the LEGS. The legs have to be sized large enough that the plate can’t fall over backwards — because, center of gravity — but not so large as to extend above the plate.

The dimensions of the legs are easy to calculate with a little trigonometry (thank you,
Fr. Edwin Karlovich, C.R.).

The hypotenuse, h, of the triangle is the same length as the plate’s height. A 15 deg. angle from vertical means a 75 deg. angle from horizontal. The bottom back of the leg is 90 deg. We know 3 facts about the triangle – 1 length and 2 angles. That’s enough for us to determine the lengths of the remaining two sides. The width (or “base”) of the triangle is

and the height is

Simple calculations for just about any calculator.

The cut diagram shows how simple the geometry is – a rectangle for the plate, and a second rectangle split on a bias for the two legs. This is very easy to create in Inkscape.

But, I had several to do and more to do in the future. So I set about automating the process of drawing the parts.

Inkscape is extensible, and Scott Pakin wrote an extension called Simple Inkscape Scripting that allows you to write Python scripts to automate the drawing.

https://inkscape.org/~pakin/★simple-inkscape-scripting

After installing the extension, here’s the script I wrote to draw a cut-sheet for my stands. It’s a .py file created with a simple text editor. It exists as a file outside of Inkscape.

# Set the dimensions of the plate
plateWidth = 119
plateHeight = 97

# Do some trig to calculate the leg dimensions
angleDegrees = 75.0
angleRadians = angleDegrees * 0.01745329
legBase = cos(angleRadians) * plateHeight
legHeight = sin(angleRadians) * plateHeight
strokeWidth = 0.15


# Draw a rectangle
legs = rect((0, 0), (legBase, legHeight), stroke='#ff0000', stroke_width=strokeWidth)

# Draw a line from one corner of the rectangle to the opposite
diagonal = path(['m', 0,0, legBase, legHeight], stroke='#ff0000',
stroke_width=strokeWidth,
stroke_linecap='butt', stroke_linejoin='miter', stroke_opacity=1)

# Group the rectangle and line into a single object and move it off to the side
group([legs, diagonal], 'transform=translate(%.5f,%.5f)' % (plateWidth+10,0))

# Draw the plate rectangle
backplate = rect((0, 0), (plateWidth, plateHeight),
stroke='#ff0000', stroke_width=strokeWidth)

To use this script, edit the plate width and height and save the file. Inside of Inkscape, click Extensions, Render, Simple Inkscape Scripting… to open the dialog.

Browse to (or type) the name of the Python script file and click the APPLY button.

Now just send it off to the laser.

Rinse and repeat for any other stands with other dimensions.

The are several dozens of functions available within the scripting syntax to draw very complex images.

CTA Train Tracker – Part 2

“Sweet dreams and flying machines in pieces on the ground”

Taylor, J. (1970). Fire and Rain [Lyrics]

In my previous post related to this project (CTA Realtime Train Tracker), I explored the Chicago Transit Authority’s REST API from a software standpoint — what data is available, how do I access it, how can I display it. In this post, I present my efforts to build a illuminated map of the CTA rail system showing the actual locations of trains in real time.

Design Goals

Small Overall Dimensions and Light Weight17×22 inches wide at most
Geographically Suggestive👎🏻
👍🏻
Stop LabelsEach stop should be labeled with its official station name.
Bi-directional Traffic FlowThere should be something that shows the direction each train is moving.
Color CodedThe CTA Train Lines are called Red, Blue, Green, Orange, Pink, Purple, Yellow and Brown. The lights on a Line should reflect the Line’s name.
Minimal Light BleedThe glow from a light should only illuminate one stop.
Visible When Lights OffThe map needs to be printed onto the board so that it’s recognizable even if the lights are turned off.
Lights between StopsThere should be a light between each stop, to indicate that a train has left its previous station, but has not yet arrived at its next station stop.
Inline Loop
👎🏻
Easy Wiring
👎🏻

Construction

While researching this build, I came across a map of the CTA Rail System on the Chicago Tribune web site (pictured above in the Design Goals table). Every stop was labeled, the Loop was not in a separate call-out, and the placement of the Lines bears a strong suggestion of a geographically accurate map. The straight lines with minimal corners lend themselves to the use of NeoPixel strips. NeoPixel strips are easy to wire, easy to program and color coded.

NeoPixel strips come in several sizes. I found a set that included 150 pixels per meter – that’s 6.6mm spacing between them. A single strip that runs from Linden in Wilmette to 95th St in Chicago would require approximately 44 pixels for each named stop plus one pixel for the in-between stops. That translates into a map that’s 22 inches tall.

The strips are 4mm wide, which would also allow for two strips to be placed side-by-side. One strip for the “Southbound” trains and another for the “Northbound” trains. It would be clear which direction a train is moving by noting which strip its LED is on — just like cars on a road.

Multiple strips would be needed — roughly one for each of the eight Lines. Some stops are serviced by multiple Lines, so a given pixel might illuminate as red, brown, purple, etc. depending on which train was passing through. The strips can be cut, repositioned appropriately and rewired.

Programming

With an Adafruit RP2040 Scorpio, 8 LED strips can be driven from a single control board running CircuitPython. The Scorpio would require a Feather add-on to give it Wi-Fi capability.

The REST API has two key pieces of information to place a train — the station ID which the train is traveling towards and whether arrival at that station is imminent (<60 seconds out) or not. So, for each train running, look up the station ID in a table/dictionary. This table will contain the strip and pixel number for that station. If arrival is imminent, light that pixel with the color of the train. If arrival is not imminent, light the pixel one less than the number in the table.

This simple rule assumes the “Southbound” strip joins back up with the “Northbound” strip at the “southern” end forming one continuous strip of consecutively numbered pixels. The rule breaks down at the intersection of two (or more) Lines that share the same tracks. Luckily, the number of exceptions is manageable.

Display

With the electronics decided, the next decision is to address the look of the map. I envision a thin, wooden board that is laser etched with station names and with holes for the LEDs to shine through. The LED strips will be attached to the back of the board. The holes will keep the light focused. A layer of diffusing material could be placed to soften the lights. Colored lines can be painted on the board for each train Line to satisfy the “lights off” goal. An appropriate logo would be etched into the board as well.

Laser etching

The Space’s laser cutter is conveniently set up with Inkscape, which reads SVG files. SVG files are just plain text files formatted in XML. I could use Notepad to type an SVG file by hand and get that file cut on the laser.

My first experiment was to write a program to create an SVG to cut labels and holes on scrap matte board. I iterated over all the stations and used their lat/lons to place a hole and label it. For the labels, I went with a single-stroke font. This way, I could get a crisp, tiny vector etch of the labels rather than a smudgey raster burn. Inkscape 1.2 has an extension called Hershey Text which creates labels using Hershey Fonts. Hershey fonts are a collection of vector fonts developed c. 1967 by Dr. Allen Vincent Hershey at the Naval Weapons Laboratory, originally designed to be rendered using vectors on early cathode ray tube displays. While the Space’s laser cutter laptop doesn’t have an up-to-date copy of Inkscape, it will accept SVGs containing Hershey Fonts created elsewhere and render them on the laser.

Here is the results of my first test at creating an SVG programmatically.

As you can see, the Loop is hopelessly over etched and the station labels are very close together. Even if I used the largest board the laser’s bed could accommodate, I would not be able to eliminate the overcrowding.

Here’s a sample of Hershey Fonts on real wood. I ran the vector etch next to the raster etch to show the contrasting results of the two methods. I think vector wins hands down. The slight charring on the vector etch can be mediated with blue painter’s tape – although that would require a fair amount of weeding.

I’d convinced myself that I could, in fact, programmatically create an SVG file to draw the map. So I wrote a program that placed the stations of the Red Line in a straight line — spaced exactly to correspond to the distance between LEDs on a strip and I printed that SVG file on the laser.

Equally Spaced Stations

I attached an LED strip to the back of this board and animated it with some real-time train data. I only had one LED strip in inventory, so I illuminated southbound trains in Red and northbound trains in Blue. This Timelapse video shows the result.

The Hard Truth

It was at this point that I took stock of what I’d achieved and what remained to do. I realized that the project was going to be very expensive to complete. LED strips cost a lot and I’d need quite a few of them. Even with all my successes so far, the success of the final project was not assured. I was also disappointed that the partial prototype was a bit boring to watch in real-time. Only in time lapse does it approach being interesting. It just doesn’t seem worth it to proceed.

Alternative Designs

I could relax my design goals, e.g. one strip per Line instead of two and no in-between stops. But I think that will diminish the look of the project too much. Therefore, I’m moth-balling the project.

Still, I would like to see the entire rail network in operation. I looked for an alternative way to display the data. I settled on a small device called the PyPortal. I’d written about the PyPortal in this post, “The Adafruit PyPortal and Modifying Adafruit Libraries“. I ported my code to the PyPortal but rather than light up NeoPixels, I drew a colored dot on the screen to represent the position of each train.

Basic Program Flow

Here’s the guts of the program:

  1. Query the CTA for the latitude/longitude of all trains
  2. Map the lat/lon into screen coordinates
  3. Clear the screen
  4. Turn on the pixels at the screen coordinates
  5. Rinse, repeat

Step 3 caused an annoying flicker when the screen clears during each iteration.

I solved that problem by saving a list of all the pixels that were lit up. During the next iteration, I’d compare the list of prior screen coordinates with the list of new screen coordinates. If a pixel was in the prior list but is not in the current list — I’d turn the pixel off. Then I’d iterate through all the current pixels and turn them on. If the pixel is already on, it stays on — no harm no foul. If the pixel is new, it gets turned on. This is very fast and there is no flicker.

The Entire Network

Here is a time lapse of the PyPortal system running. The video is only 5 seconds, but may loop continuously.

The video is short for two reasons — 1) you get the point from the short video and 2) after just a few iterations, the program crashes. Other tests of my code running on macOS and a Raspberry Pi Zero did not abort. They run perfectly fine all through the night. After several weeks of very frustrating debugging, I’ve concluded there’s too much data for the PyPortal hardware and/or the CircuitPython libraries to process and the libraries do a poor job of handling the exceptions.

Stubborn Determination

I abandoned the PyPortal and made another attempt by attaching a tiny, TFT display onto a Raspberry Pi Zero.

(yeah, it really is that small.)

I rewrote the code (again) to draw onto that display. As I’d hoped, that program ran for many, many iterations without failing. The fact that the program runs continuously allowed me to experiment with tweaking the display.

One change I made was to the way I refreshed the screen at each iteration. Rather than clearing the pixels and drawing the updated positions, I just left all the prior iterations’ pixels turned on. After only a few iterations, the rail lines began to appear as thin lines. The end result is a glowy map of the entire CTA rail network — geographically accurate. (This is similar to what I did in Part 1 when I tracked individual trains and built a Google Earth map from the data.)

The downside of this is that once the lines are fully drawn, you can no longer dicern the current locations of the trains.

To fix that, I decided rather than turn pixels off or leave them on, I would instead turn the prior pixels dark grey. This made it easier to see both the current train locations as well as the line it was running on.

Taking it one step further, rather than grey for all lines, I set a train’s old pixel to a dimmed version of that color. Now all the trains (bright dots) are running on color coded lines (dim dots).

Finally, the TFT has two built-in buttons. I programmed them to toggle between two different versions of the data — one is the real-time train positions map, and the other is a bar chart showing how many trains are running on each line.

(Interesting aside: When photographed straight on, the camera sees the TFT colors as totally washed out and indistinguishable from one another. However, at a steep angle, the colors are saturated and vivid.)

End of the Line

While this TFT version of the map misses many of the design goals, I am at least able to see the entire CTA rail network in real time.

Many thanks to the members of Workshop 88 who shared their thoughts with me throughout the project. Without their involvement, this project might never have gotten as far as it did.

Final project

And with that… I’m done with this project (until I can find cheap, tightly spaced, LED strips).