Add docs, new define directive and ASCII table output as an option

This commit is contained in:
mrkmntal 2026-09-24 22:59:45 -04:00
commit 62c941b4e8
12 changed files with 1215 additions and 106 deletions

View file

@ -3,6 +3,7 @@ name = "tssr"
version = "0.1.0"
edition = "2021"
description = "Tiny Soccer Simulator Rust - generates random soccer matches on the spot"
authors = ["Mark Robillard Jr (MARKMENTAL)"]
[lib]
name = "tssr"

456
LICENSE Normal file
View file

@ -0,0 +1,456 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other works are designed to take
away your freedom to share and change the works. By contrast, the GNU
General Public License is intended to guarantee your freedom to share
and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source or can get it for free if
you receive it, that you can change the software or use pieces of it in
new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software: if you
modify it, you must pass on the same freedoms to the recipients.
Responsibilities include passing along copyright notices, license text,
and noting any changes.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For developers' and users' protection, the GPL clearly explains that
there is no warranty for this free software. Both users' and
developers' interests require that the software be maintained, and the
GPL requires that modified versions be marked as changed so that
problems will not be attributed erroneously to previous authors.
Finally, the GPL requires that software patents cannot be used to
render the non-free program effectively proprietary. To prevent this,
the GPL promises that patents cannot be used to restrict the making,
using, or selling of a program, or to restrict the freedoms that the
GPL guarantees.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "You" and "your". "Licensees"
and "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you infringe under applicable copyright law,
except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without making
the work available to the public), making available in some countries,
and other acts of public distribution.
To "convey" a work means any kind of propagation that enables other
parties to make or modify copies. Mere interaction with a computer
network, without transfer of a copy, does not convey a work.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominent feature that
(1) displays an appropriate copyright notice, and (2) tells the user
that there is no warranty for the work (except to the extent that
warranties are provided), that the licensees may convey the work under
this License, and how to view a copy of this License. If the interface
presents a specific list of user commands or options, such as a menu,
that satisfies this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an
established standard recognized by an international standards body, or,
in the case of interfaces for a given programming language, one that
is widely used among developers working in that language. The standard
interface for a function call is its name, parameter list, and return
value type.
The "System Libraries" for a work include anything, other than the
work as a whole, that (a) is included in the normal manner that the
operating system is distributed in, and (b) that makes the major
components of the operating system (such as the kernel, window system,
and command-line shells) function, but which are not themselves
considered major components of the system. A "System Library" is
anything included with the work that can be considered the operating
system's compiler, or a major compiler used to produce the work, or the
dynamic linker used to load the work for execution.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to make modifications to the work,
including scripts to control those activities. However, it does not
include the work's System Libraries, or general-purpose tools or
generally available free programs which are used unmodified in
performing those activities but which are not part of the work. For
example, Corresponding Source includes interface definition files
associated with source files for the work, and the shared libraries
and dynamic linker scripts for an executable work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the copyright
of the Program, and are irrevocable provided the stated conditions are
met. This License explicitly affirms your unlimited permission to run
the unmodified Program. The output from running a covered work is
covered by this License only if the output, given its content,
constitutes a covered work. This License acknowledges your rights of
fair use or other equivalent, as provided by any copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or providing you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any additional use of
the copyrighted material.
Conveying under any other permission is permitted solely under the
conditions stated below. Sublicensing is not permitted; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 26 May 1996, or under any
similar law prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to
prohibit circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you intend that the waiver not restrict
the operation or interaction of the work with technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, on any condition, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all notices
stating that this License and the additional terms apply to the Program;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You must convey a modified version of the Program under terms that
ensure that the construction of the modified version is licensed under
this License. The modified version must be licensed under this License,
and you must make the source code of the modified version available
under the terms of this License. A modified version of the Program
must be licensed under this License, not under any other license.
Moreover, you must make it clear that the license of the modified
version is this License, not any other license.
6. Conveying Non-Source Forms.
If you convey a covered work in object code form, you must also
convey the Corresponding Source under the terms of this License,
together with information sufficient to allow the recipient to produce
the Corresponding Source. This may be done in one of the following
ways:
a) Accompany the object code with a written offer, valid for at
least three years and valid for as long as you offer spare parts or
customer support for that product, to give anyone who possesses the
object code a complete copy of the Corresponding Source, under the
terms of this License, plus a nominal fee for the cost of physically
performing this transfer.
b) Accompany the object code with a written offer, valid for at
least three years and valid for as long as you offer spare parts or
customer support for that product, to provide a copy of the
Corresponding Source from a public server or from a similar
server that is chargeable.
c) Accompany the object code with a copy of the written offer
given to the individual. This is only possible if you convey the
object code in response to such an offer.
d) Convey the object code with a written offer that allows the
recipient limited distribution of the object code and under terms
that permit the recipient to reconstruct the Corresponding Source.
If the object code is conveyed in a tailed approach (i.e., with a
written offer that is valid for at least three years and valid
for as long as you offer spare parts or customer support for that
product), the Corresponding Source must be provided under the terms
of this License, plus a nominal fee for the cost of physically
performing this transfer.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that apply to the entire Program shall be
treated as if they were included in the License, to the extent that
they are valid under applicable law. If additional permissions apply
only to part of the Program, that part may be used separately under
those permissions, but the entire Program remains governed by this
License without regard to the additional terms.
Notwithstanding the above, no additional terms may be applied to
the Program if they alter or restrict the meaning of this License.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the terms
of this License).
However, if you cease all violation of a particular licensee, then
their license from you under this License is reinstated (a)
provisionally, unless and until the licensor explicitly and finally
terminates your license, and (b) permanently, if the licensor fails to
notify the violation by some reasonable means prior to 60 days after
the notification.
Moreover, a license from you under this License is reinstated
permanently if the licensee notifies you of the violation by some
reasonable means, this is the first time you have received notice of
this violation under this License, and the licensee cures the
violation within 30 days after receiving the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you are not eligible to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a result of having interacted with the Program
without any such propagation does not require acceptance. However,
nothing else grants you permission to propagate or modify the Program.
These actions infringe copyright if you do not have this License. If
you do not accept this License then you may not modify the Program
beyond the scope of fair use or other exceptions provided by
copyright law.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for ensuring compliance by third parties with this License.
An "entity transaction" is a transfer of control of an organization,
or substantially all assets of one, or a corporation, or a partnership,
or an organization with or without a legal personality. If you convey
a covered work to an entity transaction, and the entity received the
covered work is granted a license for the work, then the license is
also granted to the successor in interest that benefits from such a
conveyance, but only if the successor agrees to the terms of this
License.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of either the Program or a work on which the Program is based.
Each contributor retains the exclusive right, under their own
patent law, to grant permission for making, using, selling, offering
for sale, or importing the contribution, and to sublicense those
rights at or through the contributor's own patent office.
If a patent holder conveys a covered work, and the work was
included in the Program for the purpose of practicing the patent
without a license, then the patent holder's license from you for
the work is void, and the patent holder's patent license for the
work will not be extended to the work by the terms of this License.
If a patent holder conveys a covered work as part of a larger
fabrication, and the work was included in the Program for the
purpose of practicing the patent without a license, then the patent
holder's license from you for the work is void.
If a contributor that conveys a covered work is a patent licensee
that extends the license to a larger fabrication, and the work was
included in the Program for the purpose of practicing the patent
without a license, then the license is void.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement
or otherwise) that could require you to surrender the freedom granted
under this License, you may not convey a covered work unless you also
accept those conditions. For example, if you convey a covered work
under terms that oblige you to convey the same freedom to others, you
would be doing so only if you could also ensure the freedom of the
recipients to receive and modify the work under this License.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a
single combined program, and to convey the resulting work. The terms
of this License will apply to the Program part of the work, and the
terms of the GNU Affero General Public License will apply to the
part of the work that is licensed under that license.
14. Revised Versions of this License.
The Free Software Foundation may publish new and/or revised versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions of either that numbered
version or any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used for the
Program, that proxy's public statement of acceptance of a version
permanently authorizes that version for the use of the Program.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. OTHERWISE THE PROGRAM IS PROVIDED "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICE, REPAIR OR
CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL ANY COPYRIGHT
HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given effect, then the effect of those provisions is
reduced to a minimum, and the Program shall be treated as subject to
an implied warranty of merchantability and a right to damages for
breach of contract or breach of the implied warranty of a
merchantable quality.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be the best
possible program, the best way to achieve this is to make it free
software which everyone can redistribute and change under these terms.
To do this, attach the following notices to your program. It is
safest to attach them to the start of each source file to most
effectively state the exclusion of warranty; and each file should
have at least the "copyright" line and a pointer to where the full
notice can be found.
tssr — Tiny Soccer Simulator Rust
Copyright (C) 2026 Mark Robillard Jr (MARKMENTAL)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and physical mail.
If your program can perform simple terminal interactions, attach a
short notice like this when the program starts:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w`.
This is free software, and you are welcome to distribute it under
certain conditions; type `show c` for details.
The hypothetical commands `show w` and `show c` should show the
appropriate parts of the General Public License.
You should also get your employer (if you work as a student) or
your school, if any, to sign a "copyright disclaimer" for the
program, if necessary. For more information on how to do this, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a terminal
emulator or a very simple text editor, the License does not permit
incorporating it into a larger application. If you are building a
tool to be used by the public, you might consider using the GNU
Affero General Public License instead. For more information, see
<https://www.gnu.org/licenses/licenses.html>.

176
README.md
View file

@ -2,6 +2,10 @@
A blazing-fast CLI that generates completely random soccer matches on the spot — or run scripted matches with the `.tsrc` (Tiny Soccer Sim Rust C) language.
## Author
Mark Robillard Jr (MARKMENTAL)
## Build
```bash
@ -13,22 +17,19 @@ The binary will be at `target/release/tssr`.
## Usage
```bash
./tssr # random match (table output)
./tssr --json # random match (JSON output)
./tssr --script match.tsrc # scripted match (table output)
./tssr --script match.tsrc --json # scripted match (JSON output)
./tssr --help # show usage info
./tssr # random match (Unicode table)
./tssr --json # random match (JSON)
./tssr --script match.tsrc # scripted match (table)
./tssr --script match.tsrc --json # scripted match (JSON)
./tssr --help # show usage info
```
### Shebang
`.tsrc` scripts can be run directly with a shebang:
```tsrc
#!/usr/bin/env tssr
string home = "Team A";
string away = "Team B";
// ...
match(home, away);
```
@ -37,9 +38,17 @@ chmod +x match.tsrc
./match.tsrc
```
## Output
## Output Modes
### Table mode (default)
| Mode | How to enable |
|------|---------------|
| Unicode table | Default (no flag, no `#define`) |
| ASCII table | `#define ASCII_TABLE` in script |
| JSON | `--json` CLI flag or `#define JSON` in script |
The `--json` CLI flag always takes priority over `#define`.
### Unicode table (default)
```
╔════════════════════════════════════════╗
@ -48,19 +57,33 @@ chmod +x match.tsrc
║ Man Page City 2 - 1 Libre Pool║
╠════════════════════════════════════════╣
║ ║
║ Shots 10 8 ║
║ On Target 4 3 ║
║ Possession 55% 45% ║
║ Shots 10 8 ║
║ On Target 4 3 ║
║ Possession 55% 45% ║
║ ║
╠════════════════════════════════════════╣
║ Goals: ║
║ Man Page City: Marco Smith (23') ║
║ Man Page City: Leo Brown (67') ║
║ Libre Pool: Dan Garcia (45') ║
╚════════════════════════════════════════╝
```
### JSON mode (`--json`)
### ASCII table (`#define ASCII_TABLE`)
```
+--------------------------------------------+
| TSSR MATCH RESULT |
+--------------------------------------------+
| Man Page City 2 - 1 Libre Pool |
+--------------------------------------------+
| |
| Shots 10 8 |
| On Target 4 3 |
| Possession 55% 45% |
+--------------------------------------------+
```
### JSON (`--json` or `#define JSON`)
```json
{
@ -71,8 +94,7 @@ chmod +x match.tsrc
"shots_on_target": 4,
"possession": 55,
"scorers": [
{"player": "Marco Smith", "minute": 23},
{"player": "Leo Brown", "minute": 67}
{"player": "Marco Smith", "minute": 23}
]
},
"away": {
@ -88,88 +110,45 @@ chmod +x match.tsrc
}
```
## `.tsrc` Scripting Language
## Scripting with `.tsrc`
`.tsrc` (Tiny Soccer Sim Rust C) is a minimal C-like DSL for defining teams, players, and match events programmatically.
`.tsrc` (Tiny Soccer Sim Rust C) is a minimal C-like DSL for defining teams, players, goals, and output mode.
### Language Reference
### Quick example
```tsrc
// Declare a team variable
string var_name = "Team Name";
#!/usr/bin/env tssr
#define ASCII_TABLE
// Assign a player to a team (by variable or literal)
players["Player Name"] = var_name;
players["Player Name"] = "Team Name";
string home = "GNU United";
string away = "BSD Athletic";
// Script a goal: minute (1-90), team (var or literal), player (literal)
goal(45, var_name, "Player Name");
goal(45, "Team Name", "Player Name");
players["Richard Stallman"] = home;
players["Brian Fox"] = home;
players["Theo de Raadt"] = away;
// Select teams for the match (required)
match(var_home, var_away);
goal(12, home, "Richard Stallman");
goal(34, away, "Theo de Raadt");
goal(78, "BSD Athletic", "Theo de Raadt");
match(home, away);
```
### Rules
### Language syntax
- Variables must be declared with `string` before use
- `match()` directive is required — references variable names
- `players[]` references team names (values), not variable names — but variables are resolved automatically
- `goal()` arguments: `minute, team, player` — team can be variable or literal
- All semicolons are required
- `//` for single-line comments
- Strict validation: any inconsistency produces an error with a helpful message and exits immediately
| Statement | Purpose |
|-----------|---------|
| `string var = "Team";` | Declare a team variable |
| `players["Player"] = var;` | Assign player to team (variable or literal) |
| `goal(min, team, "Player");` | Script a goal at a specific minute |
| `match(home, away);` | Select teams for the match (required) |
| `#define JSON / ASCII_TABLE / UNICODE_TABLE` | Set output mode |
### Validation Errors
When `goal()` is used, the score becomes deterministic — goals, minutes, and scorers are exact. Shots are floored at the goal count. Without `goal()`, everything is fully randomized.
The parser is strict — it will fail with a clear message if:
The parser is strict: any inconsistency produces a clear error and exits immediately.
- A variable is used before being declared
- A variable is defined twice
- A player is added to a team that isn't in `match()`
- A player is added to the same team twice
- A goal references a player not on that team
- A goal references a team not in the match
- A goal minute is outside 1–90
- The same player scores at the same minute twice
- A match team has no players assigned
- `match()` references the same team for both home and away
### Scripted Goals
When `goal()` is used, the match becomes deterministic:
- Score is derived from the count of scripted goals per team
- Goal scorers and minutes are exactly as specified
- Shots and shots-on-target are floored at the goal count
- Possession remains randomized
Without `goal()`, everything is fully randomized.
## Team Names (Random Mode)
When no script is provided, teams are randomly selected from PES-style parodies referencing free software and open source:
- Man Page City, Libre Pool, ArsenAL, Shellsea, TomThumb FC
- Real Madread, BarcelON, BayErR, Door Mount, Inter Mission
- Man Unixited, A-JaX, CellTick, RanGERS, LaZIO, RomaAEM
- Port OS, BenFICA, Fine Node, AC M1Lan, PSGROOT
- Free BSD Athletic, Kernel Panic, Tor Valdes, Debian United
- Arch Lisbon, NixOS City, Fork Enoord, Branchand FC, Merge Conflict
## Stats Included
- Score
- Shots & shots on target
- Possession percentage
- Goal scorers with minute of goal
## Example Files
| File | Description |
|------|-------------|
| `scripted_match.tsrc` | GNU United vs BSD Athletic — 5 scripted goals with mixed variable/literal team references |
| `gnu-bsd-soccer.tsrc` | Variable-reference players, random goals |
| `test_match.tsrc` | Intentionally broken — demonstrates error handling |
See [`docs/tsrc-language/README.md`](docs/tsrc-language/README.md) for the full language reference.
## Testing
@ -177,15 +156,26 @@ When no script is provided, teams are randomly selected from PES-style parodies
cargo test
```
13 integration tests cover:
- Goal validation (player on team, minute range, duplicates)
- Script validation (undefined variables, same team, missing match, no players)
- Happy paths (valid script, variable references, full scripted match)
19 integration tests covering:
- Goal validation (player on team, minute range, duplicates, undefined variables)
- Script validation (undefined variables, same team, missing match, no players, duplicate variables)
- `#define` directive (all modes, duplicate, unknown)
- Happy paths (valid scripts, variable references, full scripted match)
## Documentation
| Document | Description |
|----------|-------------|
| [`docs/tsrc-language/README.md`](docs/tsrc-language/README.md) | Full `.tsrc` language reference, grammar, validation rules |
| [`docs/engine/README.md`](docs/engine/README.md) | Engine internals: random generation, scripted override, output formatting |
## Performance
- Single runtime dependency (`rand`)
- `tempfile` is a dev-dependency only (for tests)
- Release profile uses LTO, single codegen unit, panic=abort, and symbol stripping
- Designed for sub-second cold compilation and instant execution
- Release profile uses LTO, single codegen unit, panic=abort, symbol stripping
- Sub-second cold compilation
- Binary size: ~340KB stripped
## License
This project is licensed under the GNU General Public License v3.0 — see [LICENSE](LICENSE).

