Metadata-Version: 2.5
Name: blindgrid
Version: 0.3.0
Summary: Generate lottery grids from cryptographic randomness and plan a month within a hard budget cap.
Project-URL: Homepage, https://github.com/adrnbttr/blindgrid
Project-URL: Repository, https://github.com/adrnbttr/blindgrid
Project-URL: Issues, https://github.com/adrnbttr/blindgrid/issues
Author-email: Adrien Bouttier <adrnbttr@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Adrien Bouttier
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: budget,cli,csprng,lottery,randomness
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: questionary>=2.0
Requires-Dist: rich>=13.7
Requires-Dist: tomli-w>=1.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

<div align="center">

<img src="https://raw.githubusercontent.com/adrnbttr/blindgrid/main/docs/logo.svg" alt="" width="88" height="88">

<h1>blindgrid</h1>

<p>
  <b>Lottery grids from cryptographic randomness,<br>
  inside a budget you cannot exceed.</b>
</p>

<p>
  <a href="https://github.com/adrnbttr/blindgrid/actions/workflows/ci.yml"><img src="https://github.com/adrnbttr/blindgrid/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
  <a href="https://pypi.org/project/blindgrid/"><img src="https://img.shields.io/pypi/v/blindgrid?color=blue" alt="On PyPI"></a>
  <a href="https://pypi.org/project/blindgrid/"><img src="https://img.shields.io/pypi/pyversions/blindgrid" alt="Python versions"></a>
  <a href="https://github.com/adrnbttr/blindgrid/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license"></a>
  <img src="https://img.shields.io/badge/predictions-none-lightgrey" alt="Predictions: none">
</p>

<p>
  <a href="#install">Install</a> ·
  <a href="#what-this-is-not">What it refuses to do</a> ·
  <a href="#playing-as-a-household">Households</a> ·
  <a href="#on-a-tablet-or-a-phone">Tablets</a> ·
  <a href="#language">Language</a> ·
  <a href="#adding-a-lottery-from-any-country">Add your own lottery</a> ·
  <a href="#starting-over">Starting over</a> ·
  <a href="#responsible-gambling">Responsible gambling</a>
</p>

<img src="https://raw.githubusercontent.com/adrnbttr/blindgrid/main/docs/demo.gif" alt="blindgrid prompting for a budget, then printing a month of draws" width="880">

</div>

## What this is

A small CLI that answers two questions once a month:

- **Which draws should I play?** Given a budget and a set of lotteries, it
  spends the money down your order of preference, picks real calendar dates at
  random, and stops. No pattern, no habit, no "I always play on Saturdays".
- **Which numbers?** Drawn from `secrets.SystemRandom`, then filtered against a
  handful of anti-pattern rules so the grid does not look like something a
  human would have chosen.

The plan is printed as a table and exported to a Markdown file.

## What this is not

**It does not predict anything, and it never will.** Lottery draws are
independent events. The numbers drawn last week carry no information about next
week's draw; a number that has not come up in two years is not "due". This is
not an opinion about lotteries, it is what independence means.

So the following are permanently out of scope, and pull requests adding them
will be declined:

| Not here | Why |
| --- | --- |
| Historical draw data, frequency statistics, hot/cold numbers | Past draws say nothing about future ones. Displaying them implies otherwise. |
| A `--seed` flag or any deterministic mode | Reproducible numbers reintroduce exactly the structure this tool removes. |
| A history of past months | Months of stored grids invite comparing them against results, and comparing invites pattern-hunting. The current month is kept so you can find it again — one file, replaced, never accumulated. |
| Any attempt to spend the budget exactly | Unspent money is a good outcome. A tool that consumed the envelope would encourage spending — it fills the budget you set, and never rounds up to reach it. |

What is left, then? Two honest things: **randomness without human bias**, and a
**budget ceiling that does not move**.

### Why filter the numbers at all, if every combination is equally likely?

Because the *payout* is not equally likely. Every combination has the same
probability of being drawn, but combinations humans favour — birthdates below
32, sequences, neat patterns — are picked by thousands of other players at the
same time. When such a combination wins, the jackpot is split. Filtering does
not improve your odds of winning; it improves what you would receive if you
did.

The rule set stays deliberately small for the same reason. Every additional
rule shrinks the sample space, and a heavily filtered "random" pick is just a
different kind of predictable.

## Install

Runs on Linux, macOS, Windows, and iOS through a-Shell.

```bash
pip install blindgrid        # or: uv tool install blindgrid
```

