diff options
| author | citrons <citrons@mondecitronne.com> | 2025-01-29 21:57:36 -0600 |
|---|---|---|
| committer | citrons <citrons@mondecitronne.com> | 2025-01-29 21:57:36 -0600 |
| commit | dcc8a3a4f93eb5b7dcc965e63ad2ea6057a8ee6b (patch) | |
| tree | 02d7fa114a86322d92322533b2ee70874f01b121 | |
| parent | e3a5cdb759ba98ebcdf43e38efaa9d33aa29f466 (diff) | |
keyboard and mouse handling
| -rw-r--r-- | football/main.go | 237 | ||||
| -rw-r--r-- | football/platitudes.go | 81 | ||||
| -rw-r--r-- | tui/event.go | 136 | ||||
| -rw-r--r-- | tui/input.go | 57 |
4 files changed, 405 insertions, 106 deletions
diff --git a/football/main.go b/football/main.go index a6cd954..f6332c1 100644 --- a/football/main.go +++ b/football/main.go @@ -2,9 +2,13 @@ package main import ( "citrons.xyz/talk/tui" + "zgo.at/termfo/keys" "math/rand" "log" + "os" "strings" + "strconv" + "fmt" "time" ) @@ -13,14 +17,20 @@ type ball struct { y int vx int vy int + explode int inGoal bool } -func main() { - tui.Start() +var username = os.Getenv("USER") + +var score struct { + home int + guest int +} +func main() { s := tui.Size() - var balls [20]ball + var balls [50]ball for i := 0; i < len(balls); i++ { balls[i].x = rand.Int() % s.Width - 1 balls[i].y = rand.Int() % s.Height @@ -36,69 +46,198 @@ func main() { } } - var sb strings.Builder + if username == "" { + username = "GUEST" + } + username = string([]rune(username)[:10]) + + tui.Start() + + var ( + sb strings.Builder + response string + lastKey keys.Key + mouseButton int + mouseHeld bool + scrolled int + responsesUnlocked = 5 + ) + t := time.After(time.Second / 15) for { - tui.Clear() - s := tui.Size() - for y := 0; y < s.Height; y++ { - for x := 0; x < s.Width; x++ { - if (x + 66) % 10 == 1 && (y + 2) % 5 == 1 { - tui.Write(x, y, "*", tui.Style {Fg: tui.Green}) + select { + case ev := <-tui.Events(): + if ev.Err != nil { + log.Fatal(ev.Err) + } + if ev.Key == keys.Escape || ev.Key == 'c' | keys.Ctrl { + tui.End() + return + } + if ev.Key == keys.Backspace { + sb.Reset() + } + if ev.Key == keys.Enter && len(sb.String()) > 0 { + sb.Reset() + var i int + if responsesUnlocked < 40 { + i = rand.Int() % responsesUnlocked + } else { + i = 30 + rand.Int() % (responsesUnlocked - 30) } + response = platitudes[i] + if responsesUnlocked < len(platitudes) && rand.Int() % 3 == 0 { + if responsesUnlocked < 12 || score.guest > 0 { + responsesUnlocked++ + } + } + } + if ev.TextInput != 0 { + sb.WriteRune(ev.TextInput) + } + if ev.Mouse.Pressed { + mouseHeld = true + mouseButton = ev.Mouse.Button + fire(balls[:], ev.Mouse.X, ev.Mouse.Y) + } else if ev.Mouse.Released { + mouseHeld = false + } + scrolled += ev.Mouse.Scroll + if ev.Key != 0 { + lastKey = ev.Key } + case <-t: + stepFootball(balls[:]) + t = time.After(time.Second / 15) } - tui.Write(2, 1, "normal", tui.Style {Fg: tui.Red}) - tui.Write(2, 2, "bold", tui.Style {Fg: tui.Yellow, Bold: true}) - tui.Write(2, 3, "italic", tui.Style {Fg: tui.Green, Italic: true}) - tui.Write(2, 4, "underline", tui.Style {Fg: tui.Blue, Underline: true}) - tui.Write(2, 5, "strikethrough", tui.Style {Fg: tui.Magenta, Strikethrough: true}) - tui.Write(2, 6, "hooray!", tui.Style {Fg: tui.White, Bold: true, Italic: true, Underline: true, Strikethrough: true}) - gx := s.Width / 2 - 1 - for i := 0; i < len(balls); i++ { - if balls[i].inGoal { - continue + if score.home == len(balls) { + response = "wow, you did very poorly." + } + + tui.Clear() + drawFootball(balls[:]) + tui.Write(1, 9, " DIALOG:", tui.Style {Fg: tui.Red, Bold: true}) + tui.Write(1, 10, " ", tui.DefaultStyle) + w := tui.Write(2, 10, sb.String(), tui.DefaultStyle) + tui.MoveCursor(2 + w, 10) + tui.Write(1, 11, " RESPONSE:", tui.Style {Fg: tui.Green, Bold: true}) + tui.Write(1, 12, " ", tui.DefaultStyle) + tui.Write(2, 12, response, tui.DefaultStyle) + + tui.Write(1, 14, " TIP: click the mouse", tui.Style {Fg: tui.Yellow, Bold: true}) + + tui.Write(1, 15, " ", tui.DefaultStyle) + tui.Write(2, 15, lastKey.Name(), tui.DefaultStyle) + tui.Write(11, 15, " ", tui.DefaultStyle) + tui.Write(12, 15, strconv.FormatInt(int64(rune(lastKey)), 16), tui.DefaultStyle) + + if mouseHeld { + tui.Write(1, 18, " ", tui.DefaultStyle) + tui.Write(2, 18, strconv.Itoa(mouseButton), tui.DefaultStyle) + } + tui.Write(1, 19, " ", tui.DefaultStyle) + tui.Write(2, 19, strconv.Itoa(scrolled), tui.DefaultStyle) + + s := tui.Size() + tui.Write(s.Width - 26, 1, " ", tui.DefaultStyle) + tui.Write(s.Width - 25, 1, "HOME", tui.Style {Bg: tui.Green, Bold: true}) + hs := fmt.Sprintf(" %02d", score.home) + tui.Write(s.Width - 26, 2, hs, tui.Style {Fg: tui.Yellow, Bold: true}) + + tui.Write(s.Width - 16, 1, " ", tui.DefaultStyle) + tui.Write(s.Width - 15, 1, username, tui.Style {Bg: tui.Red, Bold: true}) + gs := fmt.Sprintf(" %02d", score.guest) + tui.Write(s.Width - 16, 2, gs, tui.Style {Fg: tui.Yellow, Bold: true}) + + if err := tui.Present(); err != nil { + log.Fatal(err) + } + } +} + +func stepFootball(balls []ball) { + s := tui.Size() + gx := s.Width / 2 - 1 + for i := 0; i < len(balls); i++ { + if balls[i].inGoal { + continue + } + if balls[i].explode > 0 { + balls[i].explode-- + if balls[i].explode == 0 { + balls[i].inGoal = true } - balls[i].x += balls[i].vx - balls[i].y += balls[i].vy - if balls[i].x < 0 { - balls[i].x = 0 - balls[i].vx = -balls[i].vx - } else if balls[i].x >= s.Width - 1 { + continue + } + + balls[i].x += balls[i].vx + balls[i].y += balls[i].vy + if balls[i].x < 0 { + balls[i].x = 0 + balls[i].vx = -balls[i].vx + } else if balls[i].x >= s.Width - 1 { + if balls[i].x - s.Width < 2 { balls[i].x = s.Width - 2 balls[i].vx = -balls[i].vx + } else { + balls[i].x = rand.Int() % s.Width } - if balls[i].y < 0 { - balls[i].y = 0 - balls[i].vy = -balls[i].vy - } else if balls[i].y >= s.Height { + } + if balls[i].y < 0 { + balls[i].y = 0 + balls[i].vy = -balls[i].vy + } else if balls[i].y >= s.Height { + if balls[i].y - s.Height < 2 { balls[i].y = s.Height - 1 balls[i].vy = -balls[i].vy + } else { + balls[i].y = rand.Int() % s.Height } - if balls[i].x >= gx - 1 && balls[i].x <= gx + 1 && balls[i].y == 0 { - balls[i].inGoal = true - continue - } - tui.Write(balls[i].x, balls[i].y, "⚽", tui.DefaultStyle) } - tui.Write(gx, 0, "🥅", tui.DefaultStyle) + if balls[i].x >= gx - 1 && balls[i].x <= gx + 1 && balls[i].y == 0 { + balls[i].inGoal = true + score.home++ + continue + } + } +} - w := tui.Write(2, 8, sb.String(), tui.DefaultStyle) - tui.MoveCursor(2 + w, 8) - err := tui.Present() - if err != nil { - log.Fatal(err) +func fire(balls []ball, x int, y int) { + for i := 0; i < len(balls); i++ { + if balls[i].explode > 0 || balls[i].inGoal { + continue } + if x >= balls[i].x - 2 && x <= balls[i].x + 3 && + y >= balls[i].y - 1 && y <= balls[i].y + 1 { + balls[i].explode = 5 + score.guest++ + } + } +} - select { - case ev := <-tui.Events(): - switch ev := ev.(type) { - case tui.TextInput: - sb.WriteRune(rune(ev)) - case error: - log.Fatal(ev) +func drawFootball(balls []ball) { + s := tui.Size() + for y := 0; y < s.Height; y++ { + for x := 0; x < s.Width; x++ { + if (x + 66) % 10 == 1 && (y + 2) % 5 == 1 { + tui.Write(x, y, "*", tui.Style {Fg: tui.Green}) } - case <-time.After(time.Second / 16): } } + tui.Write(2, 1, "normal", tui.Style {Fg: tui.Red}) + tui.Write(2, 2, "bold", tui.Style {Fg: tui.Yellow, Bold: true}) + tui.Write(2, 3, "italic", tui.Style {Fg: tui.Green, Italic: true}) + tui.Write(2, 4, "underline", tui.Style {Fg: tui.Blue, Underline: true}) + tui.Write(2, 5, "strikethrough", tui.Style {Fg: tui.Magenta, Strikethrough: true}) + tui.Write(2, 6, "hooray!", tui.Style {Fg: tui.White, Bold: true, Italic: true, Underline: true, Strikethrough: true}) + + for _, b := range balls { + if b.explode > 0 { + tui.Write(b.x, b.y, "💥", tui.DefaultStyle) + } else if !b.inGoal { + tui.Write(b.x, b.y, "⚽", tui.DefaultStyle) + } + } + gx := s.Width / 2 - 1 + tui.Write(gx, 0, "🥅", tui.DefaultStyle) } diff --git a/football/platitudes.go b/football/platitudes.go new file mode 100644 index 0000000..c7bfd33 --- /dev/null +++ b/football/platitudes.go @@ -0,0 +1,81 @@ +package main + +var platitudes = []string { + "yep", + "ok", + "ok then", + "yeah", + "uh huh", + "that's a funny thing to say.", + "uh huh", + "sure", + "yeah", + "yeah", + "alright.", + "cool, I guess", + "I wish the referee would stop you from exploding the Footballs. it's against the rules.", + "no", + "nope", + "no!", + "maybe", + "that's fine and all, but can you stop exploding my balls?", + "that's fine and all, but can you stop exploding my balls?", + "that's fine and all, but can you stop exploding my balls?", + "that's fine and all, but can you stop exploding my balls?", + "that's fine and all, but can you stop exploding my balls?", + "is that the best you can do?", + "I think you should focus on the game.", + "you're not focusing on the game enough!!", + "please, let's focus on the game.", + "we can talk another time.", + "can we please talk another time?", + "is now really the best time to talk?", + "why would you say that?", + "could you be any more of an asshole?", + "wipe that smirk off your ugly face.", + "it's taking every ounce of willpower in my body not to punch you right now.", + "to be honest, I don't even like Football.", + "I think you should respect Football more.", + "there is nothing in my life other than Football.", + "I don't even know what that means.", + "there's nothing I love more than Football.", + "I wish it wasn't against the rules of Football to kill you.", + "never talk to me again.", + "I hate you.", + "I really hate you.", + "I really, really hate you.", + "go to hell.", + "fuck you", + "can we be friends?", + "can we be friends?", + "can we be friends?", + "can we be friends?", + "I love you.", + "do you want to come to my place?", + "will you marry me?", + "I need you in my life.", + "I need you badly.", + "will you kiss me?", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please make out with me.", + "please, please, please.", +} + diff --git a/tui/event.go b/tui/event.go new file mode 100644 index 0000000..4d0a535 --- /dev/null +++ b/tui/event.go @@ -0,0 +1,136 @@ +package tui + +import ( + "os" + "os/signal" + "zgo.at/termfo" + "zgo.at/termfo/keys" + "strings" + "strconv" + "bufio" + "syscall" + "sync" +) + +type Event struct { + TextInput rune + Key keys.Key + Mouse struct { + Button int + Pressed bool + Released bool + X, Y int + Scroll int + } + Err error + Resize bool +} + +var evChan chan Event +var once sync.Once +func Events() <-chan Event { + once.Do(func() { + evChan = make(chan Event, 2) + + // buffer FindKeys + ch := make(chan termfo.Event, 16) + go func() { + r := bufio.NewReader(os.Stdin) + for e := range terminfo.FindKeys(r) { + ch <- e + if e.Err != nil { + return + } + } + }() + + go func() { + for kev := range ch { + var ev Event + if kev.Err != nil { + ev.Err = kev.Err + evChan <- ev + return + } + + noMods := kev.Key & (keys.Ctrl | keys.Alt) == 0 + notSpecial := kev.Key.WithoutMods() < (1 << 32) + if noMods && notSpecial { + r := rune(kev.Key.WithoutMods()) + if kev.Key & keys.Shift != 0 && r >= 'a' && r <= 'z' { + r -= 0x20 + } + ev.TextInput = r + } + + switch kev.Key { + case keys.Mouse: + if err := parseMouse(&ev, ch); err != nil { + ev.Err = err + evChan <- ev + return + } + default: + ev.Key = kev.Key + } + + evChan <- ev + } + }() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGWINCH) + go func() { + for _ = range sigs { + evChan <- Event {Resize: true} + } + }() + }) + + return evChan +} + +func parseMouse(ev *Event, ch <-chan termfo.Event) error { + var ( + arg strings.Builder + args []int + down bool + ) + parse: for kev := range ch { + if kev.Err != nil { + return kev.Err + } + switch { + case kev.Key >= '0' && kev.Key <= '9': + arg.WriteRune(rune(kev.Key)) + case kev.Key == ';': + i, _ := strconv.Atoi(arg.String()) + args = append(args, i) + arg.Reset() + case kev.Key == 'm' | keys.Shift: + down = true + fallthrough + default: + i, _ := strconv.Atoi(arg.String()) + args = append(args, i) + break parse + } + } + if len(args) != 3 { + return nil + } + ev.Mouse.X = args[1] - 1 + ev.Mouse.Y = args[2] - 1 + switch args[0] { + case 64: + ev.Mouse.Scroll = -1 + case 65: + ev.Mouse.Scroll = 1 + default: + ev.Mouse.Button = args[0] + ev.Mouse.Pressed = down + ev.Mouse.Released = !down + } + + return nil +} diff --git a/tui/input.go b/tui/input.go deleted file mode 100644 index a200e53..0000000 --- a/tui/input.go +++ /dev/null @@ -1,57 +0,0 @@ -package tui - -import ( - "os" - "os/signal" - "golang.org/x/term" - "unicode" - "syscall" - "bufio" -) - -type Event interface {} - -type TextInput rune -type Paste string -type Resize Dims - -var evChan chan Event -func Events() <-chan Event { - if evChan != nil { - return evChan - } - evChan = make(chan Event, 1) - - go func() { - rd := bufio.NewReader(os.Stdin) - for { - r, _, err := rd.ReadRune() - if err != nil { - evChan <- err - return - } - switch { - case r == '\033': - // todo - case unicode.IsControl(r): - default: - evChan <- TextInput(r) - } - } - }() - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGWINCH) - go func() { - for _ = range sigs { - w, h, err := term.GetSize(0) - if err == nil { - evChan <- Resize {w, h} - } else { - evChan <- err - } - } - }() - - return evChan -}
\ No newline at end of file |
