Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test build

on:
pull_request:
branches:
- main

jobs:
main:
name: Build and run
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Run tests
run: |
go test ./...

- name: Check if ortotris binary builds
run: |
cd cmd/ortotris
go build .

- name: Check if lettersnake binary builds
run: |
cd cmd/lettersnake
go build .
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ go.work.sum
# env file
.env

/cmd/example_app/example_app
/cmd/ortotris/ortotris
/cmd/lettersnake/lettersnake
68 changes: 57 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,65 @@
# termui
[![Go Reference](https://pkg.go.dev/badge/github.com/keenbytes/termui.svg)](https://pkg.go.dev/github.com/keenbytes/termui) [![Go Report Card](https://goreportcard.com/badge/github.com/keenbytes/termui)](https://goreportcard.com/report/github.com/keenbytes/termui)
# CLI Games monorepo

![termui](termui.png "termui")
This repository contains small termina- based games designed for my kids.

The `termui` package is designed to simplify output to a terminal window by allowing the specification of panes with static or dynamic content. These panes, defined by either vertical or horizontal splits, structure the terminal window. The main pane, which represents the entire terminal window, can be split into additional panes, which in turn can be further subdivided, much like the functionality found in the popular tool, tmux.
## Lettersnake

Pane sizes can be specified either as a percentage or by a fixed number of characters. The content within a pane can be dynamic, sourced from an attached function (referred to as a Widget in the example code below).
The concept is that you get a word in one language and you need to collect letters to form its translation in another.

Panes can also feature borders, which are customisable by defining the characters to be used for each side (e.g., left edge, top-left corner, top bar, etc.).
See screenshot below:

The package utilises ANSI escape codes and has been tested on macOS and Linux.
![Lettersnake](screenshot.png)

This library is a refactored version of my older library called `terminal-ui` which
will be deprecated soon.
### Running

### Example
To play the game just run:

Please navigate to `cmd` directory to check example applications.
go run *.go start -f words-pl-en-animals.txt

### Instructions
Use arrows to steer the snake.

### Words file
The words for the game are provided via the `-f` argument, and the file's format is straightforward.

First line is the title of the list. And starting second one,
every line contains a word in one language and its translation in another (which needs to be guessed). Words are delimetered by a colon (`:`).
Space cannot be used, so an underscore (`_`) is preffered.

For example, a sample file might look like this:

Polish-English Places
hol:hall
garaż:garage
jadalnia:dining_room


## Ortotris

It is inspired by the classic DOS game Ortotris, released in 1992, which was similar to Tetris but focused on improving spelling skills. The game runs in the terminal and follows a similar concept to help players with orthography.

See screenshot below:

![Ortotris](screenshot.png)

### Running

To play the game just run:

go run *.go start -f words-u-o.txt

### Instructions
Words descend from the top of the screen, similar to Tetris, but with one or two missing letters, indicated by an underscore (_). Use the left and right arrow keys to select one of the available letters before the word reaches the bottom. If an incorrect letter is chosen, the word will remain at the bottom. You can also press the down arrow to drop the word immediately.

### Words file
The words for the game are provided via the `-f` argument, and the file's format is straightforward.

The first line is a title of the dictionary file.
The second line specifies two or more letters that the player will choose between, separated by a colon (`:`). The following lines contain the words, which will be shuffled during the game. Each line includes two values, also separated by a colon. The first value is the word, with the missing letter(s) represented by an underscore (`_`), and the second value is the correct answer.

For example, a sample file might look like this:

Words with "u" or "ó"
u:ó
r_ża:ó
mal_je:u
81 changes: 0 additions & 81 deletions cmd/example_app/main.go

This file was deleted.

14 changes: 0 additions & 14 deletions cmd/example_app/widget_time.go

This file was deleted.

25 changes: 25 additions & 0 deletions cmd/lettersnake/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BSD 2-Clause License

Copyright (c) 2024, 2025 Mikolaj Gasior <m@gasior.dev>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

31 changes: 31 additions & 0 deletions cmd/lettersnake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Lettersnake

This project is a small, terminal-based snake game designed for my kids. The concept is that you get a word in one language and you need to collect letters to form its translation in another.

See screenshot below:

![Lettersnake](screenshot.png)

### Running

To play the game just run:

go run *.go start -f words-pl-en-animals.txt

### Instructions
Use arrows to steer the snake.

### Words file
The words for the game are provided via the `-f` argument, and the file's format is straightforward.

First line is the title of the list. And starting second one,
every line contains a word in one language and its translation in another (which needs to be guessed). Words are delimetered by a colon (`:`).
Space cannot be used, so an underscore (`_`) is preffered.

For example, a sample file might look like this:

Polish-English Places
hol:hall
garaż:garage
jadalnia:dining_room

112 changes: 112 additions & 0 deletions cmd/lettersnake/game_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package main

import (
"context"
"os"
"sync"

"github.com/keenbytes/cli-games/pkg/lettersnake"
"github.com/keenbytes/cli-games/pkg/termui"
)

type gameInterface struct {
tui *termui.TermUI
game *termui.Pane
score *termui.Pane
leftWord *termui.Pane
rightWord *termui.Pane
g *lettersnake.Game
}

func newGameInterface(g *lettersnake.Game, speed int) *gameInterface {
gui := &gameInterface{}

gui.g = g
gui.tui = termui.NewTermUI()
mainPane := gui.tui.Pane()

paneScore, _bottom := mainPane.Split(termui.Horizontally, termui.Left, 3, termui.Char)
paneGame, _bottomBottom := _bottom.Split(termui.Horizontally, termui.Right, 3, termui.Char)

paneLeftWord, paneRightWord := _bottomBottom.Split(termui.Vertically, termui.Right, 50, termui.Percent)

paneScore.Widget = &scorePane{g: g}
paneLeftWord.Widget = &leftWordPane{g: g}
paneRightWord.Widget = &rightWordPane{g: g}
paneGame.Widget = &gamePane{g: g, pane: paneGame, speed: speed}

gui.game = paneGame
gui.score = paneScore
gui.leftWord = paneLeftWord
gui.rightWord = paneRightWord

gui.tui.SetFrame(&termui.Frame{}, paneGame, paneScore, paneLeftWord, paneRightWord)

return gui
}

func (gui *gameInterface) run(ctx context.Context, cancel func()) {
wg := sync.WaitGroup{}
wg.Add(2)

stopStdio := false

go func() {
gui.tui.Run(ctx, os.Stdout, os.Stderr)
wg.Done()
stopStdio = true
}()

go func() {
var b []byte = make([]byte, 1)
for {
if stopStdio {
break
}
os.Stdin.Read(b)
// key press code here
if string(b) == "x" {
cancel()
break
}
if string(b) == "s" {
if gui.g.State() != lettersnake.GameOn {
gui.g.StartGame()
}
continue
}
// TODO: Keys should be handled differently, maybe in raw mode
// left arrow pressed
if string(b) == "D" {
if gui.g.Direction() != lettersnake.Right {
gui.g.SetDirection(lettersnake.Left)
}
continue
}
// right arrow pressed
if string(b) == "C" {
if gui.g.Direction() != lettersnake.Left {
gui.g.SetDirection(lettersnake.Right)
}
continue
}
// down arrow pressed
if string(b) == "B" {
if gui.g.Direction() != lettersnake.Up {
gui.g.SetDirection(lettersnake.Down)
}
continue
}
// up arrow pressed
if string(b) == "A" {
if gui.g.Direction() != lettersnake.Down {
gui.g.SetDirection(lettersnake.Up)
}
continue
}
}
wg.Done()
}()

wg.Wait()
}
17 changes: 17 additions & 0 deletions cmd/lettersnake/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"strings"

"github.com/keenbytes/cli-games/pkg/termui"
)

func clearPane(pane *termui.Pane) {
for y := 0; y < pane.CanvasHeight(); y++ {
clearPaneLine(pane, y)
}
}

func clearPaneLine(pane *termui.Pane, y int) {
pane.Write(0, y, strings.Repeat(" ", pane.CanvasWidth()))
}
Loading