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);
+}