21
defines-test-match.tsrc Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env tssr
#define ASCII_TABLE
string home = "GNU United";
string away = "BSD Athletic";
players["Richard Stallman"] = home;
players["Brian Fox"] = home;
players["Linus Torvalds"] = home;
players["Theo de Raadt"] = away;
players["Jordan Hubbard"] = away;
players["Ken Thompson"] = away;
goal(12, home, "Richard Stallman");
goal(34, away, "Theo de Raadt");
goal(56, home, "Brian Fox");
goal(78, away, "Ken Thompson");
goal(89, home, "Linus Torvalds");
match(home, away);

280
docs/engine/README.md Normal file
View file

@ -0,0 +1,280 @@
# tssr Engine Reference
The tssr engine generates soccer match results. It operates in two modes: **random** (everything fabricated on the spot) and **scripted** (driven by a `.tsr`c file).
## Overview
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CLI / .tsrc │────▶│ Match Gen │────▶│ Output │
│ Input │ │ Engine │ │ Formatter │
└──────────────┘ └──────────────┘ └──────────────┘
```
1. **Input** — CLI flags and/or `.tsrc` script parsed into `TsrcScript`
2. **Match Generation** — `gen_match()` produces a `Match` struct
3. **Output** — `print_table`, `print_ascii_table`, or `print_json` renders the result
## Random Mode
When no `.tsrc` script is provided, everything is randomized.
### Team Selection
Two distinct teams are picked from a hardcoded pool of 30 FOSS-parody names:
- Man Page City, Libre Pool, ArsenAL, Shellsea, TomThumb FC
- Real Madread, BarcelON, BayErR, Door Mount, Inter Mission
- Man Unixited, A-JaX, CellTick, RanGERS, LaZIO, RomaAEM
- Port OS, BenFICA, Fine Node, AC M1Lan, PSGROOT
- Free BSD Athletic, Kernel Panic, Tor Valdes, Debian United
- Arch Lisbon, NixOS City, Fork Enoord, Branchand FC, Merge Conflict
### Player Name Generation
Names are fabricated from two pools (48 first names, 50 last names):
```rust
fn gen_name(rng) -> String {
format!("{} {}", FIRST_NAMES.choose(rng), LAST_NAMES.choose(rng))
}
```
### Stat Ranges
| Stat | Range | Distribution |
|------|-------|--------------|
| Score (per team) | 0–5 | Uniform random |
| Shots (per team) | 4–18 | Uniform random |
| Shots on target | 1–min(shots, 10) | Uniform random |
| Possession (home) | 38–62% | Uniform random |
| Possession (away) | 100 - home | Derived |
| Goal minute | 1–90 | Uniform random |
## Match Generation Flow
```
gen_match(rng, script) -> Match
```
### 1. Team Resolution
```rust
let (home_name, away_name) = match script {
Some(s) => (s.teams[home_var].clone(), s.teams[away_var].clone()),
None => (random_team(), random_team_excluding(home)),
};
```
### 2. Score & Scorers
Two paths depending on whether `goal()` was scripted:
**Scripted goals present:**
- Partition goals by team
- Score = count of goals per team
- Scorers = exact `(player, minute)` from script
**No scripted goals:**
- Score = `rng.gen_range(0..=5)` per team
- Scorers = `pick_scorers(roster, score, rng)` — random selection from roster or name pools
### 3. Shots
**Scripted goals present** — shots floored at goal count:
```rust
let shots = score.max(rng.gen_range(4..=18));
let on_target = score.max(rng.gen_range(1..=shots.min(10)));
```
**No scripted goals** — fully random:
```rust
let shots = rng.gen_range(4..=18);
let on_target = rng.gen_range(1..=shots.min(10));
```
### 4. Possession
Always random: `rng.gen_range(38..=62)` for home, `100 - home` for away.
## Scripted Mode
When a `.tsrc` script is provided, parsed into `TsrcScript`:
```rust
struct TsrcScript {
teams: HashMap<String, String>, // var_name -> team_name
roster: HashMap<String, Vec<String>>, // team_name -> [players]
home_var: String, // variable name for home
away_var: String, // variable name for away
goals: Option<Vec<ScriptedGoal>>, // scripted goals (None = random)
output_mode: OutputMode, // UnicodeTable, AsciiTable, Json
}
```
### Override Behavior
| Field | Source when scripted | Source when random |
|-------|---------------------|-------------------|
| Team names | `s.teams[home_var]` | Random from TEAM_NAMES |
| Player roster | `s.roster[team]` | Empty (random names) |
| Goal scorers | Exact from `s.goals` | `pick_scorers()` |
| Score | Count from `s.goals` | `rng.gen_range(0..=5)` |
| Shots | `max(score, random)` | `rng.gen_range(4..=18)` |
| Possession | Always random | Always random |
## Output Formatting
Three output modes, selected by:
1. `--json` CLI flag (highest priority)
2. Script's `#define` directive
3. Default: `UNICODE_TABLE`
### Unicode Table
Uses box-drawing characters:
```
╔════════════════════════════════════════╗
║ TSSR MATCH RESULT ║
╠════════════════════════════════════════╣
║ Man Page City 2 - 1 Libre Pool║
╠════════════════════════════════════════╣
║ ║
║ Shots 10 8 ║
║ On Target 4 3 ║
║ Possession 55% 45% ║
║ ║
╠════════════════════════════════════════╣
║ Goals: ║
║ Man Page City: Marco Smith (23') ║
║ Man Page City: Leo Brown (67') ║
╚════════════════════════════════════════╝
```
### ASCII Table
Same layout, pure ASCII characters:
```
+--------------------------------------------+
| TSSR MATCH RESULT |
+--------------------------------------------+
| Man Page City 2 - 1 Libre Pool |
+--------------------------------------------+
+--------------------------------------------+
| Goals: |
| Man Page City: Marco Smith (23') |
+--------------------------------------------+
```
### JSON
```json
{
"home": {
"team": "Man Page City",
"score": 2,
"shots": 10,
"shots_on_target": 4,
"possession": 55,
"scorers": [
{"player": "Marco Smith", "minute": 23},
{"player": "Leo Brown", "minute": 67}
]
},
"away": {
"team": "Libre Pool",
"score": 1,
"shots": 8,
"shots_on_target": 3,
"possession": 45,
"scorers": [
{"player": "Dan Garcia", "minute": 45}
]
}
}
```
## Table Rendering Implementation
Both table modes share a single function parameterized by border characters:
```rust
fn print_table_chars(m: &Match, horiz, vert, tl, tr, bl, br, cross)
```
| Mode | horiz | vert | corners | cross |
|------|-------|------|---------|-------|
| Unicode | `═` | `║` | `╔╗╚╝` | `╠` |
| ASCII | `-` | `\|` | `++++` | `+` |
Layout is content-driven — `inner_width` is calculated from the longest line (score, title, or goal text) plus 2 padding characters.
## CLI Argument Parsing
```rust
fn main() {
let args: Vec<String> = env::args().collect();
// --help / -h → print usage, exit
// --json → JSON output
// --script <file> → parse .tsrc
// bare file path → shebang mode (auto-detect)
}
```
Shebang detection: if any non-flag argument is a path to an existing file, it's treated as a script.
## Build Configuration
```toml
[profile.release]
opt-level = 3 # Maximum optimization
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
panic = "abort" # Smaller binary, no unwinding
strip = true # Remove debug symbols
```
## Project Layout
```
tssr/
├── Cargo.toml # Package config (lib + bin)
├── src/
│ ├── lib.rs # Library root (pub mod tsrc)
│ ├── main.rs # CLI binary
│ └── tsrc.rs # .tsrc parser + validation
├── tests/
│ └── tssr.rs # 19 integration tests
├── docs/
│ ├── tsrc-language/
│ │ └── README.md # Language reference
│ └── engine/
│ └── README.md # Engine reference
├── scripted_match.tsrc # Example: scripted goals
├── defines-test-match.tsrc # Example: ASCII_TABLE mode
├── gnu-bsd-soccer.tsrc # Example: variable references
└── test_match.tsrc # Intentionally broken (for testing errors)
```
## Dependencies
| Crate | Version | Purpose | Scope |
|-------|---------|---------|-------|
| `rand` | 0.8 | Random number generation | Runtime |
| `tempfile` | 3 | Temporary files for tests | Dev only |
## Testing
```bash
cargo test
```
19 integration tests cover:
- `.tsrc` language validation (13 tests)
- `#define` directive handling (6 tests)
- All error conditions produce correct error messages
- Happy paths for valid scripts

