"A game where one attempts to write the shortest program to accomplish some goal." — inspired by Perl Golf, here played in any language.
Each round we pick a small task. You solve it in as few bytes as possible. At the deadline every solution is run against a hidden test set and a results page crowns the overall winner and the winner in each language.
It's about short, not fast or clever. The only thing that scores is the byte count of your program. A solution can be hilariously slow, brute-force, or unreadable — if it's correct and shorter, it wins. Do not optimize for performance or polish; optimize for size.
- Correctness is pass/fail. A solution must pass all test cases to be ranked. There are no points for partial output. This is olympiad-style: only a basic example is published with the task — the full set of (hidden) tests is revealed only when the round is finalized, so solve the general task, not just the example.
- Ranking is by code size in bytes, smallest wins. The shebang interpreter
line is free (declaring
#!/usr/bin/env -S perlcosts nothing); your program body — including any interpreter flags — is what counts. - Whitespace counts too — spaces, tabs, and newlines are bytes like any
other, so you can't hide logic in "free" whitespace. For a deliberately absurd
demonstration, see
claude-01.pl: its real program lives entirely in the blank-looking lines after__DATA__, where each line's length is one character code. The single line above__DATA__reads those lines and rebuilds the source in one expression —pack "C*", map {length} <DATA>— thenevals it. It scores 1854 bytes and lands dead last — yet strip the spaces and the real logic is only 61 bytes. That ~30× gap is the whole point: if spaces were free, this would masquerade as a tiny solution. - Any language goes, as long as it's available in the competition Docker
image (run
make versionsto see what's installed: Perl, Python, Raku, Scala/Java, Node.js, sed, bash, …). There's an overall winner and a winner per language. - There is a timeout. Slow is fine, but not infinite: each test run is sent
SIGHUPafter 10s andSIGKILL2s later. A solution that exceeds it is recorded astimeoutand does not rank. That's the only nod to performance — everything within the budget is fair game. - LLMs are allowed, but please don't. Using an LLM isn't prohibited, but
this is a fun competition — you'll get far more out of it (and so will
everyone reading the solutions) if you golf it yourself. Even better, reach for
a language you don't normally use:
perl,raku, andsedare unreasonably good at golf, so picking one up is half the fun.
Each round lives in competition/<NNN>-<name>/ (e.g. competition/000-hello/):
competition/000-hello/
├── README.md # the announcement: task description + one basic example
├── solutions/ # your solutions go here
└── tests/
├── example.in # the single visible test case
└── example.ex
When a round is announced (by email), the folder ships with only the task
README.md and the example test pair. The remaining test cases are kept
out of the repo until the deadline.
A test case is a pair of files sharing a base name:
<name>.in— the bytes fed to your program on stdin.<name>.ex— the exact stdout your program is expected to produce.
Evaluation is literally cat <name>.in | your-solution | diff - <name>.ex. If
diff reports no difference (exit 0), the case passes. The published example is
example.in / example.ex; at finalization more hidden pairs are added.
- The organizer merges the competition PRs into
master. - The full, hidden test cases are merged into each task's
tests/. - The merge commit is tagged with the task folder name (e.g.
000-hello). - That tag triggers a GitHub Action that evaluates every solution against the full test set and publishes a release page with the leaderboards.
Forgot to merge in time? The organizer can move the tag and the release regenerates — but don't count on it.
💡 Submit with a pull request. Don't merge to
masteryourself — open a PR and the organizer will merge the entries at the deadline. Title it[<tag>] <your name>(e.g.[000-hello] alex) so the organizer can tell at a glance which PRs are competition entries. And on your honour: please don't peek at other people's open PRs until the round is finalized — figuring it out yourself is the whole point, and comparing notes afterwards is half the fun.
-
Clone the repo:
git clone https://github.com/tubular/code-golf-evaluator.git cd code-golf-evaluator -
Branch — do your work on your own branch:
git checkout -b golf/<your-name>
-
Read the task in
competition/<NNN>-<name>/README.md. -
Write your solution in that round's
solutions/folder, named<your-name>-<NN>.<ext>(bumpNNfor each new attempt), starting with a shebang so the evaluator knows how to run it:competition/000-hello/solutions/alex-01.pl#!/usr/bin/env -S perl -p s/^/hello, /
The interpreter line is free; only the body below it counts toward your size.
-
Build the image (first time, or after the base changes):
make # builds the app image (needs the base image; see `make base`) make versions # list the languages/versions available to you
-
Test against the example. The simplest way is to run the evaluator over the whole competition tree inside the container — it has every interpreter:
make evaluate # runs `code-golf evaluate` over competition/ in DockerYour row shows
successwhen your output matchesexample.ex. To iterate quickly on a single file you can also run the pipeline by hand:cat competition/000-hello/tests/example.in \ | ./competition/000-hello/solutions/alex-01.pl \ | diff - competition/000-hello/tests/example.ex && echo PASS
-
Shrink it. Trim bytes, try another language, repeat. Remember: a correct but slow/ugly solution that's shorter beats a fast, pretty, longer one.
-
Open a pull request titled
[<tag>] <your name>(e.g.[000-hello] alex). No need to merge it yourself — the organizer merges the entries at the deadline. And please don't peek at others' open PRs until then.
Want to extend the evaluator — add a language, a new output format, or another pipeline stage? Here's the lay of the land. The code is Raku; if you've never read it, the quick primer below is enough to follow along.
- Sigils tag variables by shape:
$xscalar,@xarray,%xhash,&xa callable.my $x = …declares one. class Foo does Bar { method baz($arg) { … } }— a class composing a roleBar(a role is an interface/mixin) and defining a method.-> $x { … }is a lambda.:foo/:$barare named arguments (:$baris shorthand forbar => $bar).- The async bits you'll see: a
Supplyis an asynchronous stream of values.supply { … }builds one,emit $vpushes a value into it, andwhenever $stream -> $v { … }reacts to each value as it arrives. That's the whole reactive vocabulary used here.
bin/code-golf → Tubular::CodeGolf::CLI (MAIN) → Tubular::CodeGolf::Runner.run
bin/code-golf just loads CLI.rakumod, whose MAIN multi dispatches the
subcommand (evaluate, release <tag>, compile) and hands off to the
Runner. In Docker everything goes through docker/entrypoint.sh code-golf ….
Evaluation is a chain of small stages. Every stage is a Unit — a class
that implements one method:
role Unit { method transform(Supply $in --> Supply) { … } }It takes a stream in and returns a transformed stream out. Runner.run
(lib/Tubular/CodeGolf/Runner.rakumod) just wires the stages together with
Chain and starts them:
Mono(root path) # Producer: emits the starting value
→ DirList # list competition task folders
→ Filter # (release only) keep just the tagged task
→ CrossProduct # pair every test with every solution
→ EntityTransformer # turn paths into Solution objects
→ SolutionExecutor # run each solution, emit a TestResult
→ ResultToCSV / ResultToMD # the sink: format the results
Chain (Runner/Flow/Chain.rakumod) feeds each stage's output Supply into the
next. The first stage is a Producer (Mono) that seeds the stream; the
last is a sink that formats output. Adding a stage = write a Unit and slot
it into that list.
The whole thing is lazy and concurrent: nothing runs until Chain.start taps
the final Supply, then values flow through as they're produced. Mono and
DirList emit paths, and downstream stages process them as they arrive rather
than waiting for the full list.
The interesting async work is in SolutionExecutor: for each solution it spawns
the pipeline cat <input> | <solution> | diff - <expected> with Proc::Async,
wrapped in Utils::PipeTimeout, which races the processes against a timer
(SIGHUP at 10s, SIGKILL 2s later) and emits a TestResult with status
success / wrong / timeout / error.
There is nothing language-specific in the evaluator — it runs each solution file directly via its shebang. So "adding a language" usually means just making its interpreter available:
- Install the toolchain in
docker/Dockerfile.base(where Perl, Python, Raku, Scala/Java, Node.js, … are set up), thenmake baseto rebuild. - Make sure the shebang form parses: language detection lives in the
Shebanggrammar inlib/Tubular/CodeGolf/Entity/Solution.rakumod(the reported language is the interpreter's basename). Standard#!/usr/bin/env -S <interp> <flags>shebangs work out of the box. - Add a solution in that language under some
competition/*/solutions/and runmake evaluateto confirm it executes and ranks.
- Create a class under
lib/Tubular/CodeGolf/Runner/thatdoes Unitand implementstransform. (A formatter likeResultToMDis a good template; a filter likeFilteris the minimal one.) - Register it in the
providessection ofMETA6.json— otherwiseprove6 -I.and CI can't resolve it. - Slot it into the pipeline list in
Runner.run. - Add a test under
t/and run the suite (prove6 -Ilib t, ormake testfor a full container run). Always run the tests after a Raku change.
cono q@cono.org.ua
Copyright 2022-2026 Chartbeat, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0. See the LICENSE
file for the full text.