Complete Projects

Add an SUnit test for the *blinker* oscillator (three cells in a row flip between horizontal and vertical).

42.1 Word-frequency command-line tool (headless Pharo)

"wordfreq.st — run: ./pharo Pharo.image st --quit wordfreq.st"
| text words counts out |
text := 'input.txt' asFileReference contents asLowercase.
words := (text substrings: ' .,;:!?()"''-' , String crlf , String tab)
    reject: [ :w | w size < 3 ].
counts := words asBag.
out := Stdio stdout.
(counts sortedCounts first: (10 min: counts asSet size)) do: [ :assoc |
    out
        nextPutAll: (assoc value padRightTo: 15);
        nextPutAll: assoc key printString;
        lf ].
out flush.

42.2 Conway's Game of Life (model + Morphic view)

Add an SUnit test for the *blinker* oscillator (three cells in a row flip between horizontal and vertical).

Object << #LifeGrid
    slots: { #cells . #dimension };
    package: 'Life'

LifeGrid class >> dimension: n
    ^ self new setDimension: n

LifeGrid >> setDimension: n
    dimension := n.
    cells := Array new: n * n withAll: false

LifeGrid >> dimension
    ^ dimension

LifeGrid >> indexOf: x at: y
    ^ ((y - 1) \\ dimension) * dimension + ((x - 1) \\ dimension) + 1     "wraps around"

LifeGrid >> at: x at: y
    ^ cells at: (self indexOf: x at: y)

LifeGrid >> at: x at: y put: aBoolean
    cells at: (self indexOf: x at: y) put: aBoolean

LifeGrid >> neighboursOf: x at: y
    | n |
    n := 0.
    -1 to: 1 do: [ :dx | -1 to: 1 do: [ :dy |
        ((dx = 0 and: [ dy = 0 ]) not and: [ self at: x + dx at: y + dy ])
            ifTrue: [ n := n + 1 ] ] ].
    ^ n

LifeGrid >> step
    | next |
    next := Array new: cells size.
    1 to: dimension do: [ :x | 1 to: dimension do: [ :y |
        | n |
        n := self neighboursOf: x at: y.
        next at: (self indexOf: x at: y) put: (n = 3 or: [ n = 2 and: [ self at: x at: y ] ]) ] ].
    cells := next

LifeGrid >> randomize
    1 to: cells size do: [ :i | cells at: i put: 4 atRandom = 1 ]

Morph << #LifeMorph
    slots: { #grid };
…
(LifeMorph new grid: ((LifeGrid dimension: 60) randomize; yourself)) openInWorld.

42.3 URL shortener (Teapot + in-memory store)

Extensions: persist with Voyage, count visits with Announcements, add a Spec2 admin UI, test with ZnClient.

Object << #Shortener
    slots: { #urls . #server };
    package: 'Shortener'

Shortener >> initialize
    super initialize.
    urls := Dictionary new

Shortener >> shorten: aUrl
    | code |
    [ code := String new: 6 streamContents: [ :s |
            6 timesRepeat: [ s nextPut: 'abcdefghijkmnpqrstuvwxyz23456789' atRandom ] ].
      urls includesKey: code ] whileTrue.
    urls at: code put: aUrl.
    ^ code

Shortener >> start
    server := (Teapot configure: { #port -> 8080 })
        POST: '/shorten' -> [ :req |
            { #code -> (self shorten: (req at: #url)) } asDictionary ]; output: #json;
        GET: '/<code>' -> [ :req |
            TeaResponse redirect location: (urls at: (req at: #code)) ];
        exception: KeyNotFound -> (TeaResponse notFound body: 'Unknown code');
        start

Shortener >> stop
    server stop
curl -X POST -d "url=https://pharo.org" http://localhost:8080/shorten     # {"code":"k3x9ab"}
curl -i http://localhost:8080/k3x9ab                                       # 303 → https://pharo.org

42.4 Other complete project ideas

Bank system (Chapters 13–33 combined): accounts, transactions, custom exceptions, announcements, SUnit tests, a Spec2 UI, and a Teapot JSON API — all in one Git repository with a baseline.

Todo application: model with announcements, Spec2 master–detail UI, STON file persistence, Iceberg + smalltalkCI.

Code-metrics dashboard: walk packages with reflection (classes, methods, lines of code, senders), display with Roassal, export to CSV.