View file

@ -0,0 +1,230 @@
# `.tsrc` Language Reference
`.tsrc` (Tiny Soccer Sim Rust C) is a minimal C-like DSL for scripting soccer matches in tssr. It lets you define teams, assign players, script goals, and control output format — all from a plain text file.
## Invocation
### Via `--script` flag
```bash
tssr --script match.tsrc # table output
tssr --script match.tsrc --json # JSON output
```
### Via shebang
Add this as the first line of your `.tsrc` file:
```tsrc
#!/usr/bin/env tssr
```
Then make it executable and run directly:
```bash
chmod +x match.tsrc
./match.tsrc
```
## Language Syntax
### Team Declaration
```tsrc
string var_name = "Team Name";
```
Declares a variable that holds a team name. Variables can be referenced later in `players[]`, `goal()`, and `match()`.
**Rules:**
- Variable names must be alphanumeric + underscores
- Duplicate variable names are an error
- The string value is the team's display name
### Player Assignment
```tsrc
players["Player Name"] = var_name;
players["Player Name"] = "Team Name";
```
Assigns a player to a team. The right side can be either a previously declared variable or a string literal.
**Rules:**
- The team (right side) must resolve to a team that appears in `match()`
- The same player cannot be added to the same team twice
- Variables must be declared before use (forward references are an error)
### Scripted Goals
```tsrc
goal(minute, team, player);
```
Scripts a specific goal event. When any `goal()` appears, the match score becomes deterministic.
| Argument | Type | Description |
|----------|------|-------------|
| `minute` | integer (1-90) | Minute the goal was scored |
| `team` | variable or string | The team that scored |
| `player` | string literal | The player who scored |
**Rules:**
- Minute must be between 1 and 90
- Team must be one of the two match teams
- Player must be on that team's roster
- The same player cannot score at the same minute twice
- Variables must be declared before use
### Match Selection
```tsrc
match(var_home, var_away);
```
Selects which two teams play. **Required** — every `.tsrc` script must contain exactly one `match()`.
**Rules:**
- Both variables must be previously declared
- Home and away must be different teams
- Both teams must have at least one player assigned
### Output Mode Directive
```tsrc
#define JSON
#define ASCII_TABLE
#define UNICODE_TABLE
```
Sets the output format for the match. Optional — defaults to `UNICODE_TABLE`.
**Rules:**
- Only one `#define` per script
- Must be one of the three known modes
- Unknown names produce an error
The `--json` CLI flag always overrides the script's `#define`.
## Complete Grammar
```
script = statement*
statement = define | string_decl | player_insert | goal_stmt | match_stmt
define = "#define" identifier
string_decl = "string" identifier "=" string_literal ";"
player_insert = "players" "[" string_literal "]" "=" (identifier | string_literal) ";"
goal_stmt = "goal" "(" number "," (identifier | string_literal) "," string_literal ")" ";"
match_stmt = "match" "(" identifier "," identifier ")" ";"
comment = "//" .* "\n"
shebang = "#!" .* "\n"
```
## Output Modes
| Mode | `#define` | CLI flag | Description |
|------|-----------|----------|-------------|
| Unicode table | `UNICODE_TABLE` (default) | — | Box-drawing characters (`╔`, `═`, `║`) |
| ASCII table | `ASCII_TABLE` | — | ASCII-only (`+`, `-`, `|`) |
| JSON | `JSON` | `--json` | Machine-readable JSON |
## Scripted Goals Behavior
When `goal()` is used, the match becomes deterministic:
| Field | Behavior with `goal()` | Behavior without |
|-------|------------------------|-------------------|
| Score | Count of goals per team | Random 0-5 |
| Scorers | Exact `(player, minute)` from script | Random names + minutes |
| Shots | `max(score, random(4..=18))` — floor = goals | Random 4-18 |
| Shots on target | `max(score, random(1..shots))` | Random |
| Possession | Still random | Random |
## Variable Resolution
Variables are resolved at parse time by looking up their declared value:
```tsrc
string home = "GNU United";
string away = "BSD Athletic";
players["Richard Stallman"] = home; // resolves to "GNU United"
players["Theo de Raadt"] = away; // resolves to "BSD Athletic"
goal(12, home, "Richard Stallman"); // goal for "GNU United"
goal(34, "BSD Athletic", "Theo de Raadt"); // literal also works
match(home, away); // GNU United vs BSD Athletic
```
You can mix variables and literals freely. Forward references (using a variable before its `string` declaration) are an error.
## Validation
The parser is strict. Any inconsistency produces an error and exits immediately.
### Error Conditions
| Error | Cause |
|-------|-------|
| `undefined variable 'x'` | Variable used before `string x = ...` declaration |
| `variable 'x' redefined` | Two `string x = ...` declarations |
| `players[...] = "X" which is not a match team` | Roster team isn't in `match()` |
| `duplicate player "X" on team "Y"` | Same player added to same team twice |
| `"X" has no players assigned` | A match team has zero players |
| `match() has the same team for both home and away` | Both `match()` args resolve to same team |
| `no match() directive found` | Missing `match()` |
| `goal() minute must be between 1 and 90` | Invalid minute |
| `player "X" is not on team "Y"` | Goal references unregistered player |
| `duplicate goal` | Same player scores same minute twice |
| `undefined variable 'x' in goal()` | Goal uses undeclared team variable |
| `duplicate #define` | More than one `#define` in script |
| `unknown #define 'FOO'` | Unrecognized output mode |
## Full Example
```tsrc
#!/usr/bin/env tssr
// GNU United vs BSD Athletic
#define ASCII_TABLE
string home = "GNU United";
string away = "BSD Athletic";
players["Richard Stallman"] = home;
players["Brian Fox"] = home;
players["Linus Torvalds"] = home;
players["Theo de Raadt"] = away;
players["Jordan Hubbard"] = away;
players["Ken Thompson"] = away;
goal(12, home, "Richard Stallman");
goal(34, away, "Theo de Raadt");
goal(56, home, "Brian Fox");
goal(78, "BSD Athletic", "Ken Thompson");
goal(89, home, "Linus Torvalds");
match(home, away);
```
**Output:**
```
+------------------------------------+
+ TSSR MATCH RESULT +
+------------------------------------+
+ GNU United 3-2 BSD Athletic +
+------------------------------------+
+ +
+Shots 4 5 +
+On Target 3 2 +
+Possession 48% 52% +
+ +
+Goals: +
+GNU United: Richard Stallman (12') +
+GNU United: Brian Fox (56') +
+GNU United: Linus Torvalds (89') +
+BSD Athletic: Theo de Raadt (34') +
+BSD Athletic: Ken Thompson (78') +
+------------------------------------+
```

