Skip to content

Commit

Permalink
feat: implement ultra game mode (#10)
Browse files Browse the repository at this point in the history
* refactor: use KVPs for horizontal picker component; add ultra to mode picker

* refactor: move subcommands

* refactor: move shared code

* refactor: rename marathon package to single

* docs(readme): amend docs to use new play subcommand

* fix: update play subcommand game modes

* chore: duplicate marathon tui code for ultra

* refactor: rename to gameStopwatch

* feat: implement ultra game mode

* feat: combine ultra & marathon models into 'single' model

* chore: fix lint

* fix: scoring

* refactor: move from uint to int

I was using uint to enforce values being positive, but this is not a
good use case for the type. It also overcomplicates things due to so
much casting between uint and int. Instead I will validate the Scoring
type, which is the container of the strictly positive integers.

* feat: validate scoring type on creation

* chore: fix lint

* test: fix scoring test
  • Loading branch information
Broderick-Westrope authored Aug 21, 2024
1 parent d99f482 commit 0b78a5a
Show file tree
Hide file tree
Showing 25 changed files with 598 additions and 370 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ You're also able to start the game directly in a game mode (eg. Marathon), skipp

```bash
# Start the game in Marathon mode with a level of 5 and the player name "Brodie"
./tetrigo marathon --level=5 --name=Brodie
./tetrigo play marathon --level=5 --name=Brodie
```

To see more options for starting the game you can run:
Expand Down
62 changes: 0 additions & 62 deletions cmd/tetrigo/main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
package main

import (
"fmt"

"github.com/Broderick-Westrope/tetrigo/internal/config"
"github.com/Broderick-Westrope/tetrigo/internal/data"
"github.com/Broderick-Westrope/tetrigo/internal/tui/common"
"github.com/Broderick-Westrope/tetrigo/internal/tui/starter"
"github.com/alecthomas/kong"

tea "github.com/charmbracelet/bubbletea"
)

type CLI struct {
Expand All @@ -25,35 +17,6 @@ type GlobalVars struct {
DB string `help:"Path to database file" default:"tetrigo.db"`
}

type MenuCmd struct{}

func (c *MenuCmd) Run(globals *GlobalVars) error {
return launchStarter(globals, common.ModeMenu, common.NewMenuInput())
}

type PlayCmd struct {
GameMode string `arg:"" help:"Game mode to play" default:"marathon"`
Level uint `help:"Level to start at" short:"l" default:"1"`
Name string `help:"Name of the player" short:"n" default:"Anonymous"`
}

func (c *PlayCmd) Run(globals *GlobalVars) error {
switch c.GameMode {
case "marathon":
return launchStarter(globals, common.ModeMarathon, common.NewMarathonInput(c.Level, c.Name))
default:
return fmt.Errorf("invalid game mode: %s", c.GameMode)
}
}

type LeaderboardCmd struct {
GameMode string `arg:"" help:"Game mode to display" default:"marathon"`
}

func (c *LeaderboardCmd) Run(globals *GlobalVars) error {
return launchStarter(globals, common.ModeLeaderboard, common.NewLeaderboardInput(c.GameMode))
}

func main() {
cli := CLI{}
ctx := kong.Parse(&cli,
Expand All @@ -65,28 +28,3 @@ func main() {
err := ctx.Run(&cli.GlobalVars)
ctx.FatalIfErrorf(err)
}

func launchStarter(globals *GlobalVars, starterMode common.Mode, switchIn common.SwitchModeInput) error {
db, err := data.NewDB(globals.DB)
if err != nil {
return fmt.Errorf("error opening database: %w", err)
}

cfg, err := config.GetConfig(globals.Config)
if err != nil {
return fmt.Errorf("error getting config: %w", err)
}

model, err := starter.NewModel(
starter.NewInput(starterMode, switchIn, db, cfg),
)
if err != nil {
return fmt.Errorf("error creating starter model: %w", err)
}

if _, err = tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil {
return fmt.Errorf("error running tea program: %w", err)
}

return nil
}
67 changes: 67 additions & 0 deletions cmd/tetrigo/subcommands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"fmt"

"github.com/Broderick-Westrope/tetrigo/internal/config"
"github.com/Broderick-Westrope/tetrigo/internal/data"
"github.com/Broderick-Westrope/tetrigo/internal/tui/common"
"github.com/Broderick-Westrope/tetrigo/internal/tui/starter"
tea "github.com/charmbracelet/bubbletea"
)

type MenuCmd struct{}

func (c *MenuCmd) Run(globals *GlobalVars) error {
return launchStarter(globals, common.ModeMenu, common.NewMenuInput())
}

type PlayCmd struct {
GameMode string `arg:"" help:"Game mode to play" default:"marathon"`
Level int `help:"Level to start at" short:"l" default:"1"`
Name string `help:"Name of the player" short:"n" default:"Anonymous"`
}

func (c *PlayCmd) Run(globals *GlobalVars) error {
switch c.GameMode {
case "marathon":
return launchStarter(globals, common.ModeUltra, common.NewSingleInput(common.ModeMarathon, c.Level, c.Name))
case "ultra":
return launchStarter(globals, common.ModeUltra, common.NewSingleInput(common.ModeUltra, c.Level, c.Name))
default:
return fmt.Errorf("invalid game mode: %s", c.GameMode)
}
}

type LeaderboardCmd struct {
GameMode string `arg:"" help:"Game mode to display" default:"marathon"`
}

func (c *LeaderboardCmd) Run(globals *GlobalVars) error {
return launchStarter(globals, common.ModeLeaderboard, common.NewLeaderboardInput(c.GameMode))
}

func launchStarter(globals *GlobalVars, starterMode common.Mode, switchIn common.SwitchModeInput) error {
db, err := data.NewDB(globals.DB)
if err != nil {
return fmt.Errorf("error opening database: %w", err)
}

cfg, err := config.GetConfig(globals.Config)
if err != nil {
return fmt.Errorf("error getting config: %w", err)
}

model, err := starter.NewModel(
starter.NewInput(starterMode, switchIn, db, cfg),
)
if err != nil {
return fmt.Errorf("error creating starter model: %w", err)
}

if _, err = tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil {
return fmt.Errorf("error running tea program: %w", err)
}

return nil
}
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Config struct {
LockDownMode string `toml:"lock_down_mode"`

// The maximum level to reach before the game ends or the level stops increasing.
MaxLevel uint `toml:"max_level"`
MaxLevel int `toml:"max_level"`

// Whether the game ends when the max level is reached.
EndOnMaxLevel bool `toml:"end_on_max_level"`
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ type Model struct {
// cursor is the index of the currently selected option.
selected int
// options is a list of the possible options for this component.
options []string
options []KeyValuePair
// keymap encodes the keybindings recognized by the component.
keymap *KeyMap
styles Styles
}

type KeyValuePair struct {
Key string
Value any
}

type Option func(*Model)

func NewModel(options []string, opts ...Option) *Model {
func NewModel(options []KeyValuePair, opts ...Option) *Model {
m := &Model{
options: options,
keymap: defaultKeyMap(),
Expand All @@ -39,9 +44,10 @@ func NewModel(options []string, opts ...Option) *Model {

func WithRange(minValue, maxValue int) Option {
return func(m *Model) {
m.options = make([]string, (maxValue-minValue)+1)
m.options = make([]KeyValuePair, (maxValue-minValue)+1)
for i := minValue - 1; i < maxValue; i++ {
m.options[i] = strconv.Itoa(i + 1)
m.options[i].Key = strconv.Itoa(i + 1)
m.options[i].Value = i + 1
}
}
}
Expand Down Expand Up @@ -82,7 +88,7 @@ func (m *Model) View() string {

return lipgloss.JoinHorizontal(lipgloss.Center,
prev.Render(m.styles.PrevIndicator),
m.styles.SelectionStyle.Render(m.options[m.selected]),
m.styles.SelectionStyle.Render(m.options[m.selected].Key),
next.Render(m.styles.NextIndicator),
)
}
Expand Down Expand Up @@ -111,6 +117,6 @@ func (m *Model) isLast() bool {
return m.selected == len(m.options)-1
}

func (m *Model) GetSelection() string {
func (m *Model) GetSelection() KeyValuePair {
return m.options[m.selected]
}
File renamed without changes.
20 changes: 18 additions & 2 deletions internal/tui/common/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package common
import "github.com/Broderick-Westrope/tetrigo/internal/data"

type MarathonInput struct {
Level uint
Level int
PlayerName string
}

func NewMarathonInput(level uint, playerName string) *MarathonInput {
func NewMarathonInput(level int, playerName string) *MarathonInput {
return &MarathonInput{
Level: level,
PlayerName: playerName,
Expand All @@ -16,6 +16,22 @@ func NewMarathonInput(level uint, playerName string) *MarathonInput {

func (in *MarathonInput) isSwitchModeInput() {}

type SingleInput struct {
Mode Mode
Level int
PlayerName string
}

func NewSingleInput(mode Mode, level int, playerName string) *SingleInput {
return &SingleInput{
Mode: mode,
Level: level,
PlayerName: playerName,
}
}

func (in *SingleInput) isSwitchModeInput() {}

type MenuInput struct {
}

Expand Down
18 changes: 0 additions & 18 deletions internal/tui/common/keys.go

This file was deleted.

28 changes: 20 additions & 8 deletions internal/tui/common/mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
)

type Mode int

const (
ModeMenu = Mode(iota)
ModeMarathon
ModeLeaderboard
)

type SwitchModeMsg struct {
Target Mode
Input SwitchModeInput
Expand All @@ -29,3 +21,23 @@ func SwitchModeCmd(target Mode, in SwitchModeInput) tea.Cmd {
}
}
}

type Mode int

const (
ModeMenu = Mode(iota)
ModeMarathon
ModeUltra
ModeLeaderboard
)

var modeToStrMap = map[Mode]string{
ModeMenu: "Menu",
ModeMarathon: "Marathon",
ModeUltra: "Ultra",
ModeLeaderboard: "Leaderboard",
}

func (m Mode) String() string {
return modeToStrMap[m]
}
33 changes: 29 additions & 4 deletions internal/tui/common/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,35 @@ import (
"github.com/muesli/termenv"
)

const (
pausedMsg = ` ____ __
/ __ \____ ___ __________ ____/ /
/ /_/ / __ ^/ / / / ___/ _ \/ __ /
/ ____/ /_/ / /_/ (__ ) __/ /_/ /
/_/ \__,_/\__,_/____/\___/\__,_/
Press PAUSE to continue or HOLD to exit.`

gameOverMsg = ` ______ ____
/ ____/___ _____ ___ ___ / __ \_ _____ _____
/ / __/ __ ^/ __ ^__ \/ _ \ / / / / | / / _ \/ ___/
/ /_/ / /_/ / / / / / / __/ / /_/ /| |/ / __/ /
\____/\__,_/_/ /_/ /_/\___/ \____/ |___/\___/_/
Press EXIT or HOLD to continue.`
)

// Most of this code is borrowed from
// https://github.com/charmbracelet/lipgloss/pull/102
// as well as the lipgloss library.

func OverlayPausedMessage(bg string) string {
return placeOverlayCenter(pausedMsg, bg)
}

func OverlayGameOverMessage(bg string) string {
return placeOverlayCenter(gameOverMsg, bg)
}

// Split a string into lines, additionally returning the size of the widest
// line.
func getLines(s string) ([]string, int) {
Expand All @@ -31,14 +56,14 @@ func getLines(s string) ([]string, int) {
return lines, widest
}

func PlaceOverlayCenter(fg, bg string, opts ...WhitespaceOption) string {
func placeOverlayCenter(fg, bg string, opts ...WhitespaceOption) string {
x := lipgloss.Width(bg) / 2
y := lipgloss.Height(bg) / 2
return PlaceOverlay(x, y, fg, bg, opts...)
return placeOverlay(x, y, fg, bg, opts...)
}

// PlaceOverlay places fg on top of bg.
func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
// placeOverlay places fg on top of bg.
func placeOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines)
Expand Down
Loading

0 comments on commit 0b78a5a

Please sign in to comment.