That is the whole of it if you already have Python 3.11 or newer —
[the package is on PyPI](https://pypi.org/project/blindgrid/). The scripts
below add a little more: they pick an install method for you, write a starter
configuration, and tell you what to put on your `PATH`.

**macOS and Linux**

```bash
curl -fsSL https://raw.githubusercontent.com/adrnbttr/blindgrid/main/install.sh | bash
```

**Windows** (PowerShell)

```powershell
irm https://raw.githubusercontent.com/adrnbttr/blindgrid/main/install.ps1 | iex
```

Either one installs into your own user profile, writes a starter config, and
tells you what to add to your `PATH` if anything is missing. Neither asks for
`sudo` or administrator rights, and neither writes outside your home
directory. Prefer to read the script first? Download it and open it — both are
short, and both are linted in CI (shellcheck, PSScriptAnalyzer).

Python 3.11 or newer is required, except with uv, which brings its own.
**3.11 is a floor, not a default**: it is the newest Python a-Shell ships, and
raising it would drop iPad support — a test says so.
Uninstalling is `install.sh --uninstall` / `install.ps1 -Uninstall`, and your
config survives it.

**iPad and iPhone**, in [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
(free, App Store):

```bash
pip install blindgrid
```

Then `python3 -m blindgrid generate`. Every dependency is pure Python, which is
what makes this work on a device with no compiler. If pip is not on your path
there, `curl -sL https://raw.githubusercontent.com/adrnbttr/blindgrid/main/install.py | python3`
does the same and sets up a starter config — see
[On a tablet or a phone](#on-a-tablet-or-a-phone).

**[Full installation guide](https://github.com/adrnbttr/blindgrid/blob/main/docs/installation.md)** — per-platform details,
other install methods, file locations, troubleshooting, updating, uninstalling.

## Use

```bash
blindgrid config show      # where your config lives and what is in it
blindgrid generate         # plan the current month
```

`generate` asks for two things — the budget for this month, and which lotteries
to include — then prints the plan and writes `plan.md`. It plans the current
month by default; `--month` is only there for planning ahead.

The budget prompt comes pre-filled with your ceiling, so **enter is enough** if
you spend the same every month. The figure is still shown and agreed to rather
than assumed, because a budget nobody looks at is a budget nobody decided.

### A month is drawn once

Run `generate` again and you get the same plan back, not a new one:

```
$ blindgrid generate
Plan already drawn on 2026-09-01. Showing it again — pass --force to draw a new one.
```

This is partly convenience — you can come back to it while you fill your grids
— but mostly it is the point. A tool that redrew on every run would let you
reroll until the numbers looked right, and picking the draw you like best is
exactly the bias the filters exist to remove.

`--force` is there for the genuine mistake, a mistyped budget or the wrong
lotteries. It says what it is replacing before it does it. To simply forget the
month instead, without answering the prompts again, `blindgrid plan clear`.

The plan lives in one file in your state directory (see
[file locations](https://github.com/adrnbttr/blindgrid/blob/main/docs/installation.md#where-files-live)), replaced when the
month turns. It is a self-contained snapshot, so editing your config
afterwards never changes a plan you already hold. Draws whose date has passed
are struck through when the plan is shown again, so what is left to play is
obvious.

## On a tablet or a phone

The plan is measured against your terminal and laid out to fit it, rather than
assuming a width.

**The table is deliberately narrow.** The month is already in the title, so
each row carries the day alone instead of repeating `2026-09-`, and the weekday
is abbreviated the way each language writes it — German uses `Mi`, not a
truncated `Mit`. That is twelve columns saved, which is what puts a two-player
plan inside the ~90 columns an **iPad gives you in portrait**. Rotating the
device no longer changes the layout.

**Below that it sheds what it can spare, one thing at a time**, rather than
giving up on the table at the first squeeze:

| | What it gives up | Two players | One player |
| --- | --- | --- | --- |
| Full table | — | 85+ | 76+ |
| Leaner | The cost — fixed per lottery, totalled below | 74+ | 65+ |
| Leaner still | …and the weekday, which follows from the date | 68+ | 59+ |
| List | The table itself | below that | below that |

Those widths are the point at which each layout is guaranteed; a column or two
either side it depends on the draw, since a row of single-digit numbers is
narrower than one full of two-digit ones. Nothing is measured in advance — the
layout is chosen against your actual terminal, every run.

What never goes, at any width, is the answer to *what do I play, and when*:
the date, the lottery, and the numbers.

The summaries follow the same rule — squeezed, they become plain lines rather
than a table with `Lottery` cut down to `Lotte…`.

**Narrower still**, on a phone or in a split view, the table would shred the
numbers across lines, so it becomes a list:

```
September 2026

 8 Tue  EuroMillions  Adrien  2.50 EUR
    10 11 30 34 47  ·  stars  8 12

 9 Wed  Loto  Adrien  2.20 EUR
     7 20 22 34 35  ·  lucky  2
```

The numbers are the one thing you copy onto a paper slip, so they are never
wrapped and nothing is ever cut with an ellipsis, at any width. This stays
readable down to about 40 columns.

`--compact` and `--table` override the choice if you would rather decide
yourself. The Markdown export is unaffected: it keeps full dates and full
weekday names, since it is read away from the terminal with no title above it
to supply the month.

## Playing as a household

Two people, one month, one command. Everyone keeps their own ceiling and their
own games:

```bash
blindgrid player add     # name, ceiling, which lotteries, in what order
blindgrid generate       # asks each person for their budget, then plans
```

`player add` asks them to rank their games, first to last — no number to invent,
and stopping early is how a game is left out. It takes options too, which skips
the questions entirely:

```bash
blindgrid player add --name Marie --ceiling 25 --play Loto=2 --play EuroDreams=1
```

```
╭──────┬─────┬────────┬─────────┬─────────────────────────────┬──────────╮
│ Date │ Day │ Player │ Lottery │ Numbers                     │     Cost │
├──────┼─────┼────────┼─────────┼─────────────────────────────┼──────────┤
│    5 │ Sat │ Adrien │ Loto    │  1 14 30 38 43  ·  lucky  6 │ 2.20 EUR │
│    5 │ Sat │ Marie  │ Loto    │  1  6 14 41 45  ·  lucky  2 │ 2.20 EUR │
│    7 │ Mon │ Adrien │ Loto    │  1  6 33 35 43  ·  lucky  8 │ 2.20 EUR │
│   12 │ Sat │ Marie  │ Loto    │ 14 21 22 32 40  ·  lucky  1 │ 2.20 EUR │
╰──────┴─────┴────────┴─────────┴─────────────────────────────┴──────────╯
```

Both play the 5th, with their own numbers. Each budget is spent on its own, so
neither of them loses a grid to the other being there.

Three things hold:

- **Nobody spends against anyone else's ceiling.** Each person has their own
  `max_monthly_budget`, and one player's budget never changes another's. There
  is no household cap, on purpose: if two people together want to spend more
  than they should, that is a conversation, not something software gets to
  arbitrate.
- **Everyone plays only their own games, in their own order.** Priorities are
  per person; a priority of zero, or simply leaving a lottery out, means they
  never play it. The same game can come first for one person and last for
  another.
- **Everyone gets the grids they paid for.** Budgets are separate and the
  calendar is shared, so two people playing the same game play the same draws,
  with their own numbers.

### On playing the same draw

Two people on one draw **does not change anyone's odds**, and the tool will
never claim it does. Two grids are two independent chances whether they sit on
the same draw or on two different ones, and the probability that at least one
of them wins is identical either way.

Earlier versions spread the household across distinct dates before sharing any.
That bought exposure to more separate jackpots, but it cost people grids they
had budgeted for: with 30.00 each on a 2.20 game, whoever came second could not
use their whole envelope. Grids someone paid for are worth more than a
preference that does not move the odds, so the spreading is gone.

Within one person's month, though, a draw is played once: if your budget covers
more grids than your games have draws left, the difference stays unspent and the
plan says so. Two grids on one draw would be two independent chances — the odds
do not forbid it — but a second game is a better home for the money.

```toml
[[player]]
name = "Adrien"
max_monthly_budget = 40.00
  [player.priority]
  EuroMillions = 3.0
  Loto = 2.0
  EuroDreams = 1.0

[[player]]
name = "Marie"
max_monthly_budget = 25.00
  [player.priority]
  Loto = 2.0
  EuroDreams = 1.0
```

Declare nobody and blindgrid stays in single-player mode, exactly as before.

```
Usage: blindgrid [OPTIONS] COMMAND [ARGS]...

  generate    Build this month's plan: how much to spend, which draws, which numbers.
  config      Inspect and edit the configuration.
  lottery     Manage lottery definitions.
  player      Manage the people who play.
  plan        Work with the plan already drawn.
  reset       Delete the configuration and the stored plan, to start again from nothing.
  version     Print the installed version.
```

Useful options on `generate`:

| Option | Effect |
| --- | --- |
| `-b, --budget 30` | Skip the prompt and use this amount. With players: `-b "Adrien=30"`, repeatable. Leave the prompt and press enter to reconduct the ceiling. |
| `-l, --lottery Loto` | Include a specific lottery. Repeatable. Single-player mode only. |
| `-p, --player Marie` | Limit the plan to these people. Repeatable. |
| `-m, --month 2026-09` | Plan a month other than the current one. |
| `--force` | Draw a new plan even though this month already has one. |
| `--no-export` | Print the plan without writing `plan.md`. |
| `--lang fr` | Interface language for this run. See [Language](#language). |
| `--compact` / `--table` | Force the narrow layout, or the table. Default: whichever fits. |
| `-c, --config path` | Use a specific configuration file. |

Every prompt has an option that skips it, so the whole tool works without an
interactive terminal — in a script, over SSH, or in a shell that cannot draw
prompts.

## Language

The interface speaks **English, French, Spanish and German**, and follows your
system locale by default — so there is usually nothing to set at all.

| | |
| --- | --- |
| `blindgrid generate --lang fr` | Just this run. Works before the command too. |
| `blindgrid config edit` | Pick one and keep it. |
| `export BLINDGRID_LANG=es` | For a shell session. |
| `language = "de"` in `config.toml` | Written by `config edit`, or by hand. |

Order of precedence: `--lang`, then `BLINDGRID_LANG`, then `language` in the
config file, then your system locale, then English.

The whole interface moves, including month and weekday names:

```
$ blindgrid generate --lang fr

              Tirages à jouer — septembre 2026
╭──────┬──────┬──────────────┬────────────────────────────────┬──────────╮
│ Date │ Jour │ Loterie      │ Numéros                        │     Coût │
├──────┼──────┼──────────────┼────────────────────────────────┼──────────┤
│    4 │ ven  │ EuroMillions │  5 19 28 32 50  ·  stars  1  2 │ 2.50 EUR │
╰──────┴──────┴──────────────┴────────────────────────────────┴──────────╯

  Priorité · Grilles · Engagé · Non dépensé
```

Terms follow each country's own vocabulary rather than a literal translation:
a played grid is an *apuesta* in Spanish and a *Tipp* in German, which is what
the operators there call it.

Only the interface is translated. Lottery labels, pool names and player names
are printed exactly as you wrote them, and the weekday keys in the config file
stay English (`monday`, `tuesday`, …) — those are a file format, not text for
reading.

Adding a language is one file in `src/blindgrid/i18n/` plus one line in its
`__init__.py`. Tests check that every catalogue carries exactly the same keys
and placeholders as the English one, so a half-finished translation fails the
build rather than leaking English sentences into someone's session.

## Configuration

Two files:

- `config.example.toml` — versioned, three French games as an illustration.
- `config.toml` — yours, gitignored, holding your own ceiling.

The ceiling is the point of the file:

```toml
max_monthly_budget = 40.00
```

`generate` refuses any budget above it. There is no override flag. A cap you
can raise in the moment while looking at a jackpot headline is not a cap.

### Adding a lottery from any country

Nothing in the code knows about France, or about any specific game. A lottery
is a price, a set of draw days, and one or more pools of numbers. That is the
whole model, and it covers every draw-style lottery I am aware of.

Here is the US **Powerball** — five numbers from 69, plus one from 26, drawn on
Mondays, Wednesdays and Saturdays at $2 a play:

```toml
[[lottery]]
label = "Powerball"
currency = "USD"
price_per_grid = 2.00
draw_days = ["monday", "wednesday", "saturday"]
priority = 1.0

  [[lottery.pool]]
  name = "numbers"
  count = 5
  max = 69

  [[lottery.pool]]
  name = "powerball"
  count = 1
  max = 26
```

Drop that into `config.toml` and it works. Or run `blindgrid lottery add` and
answer the prompts.

The filters adapt on their own: the 1-from-26 Powerball pool is too small for a
parity or spread rule to mean anything, so those rules switch themselves off
for it rather than searching forever for a combination that cannot exist.

### Priorities

Priority is an **order**, not a share.

```toml
priority = 3.0   # EuroMillions — fed first
priority = 2.0   # Loto — next, with whatever is left
priority = 1.0   # EuroDreams — last
priority = 0.0   # disabled, without deleting the definition
```

The highest priority is served first and plays **every draw it can afford**.
What remains of the budget then goes to the next game, and so on down the list.
With €35 and the priorities above, in a September that holds 9 EuroMillions
draws and 13 Loto draws:

| | | |
| --- | --- | --- |
| EuroMillions | 9 grids × €2.50 | €22.50, all of its draws |
| Loto | 5 grids × €2.20 | €11.00, out of the €12.50 left |
| EuroDreams | — | nothing was left for a €2.50 grid |
| | **Committed** | **€33.50 of €35.00** |

Money is never allocated to a game that has no draw left to spend it on: the
calendar is measured before the budget is split, so a plan can never contain a
grid that was paid for and has nowhere to be played.

**One grid per draw, and no more.** A draw is played once, so a game stops at
its own number of remaining draws and the rest goes to the next game. If every
game you play is covered and money is still left, it stays unspent and the plan
says the month is full — what you want then is another game in your list, not a
second grid on a date you already hold.

**Equal priorities are rarely what you mean.** Ties keep the order of the file,
so `1.0` on everything feeds the first game in the file until its draws run out.
Give them distinct numbers and the order is yours rather than the file's.

`player add` never asks for these numbers. It asks which game comes first, then
what comes after it, and writes the countdown for you — the ranking is the
question, the numbers are just how the file stores it:

```
? Which game comes first? (arrow keys, then enter)
 » Loto — 2.20 EUR per grid
   EuroMillions — 2.50 EUR per grid
   EuroDreams — 2.50 EUR per grid

? And after that? (number 2)
 » EuroMillions — 2.50 EUR per grid
   EuroDreams — 2.50 EUR per grid
   — that is the lot, nothing else is played —
```

What cannot buy a grid at the end **stays unspent**, and the totals say so. It
is never rounded up into one more grid.

> Earlier versions split the budget proportionally: with weights of 1.0 / 1.0 /
> 0.4, a €40 budget became envelopes of €16.66 / €16.66 / €6.66. The key `weight`
> is still read as a priority, so an existing config keeps working — and keeps
> the same ranking, since a higher number has always meant "more of this one".
> Writing the file back turns it into `priority`.

## Starting over

Set something up wrong, or want next month to start from a clean slate? Nothing
here is precious — the plan is regenerable and the config is a short file you
can rewrite.

| | What goes | What stays |
| --- | --- | --- |
| `blindgrid plan clear` | This month's drawn plan | Everything in your config |
| `blindgrid player clear` | Every player, back to solo mode | Lotteries, ceiling, filters |
| `blindgrid player remove Marie` | One person | Everyone else |
| `blindgrid config reset` | Your config, replaced by the example | The drawn plan |
| `blindgrid reset` | The config **and** the plan | Nothing — this is all of it |

Each one names what it is about to delete and asks before doing it:

```
$ blindgrid reset
This will delete:
  · /home/adrien/.config/blindgrid/config.toml
      configuration — ceiling 40.00, 3 lottery/ies, 2 player(s)
  · /home/adrien/.local/state/blindgrid/plan.json
      plan for September 2026 — 14 draw(s)

? Delete all of it? (y/N)
```

`--yes` skips the question, for a script or for one of the iOS shells that
cannot draw a prompt. `reset` reads the files rather than loading them, so a
configuration too broken to parse is still something you can clear.

## The randomness

Every number comes from `secrets.SystemRandom`, which draws from the operating
system's entropy pool. The `random` module is not imported anywhere in the
package — its Mersenne Twister is seedable and reproducible, which is a
liability here — and a test enforces that.

The same source picks which dates to play, because a schedule chosen by habit
is as predictable as numbers chosen by birthdate.

## Development

```bash
git clone https://github.com/adrnbttr/blindgrid.git
cd blindgrid
uv venv && uv pip install -e ".[dev]"

pytest
ruff check . && ruff format --check .
```

CI runs the suite on Linux (3.11, 3.12, 3.13), macOS and Windows, and installs
the project with each of the three installers on a clean runner before
uninstalling it again.

**[CONTRIBUTING.md](https://github.com/adrnbttr/blindgrid/blob/main/CONTRIBUTING.md)**
covers the rest — including what will not be merged, which is worth reading
first — and
**[CHANGELOG.md](https://github.com/adrnbttr/blindgrid/blob/main/CHANGELOG.md)**
records what changed in each release.

## Responsible gambling

This tool exists because a budget written down and capped is better than a
budget decided in the moment. It does not make gambling profitable. Over any
meaningful number of draws, the expected return of a lottery ticket is
negative — that is how lotteries fund themselves — and no arrangement of
numbers changes that.

Play with money you have already decided to lose. If gambling has stopped being
a small monthly cost and started being something else, these services are free
and confidential:

- **France** — Joueurs Info Service, 09 74 75 13 13, <https://www.joueurs-info-service.fr>
- **United Kingdom** — GamCare, 0808 8020 133, <https://www.gamcare.org.uk>
- **United States** — 1-800-GAMBLER, <https://www.ncpgambling.org>
- **International** — <https://www.gamblingtherapy.org>

## License

MIT — see [LICENSE](https://github.com/adrnbttr/blindgrid/blob/main/LICENSE).
