fishing mechanic in ssh and fixed rotating host key
This commit is contained in:
+191
-59
@@ -6,8 +6,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
"charm.land/bubbles/v2/spinner"
|
"charm.land/bubbles/v2/spinner"
|
||||||
@@ -132,7 +134,7 @@ func main() {
|
|||||||
// The SSH server need its own keys, this will create a keypair in the
|
// The SSH server need its own keys, this will create a keypair in the
|
||||||
// given path if it doesn't exist yet.
|
// given path if it doesn't exist yet.
|
||||||
// By default, it will create an ED25519 key.
|
// By default, it will create an ED25519 key.
|
||||||
wish.WithHostKeyPath(".ssh/id_ed25519"),
|
wish.WithHostKeyPath("/data/.ssh/ssh_host_ed25519_key"),
|
||||||
|
|
||||||
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||||
angler, err := db.GetAnglerByKey(key)
|
angler, err := db.GetAnglerByKey(key)
|
||||||
@@ -207,6 +209,15 @@ const (
|
|||||||
PageGameplay
|
PageGameplay
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type playerState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerIdle playerState = iota
|
||||||
|
PlayerCasting
|
||||||
|
PlayerCaught
|
||||||
|
PlayerFailed
|
||||||
|
)
|
||||||
|
|
||||||
type tab int
|
type tab int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -217,21 +228,25 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
profile string
|
ctx context.Context
|
||||||
term string
|
cancel context.CancelFunc
|
||||||
width int
|
profile string
|
||||||
height int
|
term string
|
||||||
bg string
|
width int
|
||||||
DB *db.Database
|
height int
|
||||||
page page
|
bg string
|
||||||
angler models.Angler
|
DB *db.Database
|
||||||
tabs []string
|
page page
|
||||||
tab tab
|
angler models.Angler
|
||||||
error string
|
tabs []string
|
||||||
ti textinput.Model
|
tab tab
|
||||||
spinner spinner.Model
|
error string
|
||||||
active tea.Model
|
ti textinput.Model
|
||||||
cast models.Cast
|
spinner spinner.Model
|
||||||
|
active tea.Model
|
||||||
|
cast models.Cast
|
||||||
|
playerState playerState
|
||||||
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
@@ -251,6 +266,8 @@ func (m model) Init() tea.Cmd {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chooseNamePrompt = "aye fresh meat, got a name laddie?"
|
||||||
|
|
||||||
func (m model) ChooseNameView() tea.View {
|
func (m model) ChooseNameView() tea.View {
|
||||||
var error string
|
var error string
|
||||||
|
|
||||||
@@ -258,7 +275,9 @@ func (m model) ChooseNameView() tea.View {
|
|||||||
error = lipgloss.NewStyle().Foreground(lipgloss.Red).Render(m.error)
|
error = lipgloss.NewStyle().Foreground(lipgloss.Red).Render(m.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
input := lipgloss.JoinVertical(lipgloss.Top, "aye fresh meat, got a name laddie?", m.ti.View(), error)
|
// m.ti.SetWidth(lipgloss.Width(chooseNamePrompt))
|
||||||
|
|
||||||
|
input := lipgloss.JoinVertical(lipgloss.Top, chooseNamePrompt, m.ti.View(), error)
|
||||||
|
|
||||||
center := lipgloss.Place(
|
center := lipgloss.Place(
|
||||||
m.width,
|
m.width,
|
||||||
@@ -284,15 +303,15 @@ func (m model) ChooseNameView() tea.View {
|
|||||||
bottom,
|
bottom,
|
||||||
)
|
)
|
||||||
|
|
||||||
var c *tea.Cursor
|
// var c *tea.Cursor
|
||||||
|
|
||||||
c = m.ti.Cursor()
|
// c = m.ti.Cursor()
|
||||||
|
|
||||||
c.Y += lipgloss.Height("aye fresh meat, got a name laddie?") + (m.height-3-lipgloss.Height(input))/2
|
// c.Y += lipgloss.Height(prompt) + (m.height-3-lipgloss.Height(input))/2
|
||||||
c.X += (m.width - lipgloss.Width(input)) / 2
|
// c.X += (m.width - lipgloss.Width(input)) / 2
|
||||||
|
|
||||||
v := tea.NewView(str)
|
v := tea.NewView(str)
|
||||||
v.Cursor = c
|
// v.Cursor = c
|
||||||
v.AltScreen = true
|
v.AltScreen = true
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@@ -343,9 +362,45 @@ func (m model) ChooseNameUpdate(msg tea.Msg) (model, tea.Cmd) {
|
|||||||
var buttonStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Background(lipgloss.Color("8"))
|
var buttonStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Background(lipgloss.Color("8"))
|
||||||
var buttonActiveStyle = buttonStyle.Background(lipgloss.Color("30"))
|
var buttonActiveStyle = buttonStyle.Background(lipgloss.Color("30"))
|
||||||
|
|
||||||
|
var keybindKeyStyle = lipgloss.NewStyle()
|
||||||
|
var keybindActionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||||
|
|
||||||
var sidebarBtnStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Foreground(lipgloss.Color("30"))
|
var sidebarBtnStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Foreground(lipgloss.Color("30"))
|
||||||
var sidebarBtnActiveStyle = sidebarBtnStyle.UnsetForeground().Background(lipgloss.Color("30"))
|
var sidebarBtnActiveStyle = sidebarBtnStyle.UnsetForeground().Background(lipgloss.Color("30"))
|
||||||
|
|
||||||
|
func (m model) GameplayFishingView(width int, height int) string {
|
||||||
|
help := keybindKeyStyle.Render("c") + keybindActionStyle.Render(" cast") + "\t" + keybindKeyStyle.Render("x") + keybindActionStyle.Render(" cancel")
|
||||||
|
|
||||||
|
switch m.playerState {
|
||||||
|
case PlayerIdle:
|
||||||
|
return lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
lipgloss.NewStyle().MarginBottom(1).Render(fmt.Sprintf("Aye matie %s...time to get fishin'", m.angler.Username)),
|
||||||
|
help,
|
||||||
|
)
|
||||||
|
case PlayerCasting:
|
||||||
|
return lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
lipgloss.NewStyle().MarginBottom(1).Render(m.spinner.View()+" waitin' for a bite"),
|
||||||
|
help,
|
||||||
|
)
|
||||||
|
case PlayerCaught:
|
||||||
|
return lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
lipgloss.NewStyle().MarginBottom(1).Render(m.message),
|
||||||
|
help,
|
||||||
|
)
|
||||||
|
case PlayerFailed:
|
||||||
|
return lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
lipgloss.NewStyle().MarginBottom(1).Foreground(lipgloss.Red).Render(m.message),
|
||||||
|
help,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "bad player state"
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) GameplayView() tea.View {
|
func (m model) GameplayView() tea.View {
|
||||||
|
|
||||||
var renderedTabs []string
|
var renderedTabs []string
|
||||||
@@ -365,28 +420,33 @@ func (m model) GameplayView() tea.View {
|
|||||||
|
|
||||||
paddingY, paddingX := 1, 2
|
paddingY, paddingX := 1, 2
|
||||||
|
|
||||||
content := lipgloss.NewStyle().
|
maxWidth := min(m.width, 80)
|
||||||
Width(m.width-paddingX*2).
|
maxHeight := min(m.height, 80)
|
||||||
Height(m.height-2-paddingY*2).
|
|
||||||
|
var content string
|
||||||
|
|
||||||
|
if m.tab == TabFishin {
|
||||||
|
content = m.GameplayFishingView(maxWidth, maxHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped := lipgloss.NewStyle().
|
||||||
|
Width(maxWidth-paddingX*2).
|
||||||
|
Height(maxHeight-2-paddingY*2).
|
||||||
BorderStyle(lipgloss.NormalBorder()).
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
BorderForeground(lipgloss.Color("30")).
|
BorderForeground(lipgloss.Color("30")).
|
||||||
Padding(paddingY, paddingX).
|
Padding(paddingY, paddingX).
|
||||||
Render(lipgloss.JoinVertical(
|
Render(content)
|
||||||
lipgloss.Left,
|
|
||||||
lipgloss.NewStyle().MarginBottom(1).Render(fmt.Sprintf("Aye matie %s...time to get fishin'", m.angler.Username)),
|
|
||||||
buttonActiveStyle.Render("Cast"),
|
|
||||||
))
|
|
||||||
|
|
||||||
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
||||||
|
|
||||||
box := lipgloss.NewStyle().
|
box := lipgloss.NewStyle().
|
||||||
Width(m.width).
|
Width(maxWidth).
|
||||||
Height(m.height).
|
Height(maxHeight).
|
||||||
Padding(paddingY, paddingX).
|
Padding(paddingY, paddingX).
|
||||||
Render(lipgloss.JoinVertical(
|
Render(lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
tabs,
|
tabs,
|
||||||
content,
|
wrapped,
|
||||||
footer,
|
footer,
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -414,6 +474,8 @@ func (m model) GameplayView() tea.View {
|
|||||||
type GameplayKeyMap struct {
|
type GameplayKeyMap struct {
|
||||||
CycleTab key.Binding
|
CycleTab key.Binding
|
||||||
AltCycleTab key.Binding
|
AltCycleTab key.Binding
|
||||||
|
Cast key.Binding
|
||||||
|
Cancel key.Binding
|
||||||
Interact key.Binding
|
Interact key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +488,14 @@ var DefaultGameplayKeyMap = GameplayKeyMap{
|
|||||||
key.WithKeys("shift+tab"),
|
key.WithKeys("shift+tab"),
|
||||||
key.WithHelp("shift+tab", "Cycle Tab Left"),
|
key.WithHelp("shift+tab", "Cycle Tab Left"),
|
||||||
),
|
),
|
||||||
|
Cast: key.NewBinding(
|
||||||
|
key.WithKeys("c"),
|
||||||
|
key.WithHelp("c", "Cast"),
|
||||||
|
),
|
||||||
|
Cancel: key.NewBinding(
|
||||||
|
key.WithKeys("x"),
|
||||||
|
key.WithHelp("x", "Cancel action."),
|
||||||
|
),
|
||||||
Interact: key.NewBinding(
|
Interact: key.NewBinding(
|
||||||
key.WithKeys("enter"),
|
key.WithKeys("enter"),
|
||||||
key.WithHelp("enter", "Interact with selected option"),
|
key.WithHelp("enter", "Interact with selected option"),
|
||||||
@@ -464,6 +534,35 @@ var sayings = []string{
|
|||||||
"Catching the wind in a net",
|
"Catching the wind in a net",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type castFinishedMsg struct {
|
||||||
|
caught bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForCast(
|
||||||
|
ctx context.Context,
|
||||||
|
wait time.Duration,
|
||||||
|
) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
timer := time.NewTimer(wait)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
caught := rand.IntN(100) < 75
|
||||||
|
|
||||||
|
return castFinishedMsg{
|
||||||
|
caught: caught,
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return castFinishedMsg{
|
||||||
|
err: ctx.Err(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
@@ -486,26 +585,53 @@ func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
|||||||
} else {
|
} else {
|
||||||
m.tab -= 1
|
m.tab -= 1
|
||||||
}
|
}
|
||||||
case key.Matches(msg, DefaultGameplayKeyMap.Interact):
|
case key.Matches(msg, DefaultGameplayKeyMap.Cast):
|
||||||
|
m.playerState = PlayerCasting
|
||||||
|
|
||||||
// if m.active = {
|
// if m.active = {
|
||||||
is_casting, err := m.DB.IsCasting(m.angler.ID)
|
is_casting, err := m.DB.IsCasting(m.angler.ID)
|
||||||
|
|
||||||
if is_casting {
|
if is_casting {
|
||||||
|
m.playerState = PlayerIdle
|
||||||
m.error = "looks like you are already casting somewhere"
|
m.error = "looks like you are already casting somewhere"
|
||||||
// TODO: clear after a timeout
|
// TODO: clear after a timeout
|
||||||
|
} else {
|
||||||
|
m.cast, err = m.DB.CreateNewCast(m.angler.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
m.playerState = PlayerIdle
|
||||||
|
m.error = "unable to start your cast"
|
||||||
|
log.Print(err)
|
||||||
|
} else {
|
||||||
|
// Fishing can take anywhere from 2-5 seconds
|
||||||
|
minTime, maxTime := 2, 5
|
||||||
|
timeToWait := time.Duration(rand.IntN(maxTime-minTime+1)+minTime) * time.Second
|
||||||
|
|
||||||
|
cmds = append(cmds, waitForCast(m.ctx, timeToWait), m.spinner.Tick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.cast, err = m.DB.CreateNewCast(m.angler.ID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
m.error = "unable to start your cast"
|
|
||||||
// TODO: handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement rest of casting logic
|
|
||||||
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
case castFinishedMsg:
|
||||||
|
_, err := m.DB.EndCastByID(m.cast.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
m.error = "unable to end your cast"
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.err != nil {
|
||||||
|
m.error = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.caught {
|
||||||
|
m.playerState = PlayerCaught
|
||||||
|
m.message = "Got one! Nice!"
|
||||||
|
} else {
|
||||||
|
m.playerState = PlayerFailed
|
||||||
|
m.message = sayings[rand.IntN(len(sayings))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -515,7 +641,7 @@ func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
|||||||
|
|
||||||
func (m model) gotoGameplayView() (model, tea.Cmd) {
|
func (m model) gotoGameplayView() (model, tea.Cmd) {
|
||||||
m.page = PageGameplay
|
m.page = PageGameplay
|
||||||
return m, m.spinner.Tick
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlobalKeyMap struct {
|
type GlobalKeyMap struct {
|
||||||
@@ -579,19 +705,25 @@ func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOpti
|
|||||||
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||||
pty, _, _ := s.Pty()
|
pty, _, _ := s.Pty()
|
||||||
angler, _ := s.Context().Value("angler").(models.Angler)
|
angler, _ := s.Context().Value("angler").(models.Angler)
|
||||||
|
ctx, cancel := context.WithCancel(s.Context())
|
||||||
|
|
||||||
m := model{
|
m := model{
|
||||||
term: pty.Term,
|
// using ssh context for now
|
||||||
width: pty.Window.Width,
|
// TODO: may make sens to use a custom one
|
||||||
height: pty.Window.Height,
|
ctx: ctx,
|
||||||
bg: "light",
|
cancel: cancel,
|
||||||
DB: DB,
|
term: pty.Term,
|
||||||
page: PageChooseName,
|
width: pty.Window.Width,
|
||||||
angler: angler,
|
height: pty.Window.Height,
|
||||||
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
bg: "light",
|
||||||
tab: TabFishin,
|
DB: DB,
|
||||||
ti: textinput.New(),
|
page: PageChooseName,
|
||||||
spinner: spinner.New(),
|
angler: angler,
|
||||||
|
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
||||||
|
tab: TabFishin,
|
||||||
|
ti: textinput.New(),
|
||||||
|
spinner: spinner.New(),
|
||||||
|
playerState: PlayerIdle,
|
||||||
}
|
}
|
||||||
|
|
||||||
if angler.Username == gossh.FingerprintSHA256(s.PublicKey()) {
|
if angler.Username == gossh.FingerprintSHA256(s.PublicKey()) {
|
||||||
@@ -603,10 +735,10 @@ func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOpti
|
|||||||
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("30"))
|
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("30"))
|
||||||
m.spinner.Spinner = spinner.Jump
|
m.spinner.Spinner = spinner.Jump
|
||||||
|
|
||||||
m.ti.SetVirtualCursor(false)
|
// m.ti.SetVirtualCursor(false)
|
||||||
|
m.ti.SetWidth(lipgloss.Width(chooseNamePrompt) - 3)
|
||||||
m.ti.Focus()
|
m.ti.Focus()
|
||||||
m.ti.CharLimit = 156
|
m.ti.CharLimit = 156
|
||||||
m.ti.SetWidth(20)
|
|
||||||
|
|
||||||
return m, []tea.ProgramOption{}
|
return m, []tea.ProgramOption{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ services:
|
|||||||
COOKIE_STORE_SECRET: ${COOKIE_STORE_SECRET}
|
COOKIE_STORE_SECRET: ${COOKIE_STORE_SECRET}
|
||||||
DB_CONNECTION_STRING: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
DB_CONNECTION_STRING: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
ENV: ${ENV}
|
ENV: ${ENV}
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
ports:
|
ports:
|
||||||
- "22:22"
|
- "22:22"
|
||||||
- "9900:9900"
|
- "9900:9900"
|
||||||
|
|||||||
Reference in New Issue
Block a user