From 62c941b4e8bd7392a420e478acf363713e61899d Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Thu, 24 Sep 2026 22:59:45 -0400 Subject: [PATCH] Add docs, new define directive and ASCII table output as an option --- Cargo.toml | 1 + LICENSE | 456 +++++++++++++++++++++++++++++++++++ README.md | 176 +++++++------- defines-test-match.tsrc | 21 ++ docs/engine/README.md | 280 +++++++++++++++++++++ docs/tsrc-language/README.md | 230 ++++++++++++++++++ gnu-bsd-soccer.tsrc | 2 +- scripted_match.tsrc | 2 +- src/main.rs | 31 ++- src/tsrc.rs | 33 ++- test_match.tsrc | 2 +- tests/tssr.rs | 87 +++++++ 12 files changed, 1215 insertions(+), 106 deletions(-) create mode 100644 LICENSE create mode 100755 defines-test-match.tsrc create mode 100644 docs/engine/README.md create mode 100644 docs/tsrc-language/README.md diff --git a/Cargo.toml b/Cargo.toml index efbffdf..86248e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18ecc70 --- /dev/null +++ b/LICENSE @@ -0,0 +1,456 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/README.md b/README.md index ec549f6..7383c1b 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/defines-test-match.tsrc b/defines-test-match.tsrc new file mode 100755 index 0000000..dddeaa3 --- /dev/null +++ b/defines-test-match.tsrc @@ -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); diff --git a/docs/engine/README.md b/docs/engine/README.md new file mode 100644 index 0000000..0e27187 --- /dev/null +++ b/docs/engine/README.md @@ -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, // var_name -> team_name + roster: HashMap>, // team_name -> [players] + home_var: String, // variable name for home + away_var: String, // variable name for away + goals: Option>, // 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 = env::args().collect(); + // --help / -h → print usage, exit + // --json → JSON output + // --script → 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 diff --git a/docs/tsrc-language/README.md b/docs/tsrc-language/README.md new file mode 100644 index 0000000..88492a9 --- /dev/null +++ b/docs/tsrc-language/README.md @@ -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') + ++------------------------------------+ +``` diff --git a/gnu-bsd-soccer.tsrc b/gnu-bsd-soccer.tsrc index 57f5677..b3fbfd7 100755 --- a/gnu-bsd-soccer.tsrc +++ b/gnu-bsd-soccer.tsrc @@ -1,4 +1,4 @@ -#!/home/mrkmntal/tssr/target/release/tssr +#!/usr/bin/env tssr string home = "GNU United"; string away = "BSD Athletic"; diff --git a/scripted_match.tsrc b/scripted_match.tsrc index 62e03bd..ae65bab 100755 --- a/scripted_match.tsrc +++ b/scripted_match.tsrc @@ -1,4 +1,4 @@ -#!/home/mrkmntal/tssr/target/release/tssr +#!/usr/bin/env tssr // GNU United vs BSD Athletic — fully scripted match diff --git a/src/main.rs b/src/main.rs index 0d8832e..064fcb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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), + } } } diff --git a/src/tsrc.rs b/src/tsrc.rs index 4bafb04..a8f5cbd 100644 --- a/src/tsrc.rs +++ b/src/tsrc.rs @@ -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, @@ -15,6 +22,7 @@ pub struct TsrcScript { pub home_var: String, pub away_var: String, pub goals: Option>, + pub output_mode: OutputMode, } pub fn parse_tsrc(path: &str) -> Result { @@ -31,6 +39,7 @@ pub fn parse_tsrc(path: &str) -> Result { let mut player_entries: HashMap<(String, String), usize> = HashMap::new(); let mut goals: Vec = 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 { 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 { home_var, away_var, goals: if goals.is_empty() { None } else { Some(goals) }, + output_mode, }) } +fn parse_define(s: &str, n: usize) -> Result { + 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, roster: &HashMap>, diff --git a/test_match.tsrc b/test_match.tsrc index fcaf0dc..cac83b3 100755 --- a/test_match.tsrc +++ b/test_match.tsrc @@ -1,4 +1,4 @@ -#!/home/mrkmntal/tssr/target/release/tssr +#!/usr/bin/env tssr // Man Page City vs Libre Pool string home = "Linux City"; diff --git a/tests/tssr.rs b/tests/tssr.rs index 6318f4c..ac202bf 100644 --- a/tests/tssr.rs +++ b/tests/tssr.rs @@ -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); +}