View file

@ -1,4 +1,4 @@
#!/home/mrkmntal/tssr/target/release/tssr
#!/usr/bin/env tssr
string home = "GNU United";
string away = "BSD Athletic";

View file

@ -1,4 +1,4 @@
#!/home/mrkmntal/tssr/target/release/tssr
#!/usr/bin/env tssr
// GNU United vs BSD Athletic — fully scripted match

View file

@ -192,7 +192,7 @@ fn pad_line(content: &str, inner_width: usize) -> String {
format!("{}{}", content, " ".repeat(inner_width.saturating_sub(len)))
}
fn print_table(m: &Match) {
fn print_table_chars(m: &Match, horiz: char, vert: char, tl: char, tr: char, bl: char, br: char, cross: char) {
let home = &m.home.name;
let away = &m.away.name;
let score_line = format!("{home} {}-{} {away}", m.home.score, m.away.score);
@ -230,15 +230,24 @@ fn print_table(m: &Match) {
);
let inner_width = max_content + 2;
println!("╔{}╗", "═".repeat(inner_width));
println!("║{}║", pad_line(&format!("{:^w$}", title, w = inner_width), inner_width));
println!("╠{}╣", "═".repeat(inner_width));
println!("║{}║", pad_line(&format!("{:^w$}", score_line, w = inner_width), inner_width));
println!("╠{}╣", "═".repeat(inner_width));
let h = horiz.to_string().repeat(inner_width);
println!("{}{}{}", tl, h, tr);
println!("{}{}{}", vert, pad_line(&format!("{:^w$}", title, w = inner_width), inner_width), vert);
println!("{}{}{}", cross, h, cross);
println!("{}{}{}", vert, pad_line(&format!("{:^w$}", score_line, w = inner_width), inner_width), vert);
println!("{}{}{}", cross, h, cross);
for line in &lines {
println!("║{}║", pad_line(line, inner_width));
println!("{}{}{}", vert, pad_line(line, inner_width), vert);
}
println!("╚{}╝", "═".repeat(inner_width));
println!("{}{}{}", bl, h, br);
}
fn print_table(m: &Match) {
print_table_chars(m, '═', '║', '╔', '╗', '╚', '╝', '╠');
}
fn print_ascii_table(m: &Match) {
print_table_chars(m, '-', '+', '+', '+', '+', '+', '+');
}
fn print_json(m: &Match) {
@ -337,6 +346,10 @@ fn main() {
print_json(&m);
}
else {
print_table(&m);
match script.as_ref().map(|s| &s.output_mode) {
Some(tsrc::OutputMode::Json) => print_json(&m),
Some(tsrc::OutputMode::AsciiTable) => print_ascii_table(&m),
_ => print_table(&m),
}
}
}

View file

@ -8,6 +8,13 @@ pub struct ScriptedGoal {
pub player: String,
}
#[derive(Debug, PartialEq)]
pub enum OutputMode {
UnicodeTable,
AsciiTable,
Json,
}
#[derive(Debug)]
pub struct TsrcScript {
pub teams: HashMap<String, String>,
@ -15,6 +22,7 @@ pub struct TsrcScript {
pub home_var: String,
pub away_var: String,
pub goals: Option<Vec<ScriptedGoal>>,
pub output_mode: OutputMode,
}
pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
@ -31,6 +39,7 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
let mut player_entries: HashMap<(String, String), usize> = HashMap::new();
let mut goals: Vec<ScriptedGoal> = Vec::new();
let mut goal_entries: HashMap<(u8, String), usize> = HashMap::new();
let mut output_mode: OutputMode = OutputMode::UnicodeTable;
for (line_num, raw_line) in content.lines().enumerate() {
let n = line_num + 1;
@ -40,7 +49,16 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
continue;
}
if let Some(rest) = line.strip_prefix("string ") {
if let Some(rest) = line.strip_prefix("#define ") {
if output_mode != OutputMode::UnicodeTable {
return Err(format!(
"tsrc error: output mode already defined on line {} (duplicate #define)",
n
));
}
output_mode = parse_define(rest, n)?;
}
else if let Some(rest) = line.strip_prefix("string ") {
parse_string_decl(rest, n, &mut teams, &mut var_lines)?;
}
else if let Some(rest) = line.strip_prefix("players") {
@ -78,9 +96,22 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
home_var,
away_var,
goals: if goals.is_empty() { None } else { Some(goals) },
output_mode,
})
}
fn parse_define(s: &str, n: usize) -> Result<OutputMode, String> {
match s.trim() {
"JSON" => Ok(OutputMode::Json),
"UNICODE_TABLE" => Ok(OutputMode::UnicodeTable),
"ASCII_TABLE" => Ok(OutputMode::AsciiTable),
other => Err(format!(
"tsrc error: unknown #define '{}' on line {} (expected JSON, UNICODE_TABLE, or ASCII_TABLE)",
other, n
)),
}
}
fn validate_script(
teams: &HashMap<String, String>,
roster: &HashMap<String, Vec<String>>,

View file

@ -1,4 +1,4 @@
#!/home/mrkmntal/tssr/target/release/tssr
#!/usr/bin/env tssr
// Man Page City vs Libre Pool
string home = "Linux City";

View file

@ -222,3 +222,90 @@ fn script_no_players_for_team() {
let err = r.unwrap_err();
assert!(err.contains("no players assigned"), "got: {}", err);
}
#[test]
fn define_default_is_unicode_table() {
let r = parse(r#"
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_ok(), "got: {:?}", r.err());
assert_eq!(r.unwrap().output_mode, tssr::tsrc::OutputMode::UnicodeTable);
}
#[test]
fn define_json() {
let r = parse(r#"
#define JSON
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_ok(), "got: {:?}", r.err());
assert_eq!(r.unwrap().output_mode, tssr::tsrc::OutputMode::Json);
}
#[test]
fn define_ascii_table() {
let r = parse(r#"
#define ASCII_TABLE
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_ok(), "got: {:?}", r.err());
assert_eq!(r.unwrap().output_mode, tssr::tsrc::OutputMode::AsciiTable);
}
#[test]
fn define_unicode_table_explicit() {
let r = parse(r#"
#define UNICODE_TABLE
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_ok(), "got: {:?}", r.err());
assert_eq!(r.unwrap().output_mode, tssr::tsrc::OutputMode::UnicodeTable);
}
#[test]
fn define_duplicate_error() {
let r = parse(r#"
#define JSON
#define ASCII_TABLE
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_err());
let err = r.unwrap_err();
assert!(err.contains("duplicate #define"), "got: {}", err);
}
#[test]
fn define_unknown_error() {
let r = parse(r#"
#define FOO_BAR
string h = "Team A";
string a = "Team B";
players["P1"] = h;
players["P2"] = a;
match(h, a);
"#);
assert!(r.is_err());
let err = r.unwrap_err();
assert!(err.contains("unknown #define"), "got: {}", err);
assert!(err.contains("FOO_BAR"), "got: {}", err);
}