- Playfield Dimensions: Use a standard Tetris playfield of 10 columns by 20 rows (Tetris Guideline - TetrisWiki). This will be the active game area (the "glass") where pieces fall. The implementation can include an additional hidden buffer above the 20-row playfield for piece spawning, but only 20 rows are visible during play.
- Tetromino Set: Include the seven classic Tetris tetrominoes: I, O, T, L, J, S, and Z. Each piece is composed of four blocks in the standard configurations. Use distinct identities (or colors, if applicable in text mode) for each piece type for clarity.
- Game Loop Timing: Implement a fixed-time-step game loop for consistent updates. Each iteration of the loop should advance the game by a fixed delta time, ensuring uniform behavior regardless of system performance (Game Development Patterns and Architectures in JavaScript by Olayinka Atobiloye - Video recording). This stabilizes movement speed and timing (gravity, lock delay, etc.) across different machines. For example, you might target ~60 updates per second or a similar stable frame rate for logic updates.
- Frame Rendering: Even in a text-mode environment, separate the game state updates from rendering. Only update the game state on the fixed time step; rendering to the console can be done at the same rate or a lower rate if needed to improve performance. This separation ensures smooth piece movement and consistent input handling.
- Standalone Binary: The game must compile into a single standalone Go binary. Keep external dependencies minimal – use only what’s necessary for functionality. It’s acceptable to use an external library for improved terminal control or output (for example, libraries like
termbox-go
ortcell
for handling keyboard input and drawing characters on the console), but avoid heavy frameworks. The end result should be easy to distribute and run (just an executable with no complex setup).
- Left Arrow: Move the current piece one cell to the left, if space is available. Holding the key down should repeat the movement (you may implement autorepeat with a delay, or simply rely on the fixed game loop to move repeatedly if the key remains pressed).
- Right Arrow: Move the current piece one cell to the right, with similar behavior to the left arrow (and the same collision checks).
- Down Arrow (Soft Drop): Increase the fall speed of the current piece while the key is held. This is a soft drop, meaning the piece moves down faster than the normal gravity but not instantly. It should not instantly lock the piece; instead, it just accelerates the descent. Once released, the piece returns to normal falling speed. No additional score is typically awarded for soft drop (though some versions give 1 point per cell; this can be included as a minor detail if desired (Scoring in Tetris® — Tetris Mobile Help Center)).
- Up Arrow (Rotate): Rotate the current piece 90 degrees (typically clockwise) each time the key is pressed. If using a single rotate button, only one rotation direction is needed (clockwise by default). The rotation should respect the chosen rotation system (simple or SRS) for how the piece kicks or behaves on rotation.
- Spacebar (Hard Drop): Immediately drop the piece to the lowest possible position it can occupy (all the way down until it lands on another block or the floor). Upon a hard drop, the piece should lock into place instantly (bypassing any lock delay). Hard drop does not count as a move that resets lock delay – it forces immediate lock. Optionally, award points for hard dropping (many implementations give 2 points per cell dropped as incentive (Scoring in Tetris® — Tetris Mobile Help Center)).
- Optional Controls: Although not specified in the core requirements, consider implementing Pause (e.g., press 'P' to pause the game), which can freeze the game loop until unpaused. This is not mandatory but useful for a complete game experience.
Input Handling Note: Because this is a console application, special care is needed to read arrow keys and other keys without the user pressing Enter. Use a library or terminal mode that captures key presses in real-time. Ensure that keys like arrows and space are captured reliably (these may come as escape sequences in a raw terminal). Also, make sure to disable line buffering in the console so that key presses are detected immediately.
The game should support two rotation systems, selectable via a configuration flag (e.g., useSRS = true/false
). The default will be the simple rotation system for classic behavior, but advanced players can enable the Super Rotation System (SRS) for modern Tetris rotations.
- Simple Rotation System: A basic rotation implementation. When the player presses rotate (Up arrow), rotate the tetromino 90° clockwise about its reference point. In the simple system, if the rotation would cause the piece to overlap a wall or another block, the rotation is blocked (the piece stays in its original orientation). There are minimal or no “wall kicks” in this mode – essentially, it’s the classic NES-style rotation: pieces do not try to adjust position when rotating into a wall. This is easier to implement but less forgiving to the player. If desired, you can implement a basic wall kick for the simple system (for example, allow the piece to shift one cell away from the wall if a rotation is blocked), but this should be very limited compared to SRS.
- Super Rotation System (SRS): Implement the modern Tetris Guideline rotation system (Tetris Guideline - TetrisWiki). SRS defines a set of tests (wall kicks) that will try to offset a piece when a rotation doesn’t fit. For example, if a tetromino is up against a wall or on the floor, the game will attempt alternate positions (kicks) to allow the rotation (An Overview of Kicks in SRS | FOUR) (Tetris Guideline - TetrisWiki). The SRS logic includes a specific kick table for each piece and orientation change (note that the I tetromino has its own kick data, and other pieces share a common kick data in SRS). When using SRS:
- If a normal rotation is obstructed, attempt a sequence of offset moves (e.g., shift the piece one cell right, one cell left, one cell up, etc., in a specified order) and check if any of those placements result in a successful rotation without collision (An Overview of Kicks in SRS | FOUR).
- Implement the standard SRS kick tables for all pieces, including the unique kicks for the I piece and the wall kicks for O (the O tetromino in SRS technically doesn’t move on rotation since it’s a square).
- Ensure that all rotations are reversible; if a rotation is possible in one direction, the opposite rotation should also be possible from the resulting position (a property of SRS (Tetris Guideline - TetrisWiki)).
- Rotation Selection Flag: Provide a way to select between these rotation systems, e.g., a boolean config setting (
simpleRotation=true
as default). Internally, you might have two sets of rotation logic and wall kick data. The rest of the game (controls, game loop) can call the appropriate rotation function based on this flag. This allows easy switching for the user and also makes it straightforward to test both systems. - Spawn Orientation: When a new piece spawns at the top of the playfield, start it in a default orientation (typically spawn orientations are “flat” – e.g., the T, L, J spawn pointing upwards flat, the I piece spawns horizontal, etc., per guideline). For SRS compliance, use the standard spawn orientation (flat side down) (Tetris Guideline - TetrisWiki), but the simple system can use the same orientations for consistency. This detail ensures that pieces start in expected orientations which affects how they fit in the top of the playfield.
- Boundary Checks: The active tetromino must never move outside the bounds of the 10x20 playfield. Implement checks so that if a move or rotation would place any part of the piece beyond the left wall, right wall, or below the floor (bottom of the 20th row), that action is disallowed. Similarly, pieces should not be able to move above the top boundary during normal play (the top is only exceeded when a piece spawns or if the stack is high; see game over conditions).
- Collision with Settled Blocks: Keep track of the board grid with all settled/locked blocks. Before moving the active piece (translation or rotation), check the target cells. If any cell of the active piece’s new position would overlap a cell that is already occupied by a locked block, the movement/rotation is not allowed. This ensures pieces stack up but never overlap.
- Wall Kicks and Rotation Adjustments: When using SRS rotation, implement the wall kick checks as described in the rotation system section. The collision detection should work hand-in-hand with these kicks: test each offset position in the kick table for collisions, and rotate+move the piece to the first offset that is free. If none are free, the rotation fails (the piece stays in its original orientation) (An Overview of Kicks in SRS | FOUR). For the simple rotation mode, you might allow a very limited kick (like one upward shift if on floor, or one sideways shift if against a wall) or none at all, depending on how “classic” you want it. Document the chosen behavior clearly in code comments for future maintainers.
- Lock Delay: Implement a lock delay so pieces don’t freeze the instant they touch down on the bottom or stack. Lock delay is the time between a piece first contacting the ground and when it actually locks in place. For example, the official guideline uses a 0.5 second lock delay (Tetris Guideline | Tetris Wiki | Fandom). You can choose a similar duration (e.g. 500 milliseconds) as a starting point. During this delay, the player can still move or rotate the piece to try to fit it better. If the piece is moved or rotated such that it is no longer touching the ground, reset the lock delay timer for that piece. However, to prevent infinite stalling, consider a limit on how many times lock delay can be reset (guideline uses an infinite spin prevention where after certain moves the piece will force lock (Tetris Guideline - TetrisWiki), but a simple approach is fine for this project, e.g., allow a certain number of moves or a fixed total time).
- Gravity and Soft Drop Behavior: The piece should naturally fall one cell at a time according to a timer (gravity). At level 1 (or level 0), define a baseline gravity speed (e.g., one cell per X milliseconds). This will decrease (pieces fall faster) as the level increases. When the Down arrow is held, temporarily increase the gravity (e.g., 10× normal speed) to simulate soft drop. Ensure the collision detection still checks each step of the soft drop movement – if the piece is just one cell above the ground and the player holds Down, it should move that one cell and then stop (not pass through). The game loop may handle this by moving the piece multiple times per tick when soft dropping, or by reducing the interval until the next drop.
- Hard Drop and Lock: When the player hard drops a piece (Spacebar), calculate the lowest empty position the piece can occupy (you can do this by simulating downward movement until a collision is detected, or by scanning from the piece’s current position downwards). Place the piece there instantly and lock it immediately. Collision detection will be involved in finding that position but once found, you can lock without further checks since by definition directly above that was free and the cell below is occupied or floor. Hard drop should skip the lock delay (treat it as if the delay is zero for that drop).
- No Mid-Air Locking: Ensure that a piece only locks (becomes part of the settled stack) when either the player hard drops it or when it has been at rest at the bottom (or on other blocks) for the duration of the lock delay without player intervention. If the piece is still moving down or being moved/rotated by the player, it should remain active. Only when the conditions are met (touching ground + lock timer expired, or hard drop) do you convert the piece’s cells to settled blocks in the grid and generate a new piece.
-
Line Clear Scoring: Implement the classic scoring system where clearing more lines at once yields higher scores. As a baseline, use the standard single/double/triple/Tetris values: clearing one line = 100 points, two lines = 300 points, three lines = 500 points, and four lines (a “Tetris”) = 800 points (Scoring in Tetris® — Tetris Mobile Help Center). These values can be multiplied by the current level (for a level-based score boost) or simply added as fixed points with level mainly affecting speed – decide based on desired difficulty progression. A common approach (from modern guideline) is to multiply base points by (Level + 1), but original NES Tetris used a level multiplier only for line clear points as shown above (which effectively is the same as Level+1 for their level count starting at 0). Choose one method and document it.
-
Combo Bonuses: Reward consecutive line clears with combo points. A combo is when the player clears lines with successive pieces, without any piece in between that doesn’t clear a line. For example, if two pieces in a row each clear at least one line, that’s a 1-combo (also described as “combo count = 1”). In many Tetris games, each combo beyond the first adds an extra 50 points * combo count * level (Combo - TetrisWiki). You can implement a similar system: start a combo counter at 0, increment it each time a piece clears any lines, and reset it when a piece is placed without clearing a line. Each time the counter increases, award bonus points (e.g., 50 × combo count × level). This means clearing lines with back-to-back pieces can significantly increase score, encouraging risky continuous clears. (You may choose a simpler combo scoring if desired, but make sure the principle of increasing reward for consecutive clears is present.)
-
Soft Drop and Hard Drop Scoring: Optionally, include a small score for using soft drop and hard drop, to incentivize faster play. A common rule is 1 point per cell for soft drop, and 2 points per cell for hard drop (Scoring in Tetris® — Tetris Mobile Help Center). This means if a piece is high up and the player hard-drops it down 15 rows, they get 30 points in addition to any line clear points. This is not a crucial gameplay element, but it is a nice touch that aligns with official scoring systems. If implemented, ensure these points are added immediately when the drop action occurs.
-
Leveling Up: Increase the game’s level as the player clears lines. A typical scheme is to start at Level 1 (or 0) and increase the level every 10 lines cleared. For example, every 10 lines, increment the level by 1. Alternatively, use the Tetris guideline approach: fixed-goal of 10 lines per level up (Tetris Guideline | Tetris Wiki | Fandom). The level influences the gravity (fall speed) – higher levels mean faster piece drop. You might define a table of fall speeds per level or a formula (e.g., Level 1 might be 1 cell per 0.8 seconds, Level 2 one cell per 0.7 seconds, etc., approaching maybe a max speed of one cell per tick at very high levels). Make sure the progression feels fair – early levels should be comfortably slow, and by Level ~10+ it becomes challenging.
-
Score Display: The game’s console output should include a status display showing the Score, Level, and Lines Cleared. Update these in real-time as the game progresses. For example, you might reserve a section of the terminal (above or beside the playfield) to show:
Level: 3 Score: 12500 Lines: 28
This gives the player constant feedback on their progress. Keep the display updated every game loop or every time a value changes (line clear, score change, or level-up). In a text-mode game, you might print this at a fixed position using terminal control codes or just redraw the screen each frame.
-
Back-to-Back (Optional): In modern Tetris, doing difficult moves back-to-back (like multiple Tetrises in a row) yields a bonus. This is an optional extension. If implemented, track if the last clear was a Tetris (or potentially a T-spin if you decide to include T-spin detection). If the player scores another Tetris without a single/double/triple in between, apply a back-to-back bonus (typically an extra 50% points for the Tetris). This can stack with combos. Since T-spins are not explicitly required by this spec, you can omit this or include it only if you add T-spin recognition.
- Top-Out Rule: The game ends when a new piece can no longer enter the playfield without collision. In practice, this means that when you try to spawn a new tetromino at the top of the playfield, its starting position is already partially occupied by existing blocks. This is the classic “block out” or top-out condition (Tetris Guideline | Tetris Wiki | Fandom). Implement this by checking at piece spawn time: if the spawn location of the piece overlaps any filled cell in the grid (or is above the top of the visible playfield), then the game is over.
- Lock Out: Another scenario for game over is if a piece locks into place such that part of it lies above the 20-row playfield. For example, if the stack is so high that even though the piece spawned, when it comes to rest it is sticking out of the top. This is effectively the same end result – the player can’t continue – so treat it as game over as well (Tetris Guideline | Tetris Wiki | Fandom).
- When game over is triggered, stop the game loop and display a “Game Over” message. Also, display the final score and maybe prompt if the player wants to play again or press a key to exit. Ensure that the terminal state is properly reset (if you changed modes for input capturing, etc.) so the user isn’t left with a broken terminal.
- After game over, if the player’s score is a new high score, make sure to update the high score file (see Persistence section) before exiting or restarting. Provide feedback like “New High Score!” if appropriate.
- High Score Saving: Maintain a record of high scores. At minimum, store the top score (and perhaps the level or lines corresponding to it, or timestamp). Each time a game ends, compare the player's score to the saved high score. If it's higher, update the record. Save this to a file on disk so that it persists between runs of the game. The file can be a simple JSON or even a plain text file. For example,
highscore.json
could store something like{ "score": 15000, "level": 5, "lines": 40 }
. If you want to keep multiple high scores, you could extend this to a list of scores or even a small leaderboard. - Settings File: Provide a configuration file for user settings. This could include:
- Rotation System: whether to use SRS or simple rotation (as mentioned, a boolean flag).
- Initial Level/Speed: allow the player to start at a higher level or set a custom gravity. This is similar to classic Tetris where you can start at level 0 or higher for more challenge.
- Key Bindings: allow remapping of controls. For example, some players might prefer
WASD
keys or different rotate/drop keys. The config could let them specify which key corresponds to left, right, rotate, soft drop, hard drop, etc. Use key codes or characters to define this. - Sound or Visual Settings: If any (though likely not, in a text game). Possibly toggling sound if you add sound support via the console beep or external sound (not required). Or toggling ghost piece (a faint preview of where the piece will land) if you implement that.
- Use a standard format (JSON, TOML, YAML, or even .ini) for the config for easy parsing. Since this is Go, JSON is easy with the
encoding/json
package, or you might use a library for TOML if you prefer a .toml file. Document the expected format with an example config file. - Config File Location: Decide where to store the config and high score files. Options:
- Current directory (simple, but if the game is run from different directories, the data might not persist where expected).
- User’s home directory (e.g.,
~/.tetrisrc
and~/.tetris_scores
). This is more consistent for user-specific config. - A dedicated config directory (e.g., on Linux,
~/.config/tetris/
). For simplicity, storing in the current working directory or alongside the game binary is fine for a small project, but document this choice.
- Default Settings: If no config file exists, start with sensible defaults (e.g., simple rotation, level 1 start, arrow keys for movement as specified, standard scoring, etc.). The game on first run can create a new config file with defaults. Provide a way (maybe a command-line flag or a menu option) to reset to defaults if needed.
- Persistent High Score Handling: Ensure file operations (read/write) have proper error handling (e.g., if the file is not found, handle it gracefully by starting fresh; if the file is corrupt or cannot be parsed, perhaps warn the user and reset it). When writing the high score, be careful to not interrupt the game if the disk write fails – you might log an error but still continue. Always close files properly to avoid data loss.
- Robust Input/Output Handling: The game should handle unexpected situations gracefully. For example, if the terminal window is resized or if an unsupported key is pressed, the game shouldn’t crash. Use Go’s error returns to catch issues when reading input or writing to the terminal. If using an external library for terminal I/O, use its functions (which often return errors) carefully – if an error is returned (say, terminal lost focus or an I/O issue), you might pause the game or attempt to reinitialize the input.
- Graceful Exit: Make sure to restore the terminal state on exit. If you put the terminal in raw mode (no echo, etc.), ensure that even if the game crashes or errors out, you capture that (maybe with
defer
to reset terminal settings) so the user isn’t stuck with a non-responsive shell. This is part of error handling because an abnormal termination could otherwise leave the console in a bad state. - Logging: Implement a basic logging mechanism. For debugging during development (or for a “debug mode”), it’s useful to have logs of game events. You can use Go’s built-in
log
package to write to a file (e.g., atetris.log
in the current directory) or tostderr
. Log key events such as piece spawns, line clears, score updates, level-ups, and any errors or unusual conditions. This will help track down issues like pieces not rotating correctly or scores miscalculating. - Debug Mode: Consider a command-line flag like
--debug
or a config setting that turns on additional debug output. In debug mode, you might draw additional info on the screen (like the bounding boxes, or the ghost piece for where it will land, or internal state info), or simply spam the log file with detailed state info each tick. This is invaluable for troubleshooting complex behaviors (for example, verifying that the lock delay reset logic works by logging the timer, or seeing the random sequence of pieces generated). Make sure debug mode is off by default so as not to overwhelm normal gameplay with unnecessary output. - Input Debugging: If players report that certain keys aren’t working (for example, some terminals might send different codes for arrows), having a debug mode that can print the key codes received can help diagnose and allow remapping.
- Error Messages: If a fatal error occurs (like failing to initialize the terminal, or config file permissions issues), print a clear error message to the console and exit rather than panicking with a stack trace. The message should guide the user or developer (“Error: Could not read config file. Using defaults.” or “Fatal: Terminal initialization failed. Ensure your terminal supports the required mode.”). This makes it easier to figure out what went wrong when running the game.
Develop a comprehensive test suite to cover the game’s functionality. Wherever possible, structure the code to separate pure logic from side effects (like rendering or OS-specific calls) so that core gameplay mechanics can be unit tested.
- Unit Tests for Game Logic: Create unit tests for the fundamental functions:
- Rotation: Given a piece in a certain position with certain surrounding blocks, test that rotating it results in the expected orientation and position (especially for SRS kicks). For example, place a T piece near a left wall and attempt a rotation; verify that with SRS enabled it “kicks” one cell to the right to fit (An Overview of Kicks in SRS | FOUR), and with simple rotation it fails to rotate (remains in original orientation). Test rotation at the bottom (floor kicks) as well.
- Movement and Collision: Test that moving a piece left/right stops at walls. Set up a scenario where blocks are at certain positions, and verify that the falling piece collides correctly and cannot move or rotate through them. Also test that soft drop moves multiple steps down until collision.
- Line Clearing: Construct scenarios of the grid where one or multiple full lines exist. Call the line-clear logic and assert that:
- The lines are removed.
- Blocks above those lines fall down the correct number of rows.
- The score increases appropriately based on how many lines were cleared.
- The line counter increases, and possibly the level increases if threshold reached. Write tests for single, double, triple, and quadruple line clears to ensure scoring and clearing behavior is correct.
- Scoring and Combos: Simulate sequences of clears to test score computation. For example, simulate a single line clear on level 1 and check that 100 points (times level multiplier if applied) were added. Then simulate a double line clear and verify 300 points added. Then simulate two line clears in a row (combo) and ensure the combo bonus was added. This can be done by calling the scoring function(s) directly with predetermined inputs (like linesCleared=2, comboCount=1, backToBack=false, level=3, etc.) and checking the returned score.
- Level Progression: Simulate clearing lines across level boundaries. For instance, if 10 lines = level up, simulate clearing 9 lines (no level up yet), then clear 1 line and verify that level increased by 1 and gravity speed changed. Also test multiple level increments (like clearing 20 lines quickly should increase two levels).
- Lock Delay Behavior: This one is trickier to unit test without a real time component, but you can abstract the lock delay into a function that gets called each tick with whether the piece is currently on the ground or not. Then simulate a sequence: piece touches ground at time=0 -> timer starts, at tick X simulate a rotate that lifts it off ground -> timer resets, etc., and verify that if no reset happens within the threshold the function would signal “lock now”. Essentially, test that the lock delay timer resets appropriately on movements and triggers lock after the correct duration of inactivity.
- Integration / Functional Tests: For aspects that involve interaction of multiple parts (or those that depend on real timing), consider an integration test or a controlled simulation:
- You can simulate a full small game by feeding a predetermined sequence of inputs to the game loop (this might require making the game loop able to accept an input script). For example, drop a piece in a known pattern and ensure after a series of moves a line clears and the game state (grid, score, level) matches expected values.
- Test the game over condition by programmatically filling the board to just below top and then dropping a piece that will cause top-out. Verify that the game sets a Game Over state.
- If possible, automate running the game in a headless mode (without real user input) for a few pieces to ensure no runtime errors happen. This could be done by abstracting input source to feed in a series of moves.
- Input Handling Tests: Testing actual keyboard inputs in an automated way can be tricky. Instead, refactor input handling so that it can be abstracted (e.g., an interface that yields “commands” or key presses). Then in tests, substitute a mock implementation that returns a sequence of desired inputs. This way you can simulate “Left, Left, Rotate, Down” and then check the piece’s position and orientation in the game state after those commands.
- File I/O Tests: Write tests for the config and high score persistence:
- Start with no config file (perhaps in a temp directory) and run the config load function – ensure it loads defaults.
- Then write a sample config file, run load, and verify that the in-game settings match what was in the file (e.g., if the config set leftKey to 'A', verify the game now interprets 'A' as left).
- For high score, simulate end-of-game scenarios: call a function to record high score with a given score, and ensure the file is created or updated with that score. Then simulate another game with a lower score and ensure the file remains with the higher score (if you keep only one). If you keep a list of high scores, test insertion in the correct order.
- Edge Case Tests:
- Spawn in Filled Row: Ensure that if the spawn location has filled blocks (which should only happen if the stack is extremely high), the game over triggers. You can simulate this by setting up the board state (grid) with blocks up to row 20 and then calling the spawn piece function in a test – it should detect the collision and return a game over condition.
- Rotation at Edges: Test rotating each piece in the corners of the playfield. E.g., put a J, L, or T piece at the extreme left column and try rotating (with SRS on, it should kick; with SRS off, it should fail). Similarly, test an I piece rotation at the wall (the I piece has special kicks in SRS – ensure they are correct).
- Maximum Combo: Simulate a long combo (if your logic allows, e.g., clear lines 5 pieces in a row) and verify the combo counter resets properly at a miss and that the scoring adds up for each step.
- Fast Inputs: Some players might press multiple keys quickly or at the same time. While real concurrency of inputs is not an issue (keyboard events are sequential), test scenarios like pressing rotate and right move in the same tick – ensure the game can handle it (maybe one will be processed this tick, one next tick). If using an input buffer or reading all keys each frame, ensure that combined actions don’t break anything (for example, rotating a piece at the exact moment it contacts ground – does lock delay still apply? It should).
- Testability Considerations: To facilitate the above tests, structure your code with separation of concerns:
- Model: core data structures like the grid, the piece (with its shape and position), score, etc. Provide methods to manipulate these (move, rotate, clear lines, etc.) that contain the logic, separate from input or output.
- Controller/Gameplay: the game loop that ties input, model, and output together. This part is harder to unit test, but if model is solid, this can be minimal.
- For unit testing, you can instantiate a Game state and call model methods directly, bypassing the real-time loop and actual keypresses. This makes tests deterministic and fast.
- Automated Testing: Use Go’s testing framework (
go test
) to automate running your tests. Aim for high coverage on the critical logic. Testing the actual rendering on the console is not necessary (and not feasible in an automated way), but you can test that the strings or grid representations you generate are correct. For example, a function that returns a string of the playfield (for drawing) could be tested to ensure it correctly represents a given grid state.
- Building: Ensure the project is
go build
-able on all major platforms. Include ago.mod
file for dependency management so that runninggo build
orgo install
will fetch any needed libraries automatically. The output should be a single binary (e.g.,tetris.exe
on Windows or justtetris
on Unix). There should be no additional files required to run (aside from config/high score which will be generated or optional). - Running: Document in the README how to run the game. Typically:
- After building, the user can open a terminal and run
./tetris
to start the game. - If there are command-line flags (for example,
--srs
to use SRS rotation,--debug
for debug mode, etc.), list them and perhaps provide a-h/--help
output in the program to show usage. - Mention any expectations (like “runs in a 80x24 terminal; if your terminal is smaller, the playfield might not fit on screen”). For text mode, 80x24 is standard, and our playfield plus some info should fit, but if you added a lot of UI might need slightly bigger.
- After building, the user can open a terminal and run
- Terminal Compatibility: The game should run in a standard console/terminal. It’s good to test on a few: Windows Command Prompt or PowerShell, a Linux terminal (xterm/GNOME Terminal), etc., especially if using escape codes or special libraries. Libraries like termbox or tcell handle a lot of this for you. If using termbox, note that it doesn’t work in certain environments (like the GoLand IDE console, as noted in some references) – but that’s okay as long as it works in a normal terminal. Document any such limitations.
- Installation: If publishing open-source, users should be able to install via
go install
(e.g.,go install github.com/yourname/tetris@latest
). This will fetch the source and build it. Provide the module path and usage in documentation. Alternatively, provide pre-built binaries for convenience. - Minimal Dependencies: Since this is a minimal console game, the only external dependency might be the terminal handling library. Make sure to list this in your documentation (e.g., “Uses termbox-go for cross-platform console graphics”). No need for heavy GUI or graphics libraries. This keeps the binary size small and avoids complex licensing issues.
- Cross-Compilation: With Go, it’s easy to cross-compile. Consider testing a build for Windows, Mac, Linux (if you’re not developing on all of those) to ensure nothing OS-specific is broken. Terminal handling libraries usually support all, but for example, termbox might not support older Windows terminals well – just be aware. If any special steps are needed for Windows (like enabling ANSI mode), mention them.
- Distribution: The game being a single binary means it can be distributed by simply providing that binary. In your repository or deliverable, include instructions on where to download or how to build. Also, include the source code (since this is a developer specification, the assumption is the developer will have the source).
- Post-Deployment: Provide user documentation (even if brief) – possibly as a README or as in-game instructions. For example, when the game launches, you could show a title screen with controls summary and how to start (or just start immediately and let the player figure out, but at least document in README). This ensures that once deployed, players (or testers) know how to actually use the product.
By following this specification, a Go developer should be able to implement a classic Tetris game that runs in the console with smooth gameplay, accurate Tetris mechanics, and a robust structure. The focus is on clarity, maintainability, and an authentic Tetris feel, while also providing enough flexibility (via configuration and debug modes) to extend or tweak the game in the future.