Repository: ozankasikci/vim-man Branch: master Commit: 378b6360890e Files: 23 Total size: 50.8 KB Directory structure: gitextract_a3ra5gw2/ ├── .gitignore ├── LICENSE ├── README.md ├── canvas.go ├── canvas_test.go ├── cmd/ │ └── console/ │ └── vimman.go ├── docs/ │ └── LEVEL_PLANNING.md ├── entity.go ├── game.go ├── go.mod ├── go.sum ├── level.go ├── level1_basic_movement.go ├── level2_exiting_vim.go ├── level3_text_editing.go ├── level4_bomberman.go ├── logger.go ├── stage.go ├── termbox_cell.go ├── tilemap.go ├── user.go ├── utils.go └── word.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea .DS_Store # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ logfile.txt .swp ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Ozan Kaşıkçı Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # VimMan Learn how to use Vim in its natural environment, the Terminal! ## About VimMan is terminal program that's a semi editor and a semi game. The purpose of VimMan is to teach people how to use vim and have fun at the same time! ## Installation `git clone https://github.com/ozankasikci/vim-man && cd vim-man` `go run cmd/console/vimman.go` to start from a specific level; `LEVEL=2 go run cmd/console/vimman.go` ## Demo ### Level - 1 - Basic movement in Normal Mode ![](https://raw.githubusercontent.com/ozankasikci/ozankasikci.github.io/master/gifs/fantasia-level-1.gif) ### Level - 2 - How to exit Vim ![](https://raw.githubusercontent.com/ozankasikci/ozankasikci.github.io/master/gifs/fantasia-level-2.gif) ### Level - 3 - Basic text editing ![](https://raw.githubusercontent.com/ozankasikci/ozankasikci.github.io/master/gifs/fantasia-level-3.gif) ### Level - 4 - Vimberman! ![](https://raw.githubusercontent.com/ozankasikci/ozankasikci.github.io/master/gifs/fantasia-level-4.gif) ## TODO * Add missing levels - [ ] File save - [ ] Deletion commands level (`dw`, `d$` vs) - [ ] Operators and motions - [ ] Using count - [ ] Operating on lines - [ ] Undo & redo - [ ] Put, replace, change operators - [ ] Search & substitute - [ ] Accessing Shell * Handle edge cases in some levels - [ ] level 3 ================================================ FILE: canvas.go ================================================ package vimman import "os" //import tb "github.com/nsf/termbox-go" type Canvas [][]*TermBoxCell func NewCanvas(width, height int) Canvas { canvas := make(Canvas, height) for i := range canvas { canvas[i] = make([]*TermBoxCell, width) } return canvas } func (c Canvas) GetCellAt(x, y int) *TermBoxCell { return c[y][x] } func (c Canvas) CheckCollision(x, y int) bool { // check if out of boundaries if x < 0 || y < 0 || y >= len(c) || x >= len(c[0]) { return true } if c[y][x] == nil { return true } if c[y][x].cellData.CollisionCallback != nil { c[y][x].cellData.CollisionCallback() } if os.Getenv("DEBUG") == "1" { return false } return c[y][x].collidesPhysically } func (c *Canvas) OverWriteCanvasCell(x, y int, termboxCell *TermBoxCell) { if x >= 0 && x < len((*c)[0]) && y >= 0 && y < len((*c)) { // intentionally use x,y in reverse order (*c)[y][x] = termboxCell } } func (c *Canvas) SetCellAt(row, column int, cell *TermBoxCell) { (*c)[row][column] = cell } func (c *Canvas) IsInsideOfBoundaries(x, y int) bool { return x >= 0 && x < len((*c)[0])-1 && y >= 0 && y < len(*c)-1 } func (c *Canvas) IsInLastColumn(x int) bool { return len((*c)[0])-1 == x } ================================================ FILE: canvas_test.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "github.com/stretchr/testify/assert" "testing" ) func TestCanvasCheckCollision(t *testing.T) { x, y := 10, 10 c := NewCanvas(x, y) c[1][1] = &TermBoxCell{&termbox.Cell{}, false, nil} c[0][0] = &TermBoxCell{&termbox.Cell{}, true, nil} tt := []struct { x int y int expect bool }{ {-1, 0, true}, {0, -1, true}, {1, 1, false}, {0, 0, true}, {x, 0, true}, {0, y, true}, } for _, value := range tt { res := c.CheckCollision(value.x, value.y) assert.Equal(t, value.expect, res) } c[2][2] = nil assert.True(t, c.CheckCollision(2, 2)) } ================================================ FILE: cmd/console/vimman.go ================================================ package main import ( "github.com/ozankasikci/vim-man" "os" "strconv" ) func main() { level := os.Getenv("LEVEL") levelInt, err := strconv.ParseInt(level, 10, 16) if err != nil { levelInt = 1 } vimman.Init(int(levelInt)) } ================================================ FILE: docs/LEVEL_PLANNING.md ================================================ # Vim-Man Level Planning ## Level 1: Basic Movement * **Vim Concepts:** `h` (left), `j` (down), `k` (up), `l` (right). * **Gameplay:** A simple maze. Player learns to navigate Vim-Man using the basic movement keys to reach an exit tile. Introduce the concept of "Vim" as the source of power/commands. * **Visuals:** Clean, straightforward. Maybe a terminal-like aesthetic. * **Objective:** Reach the exit. ## Level 2: Exiting Vim (The First Challenge) * **Vim Concepts:** `:` (command mode), `q` (quit), `!` (force - for `q!`). * **Gameplay:** Player reaches what seems like an exit, but it's blocked. A hint appears: "Type :q to exit". If the player has "unsaved changes" (e.g., collected an item they weren't supposed to, or stepped on a 'dirty' tile), `:q` fails. They then learn `:q!` to force quit (exit the level). * **Visuals:** The exit could be a flashing cursor or a representation of a closing window. "Dirty" tiles could have a distinct visual. * **Objective:** Successfully exit the level using the correct quit command. ## Level 3: Basic Text Editing (Deletion) * **Vim Concepts:** `x` (delete character under cursor), `dw` (delete word - maybe simplified to just deleting a 'word' entity). * **Gameplay:** The path is blocked by "error characters" (like `x` tiles) or "bug words" (small enemy-like entities). Player must use `x` to delete single error characters or `dw` (or a simplified version) to remove "bug words" to clear the path. * **Visuals:** "Error characters" could be red `x`'s. "Bug words" could be small, distinct sprites. * **Objective:** Clear the path and reach the exit. ## Level 4: The Bomberman (Introduction to Insert Mode & Esc) * **Vim Concepts:** `i` (insert mode), `Esc` (return to normal mode), placing "bombs" (characters) in insert mode. * **Gameplay:** Inspired by Bomberman. Player can enter insert mode with `i`. While in insert mode, pressing a movement key (or a specific "bomb" key) places a "character bomb" that explodes after a short delay, clearing destructible blocks. Player must use `Esc` to return to normal mode to move safely. * **Visuals:** Destructible blocks, bomb sprites, explosion effects. * **Objective:** Clear a path through destructible blocks and reach the exit. ## Level 5: The Word Jumper's Gauntlet * **Vim Concepts:** `w` (next word), `b` (previous word), `e` (end of word), `ge` (end of previous word). * **Gameplay:** The level is a series of platforms (words) separated by gaps. Player must use `w`, `b`, `e`, `ge` to jump precisely between platforms. Some platforms might be crumbling, requiring quick successive jumps. Enemies could patrol on longer "sentence" platforms. * **Visuals:** Platforms clearly look like "words". Gaps are obvious. Crumbling platforms could animate. * **Objective:** Navigate the platforms and reach the exit. ## Level 6: The Repetition Realm (Numeric Precision) * **Vim Concepts:** Using numbers with motion/commands (e.g., `3w`, `5j`, `2dd`). * **Gameplay:** Paths are blocked by gates requiring a specific number of actions. For example, a gate "3" high requires `3j` to pass if it's a vertical jump, or defeating an enemy might take `2x` (two delete actions). Collectibles could be arranged in patterns that are easiest to get with numbered movements. * **Visuals:** Gates could display the number required. Collectibles in clear numerical patterns. * **Objective:** Use numbered commands to overcome obstacles and reach the exit. ## Level 7: The Search & Find Expedition * **Vim Concepts:** `/` (search forward), `?` (search backward), `n` (next occurrence), `N` (previous occurrence). * **Gameplay:** Key items or "rescue targets" (e.g., specific characters) are hidden across a maze-like map. Using `/` or `?` followed by the target character would highlight the path or the item. Enemies might change characters, forcing the player to re-search. * **Visuals:** Maze environment. Searched items/paths could glow or have a special indicator. * **Objective:** Find the target(s) using search commands and reach the exit. ## Level 8: The Copy-Paste Assembly Line * **Vim Concepts:** `y` (yank/copy), `p` (paste after), `P` (paste before), Visual mode (`v` character-wise, `V` line-wise) for selecting text. * **Gameplay:** The player needs to construct a specific "word" or "code snippet" by yanking parts from different areas of the level and pasting them into a designated "build zone." Visual mode could be used to select multi-character "components." * **Visuals:** Distinct "source" areas for yanking. A clear "build zone" where pasting occurs. Visual feedback for selection in Visual mode. * **Objective:** Assemble the target word/snippet and complete the level. ================================================ FILE: entity.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "time" ) type Direction int const ( horizontal Direction = iota vertical ) type Entity struct { Stage *Stage Position Point Width int Height int Rune rune Cell *TermBoxCell Cells []*TermBoxCell DrawPriority int Tags []Tag InitCallback func() } type EntityOptions struct { DrawPriority int Tags []Tag InitCallback func() } type Tag struct { Name string } func NewEntity(s *Stage, x, y, w, h int, r rune, fg termbox.Attribute, bg termbox.Attribute, cells []*TermBoxCell, collidesPhysically bool, options EntityOptions) *Entity { drawPriority, tags, initCallback := options.DrawPriority, options.Tags, options.InitCallback p := Point{x, y} cell := &TermBoxCell{&termbox.Cell{r, fg, bg}, collidesPhysically, TileMapCellData{}} return &Entity{s, p, w, h, r, cell, cells, drawPriority, tags, initCallback} } func (e *Entity) SetStage(s *Stage) { e.Stage = s } func (e *Entity) GetStage() *Stage { return e.Stage } func (e *Entity) SetCells(s *Stage) { newPositionY := e.Position.y for i := 0; i < e.Height; i++ { newPositionX := e.Position.x if i != 0 { newPositionY += 1 } for j := 0; j < e.Width; j++ { if j != 0 { newPositionX += 1 } if e.Cells != nil { index := j tileMapCell := e.Cells[index] if len(e.Cells) > index { s.Canvas.OverWriteCanvasCell(newPositionX, newPositionY, tileMapCell) } } else { tileMapCell := e.Cell s.Canvas.OverWriteCanvasCell(newPositionX, newPositionY, tileMapCell) } } } } func (e *Entity) GetCells() []*TermBoxCell { return e.Cells } func (e *Entity) Update(s *Stage, event termbox.Event, time time.Duration) { } func (e *Entity) SetPosition(x, y int) { e.SetPositionX(x) e.SetPositionY(y) } func (e *Entity) SetPositionX(x int) { e.Position.x = x } func (e *Entity) SetPositionY(y int) { e.Position.y = y } func (e *Entity) GetPositionX() int { return e.Position.x } func (e *Entity) GetPositionY() int { return e.Position.y } func (e *Entity) GetPosition() (int, int) { return e.Position.x, e.Position.y } func (e *Entity) GetScreenOffset() (int, int) { screenWidth, screenHeight := e.Stage.Game.getScreenSize() return (screenWidth - e.Width) / 2, (screenHeight - e.Height) / 2 } func (e *Entity) Destroy() { } func (e *Entity) GetDrawPriority() int { return e.DrawPriority } func (e *Entity) GetTags() []Tag { return e.Tags } func (e *Entity) IsInsideOfCanvasBoundaries() bool { return e.GetStage().Canvas.IsInsideOfBoundaries(e.GetPositionX(), e.GetPositionY()) } ================================================ FILE: game.go ================================================ package vimman import ( "time" "github.com/nsf/termbox-go" "github.com/pkg/errors" ) const ( bgColor = termbox.ColorBlack fgColor = termbox.ColorWhite ) type Point struct { x int y int } type Game struct { Stage *Stage screenSizeX int screenSizeY int VimManEvents chan VimManEvent } type GameOptions struct { fps float64 initialLevel int bgCell *termbox.Cell VimManEvents chan VimManEvent } type VimManEvent struct { Content string } func NewGame(opts GameOptions) *Game { bgCell := &termbox.Cell{'░', fgColor, bgColor} game := &Game{nil, 0, 0, opts.VimManEvents} stage := NewStage(game, opts.initialLevel, opts.fps, bgCell) game.Stage = stage return game } type Renderer interface { Update(*Stage, termbox.Event, time.Duration) Destroy() SetCells(*Stage) GetCells() []*TermBoxCell GetPosition() (int, int) GetPositionX() int GetPositionY() int SetPositionX(int) GetScreenOffset() (int, int) GetDrawPriority() int GetTags() []Tag ShouldCenterHorizontally() bool } // main game loop // handles events, updates and renders stage and entities func gameLoop(termboxEvents chan termbox.Event, vimManEvents chan VimManEvent, game *Game) { termbox.Clear(fgColor, bgColor) game.setScreenSize(termbox.Size()) stage := game.Stage stage.Init() stage.Render() lastUpdateTime := time.Now() for { termbox.Clear(fgColor, bgColor) update := time.Now() select { case event := <-termboxEvents: switch { case event.Key == termbox.KeyCtrlC: // exit on ctrc + c return case event.Type == termbox.EventResize: game.setScreenSize(termbox.Size()) default: stage.update(event, update.Sub(lastUpdateTime)) } default: stage.update(termbox.Event{}, update.Sub(lastUpdateTime)) } // handle vim-man events here select { case event := <-vimManEvents: switch { case event.Content == "exit": return } default: } lastUpdateTime = time.Now() stage.Render() time.Sleep(time.Duration((update.Sub(time.Now()).Seconds()*1000.0)+1000.0/stage.Fps) * time.Millisecond) } } func termboxEventLoop(e chan termbox.Event) { for { e <- termbox.PollEvent() } } func exit(events chan termbox.Event) { close(events) termbox.Close() } func Init(level int) { if err := termbox.Init(); err != nil { panic(errors.Wrap(err, "failed to init termbox")) } if level == 0 { level = 1 } termbox.SetOutputMode(termbox.Output256) termbox.Clear(termbox.ColorDefault, bgColor) termboxEvents := make(chan termbox.Event) go termboxEventLoop(termboxEvents) vimManEvents := make(chan VimManEvent) game := NewGame(GameOptions{ fps: 50, initialLevel: level, VimManEvents: vimManEvents, }) // main game loop, this is blocking gameLoop(termboxEvents, vimManEvents, game) // dump logs after the gameLoop stops if len(lg.logs) > 0 { lg.DumpLogs() time.Sleep(2 * time.Second) } exit(termboxEvents) } func (g *Game) setScreenSize(x, y int) { if x > 0 { g.screenSizeX = x } if y > 0 { g.screenSizeY = y } } func (g *Game) getScreenSize() (int, int) { return g.getScreenSizeX(), g.getScreenSizeY() } func (g *Game) getScreenSizeX() int { return g.screenSizeX } func (g *Game) getScreenSizeY() int { return g.screenSizeY } ================================================ FILE: go.mod ================================================ module github.com/ozankasikci/vim-man go 1.13 require ( github.com/mattn/go-runewidth v0.0.8 // indirect github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.4.0 ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 h1:OkWEy7aQeQTbgdrcGi9bifx+Y6bMM7ae7y42hDFaBvA= github.com/nsf/termbox-go v0.0.0-20191229070316-58d4fcbce2a7/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= ================================================ FILE: level.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "reflect" "time" ) type VimMode int const ( normalMode VimMode = iota insertMode colonMode ) func (m VimMode) String() string { return [...]string{"NORMAL", "INSERT", "NORMAL"}[m] } const ( levelTitleCoordX int = 0 levelTitleCoordY int = 1 levelTitleFg termbox.Attribute = termbox.ColorGreen levelTitleBg termbox.Attribute = termbox.ColorBlack levelExplanationCoordX int = 0 levelExplanationCoordY int = 2 levelHintCoordX int = 0 levelHintCoordY int = 3 typedCharacterFg termbox.Attribute = termbox.ColorWhite typedCharacterBg termbox.Attribute = termbox.ColorBlack ) type Level struct { Game *Game VimMode VimMode TileMapString string TileMap [][]*TermBoxCell TileData TileMapCellDataMap Entities []Renderer InputRunes []rune BlockedKeys []termbox.Key InputBlocked bool TextShiftingDisabled bool BgCell *termbox.Cell Width int Height int Init func() ColonLineCallbacks map[string]func(game *Game) } func (l *Level) Update(s *Stage, t time.Duration) { } func (l *Level) SetCells(s *Stage) { } func (l *Level) GetSize() (int, int) { index, length := 0, 0 for i, line := range l.TileMap { if len(line) > length { index, length = i, len(line) } } return len(l.TileMap[index]), len(l.TileMap) } func (l *Level) GetScreenOffset() (int, int) { offsetX, offsetY := 0, 0 screenWidth, screenHeight := l.Game.getScreenSize() levelWidth, levelHeight := l.GetSize() if screenWidth > levelWidth { offsetX = (screenWidth - levelWidth) / 2 } if screenHeight > levelHeight { offsetY = (screenHeight - levelHeight) / 2 } return offsetX, offsetY } func (l *Level) LoadTileMapCells(parsedRunes [][]rune) [][]*TermBoxCell { var cells [][]*TermBoxCell for i, line := range parsedRunes { rowCells := make([]*TermBoxCell, len(line)) var data TileMapCellData for j, char := range line { if _, ok := l.TileData[char]; !ok { if _, ok := CommonTileMapCellData[char]; !ok { data = NewTileMapCell(char, func() {}, i) } else { data = CommonTileMapCellData[char] data.LineNumber = i } } else { data = l.TileData[char] } if reflect.DeepEqual(data, TileMapCellData{}) { data = CommonTileMapCellData[char] } cell := &TermBoxCell{ &termbox.Cell{data.Ch, data.FgColor, data.BgColor}, data.CollidesPhysically, data, } rowCells[j] = cell } cells = append(cells, rowCells) } l.TileMap = cells return l.TileMap } func (l *Level) LoadTileMap() { parsed := ParseTileMapString(l.TileMapString) l.LoadTileMapCells(parsed) } // row, length func (l *Level) GetTileMapDimensions() (int, int) { parsed := ParseTileMapString(l.TileMapString) rowLength := len(parsed[0]) columnLength := len(parsed) return rowLength, columnLength } func (l *Level) InitDefaults() { // set default quit functions exitTerms := []string{"q", "quit", "exit"} if l.ColonLineCallbacks == nil { l.ColonLineCallbacks = make(map[string]func(*Game)) } for _, term := range exitTerms { if _, ok := l.ColonLineCallbacks[term]; !ok { l.ColonLineCallbacks[term] = func(g *Game) { go func() { g.VimManEvents <- VimManEvent{Content: "exit"} }() } } } } ================================================ FILE: level1_basic_movement.go ================================================ package vimman import "github.com/nsf/termbox-go" const LevelBasicMovementTileMapString = ` +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ Try and find the exit | | | | | | + +--+--+--+--+--+ +--+ +--+ + + + + + + + +--+ + | | | | | | | | | | | | | +--+--+ +--+ + +--+ +--+ + +--+ + +--+ + + + + + | | | | | | | | | | | | + +--+--+ + +--+--+--+ +--+--+ + +--+--+--+--+--+--+ + | | | | | | | | | | + + + + +--+ +--+--+--+ +--+--+--+--+--+ + + + +--+ | | | | | | | | | | | | +--+--+ + + +--+ + + + +--+ + +--+ +--+--+ +--+ + | | | | | | | | | | | | | + + + +--+ + +--+ +--+ +--+--+ + + + +--+ + +--+ | | | | | | | | | | | | | | + + +--+ + + +--+--+ + + +--+--+ +--+--+ + +--+--+ | | | | | | | | | | | + +--+--+--+--+--+ + + +--+--+--+ + + + + +--+--+ + | | | | | | | | | | + +--+ +--+ +--+--+ + + + + +--+--+ + +--+--+--+--+ | | | | | | exit ↓ +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ ` func NewLevelBasicMovement(g *Game) *Level { // create user user := NewUser(g.Stage, 1, 1) var entities []Renderer entities = append(entities, user) tileData := TileMapCellDataMap{ '↓': TileMapCellData{ Ch: '↓', FgColor: termbox.ColorGreen, BgColor: termbox.ColorBlack, CollidesPhysically: false, CollisionCallback: func() { levelInstance := NewLevelExitingVim(g) g.Stage.SetLevel(levelInstance) }, }, } level := &Level{ Game: g, Entities: entities, TileMapString: LevelBasicMovementTileMapString, TileData: tileData, InputBlocked: true, VimMode: normalMode, Init: func() { // load info titleOptions := WordOptions{ InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} title := NewWord(g.Stage, levelTitleCoordX, levelTitleCoordY, "Level 1 - MOVING THE CURSOR", titleOptions) explanationOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} explanation := NewWord(g.Stage, levelExplanationCoordX, levelExplanationCoordY, "J: down, H: left, K: up, L: right", explanationOptions) g.Stage.AddScreenEntity(title, explanation) }, } level.InitDefaults() return level } ================================================ FILE: level2_exiting_vim.go ================================================ package vimman import "github.com/nsf/termbox-go" const LevelExitingVimTileMapString = ` +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |~ | |~ | |~ | |~ | |~ | |~ | |~ VIM | |~ Hardest editor to exit | |~ :) | |~ | |~ | |~ | |~ | |~ | |~ | |~ | |~ | |~ | |~ | |~ | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ ` func NewLevelExitingVim(g *Game) *Level { // create user user := NewUser(g.Stage, 1, 1) var entities []Renderer entities = append(entities, user) tileData := TileMapCellDataMap{ '↓': TileMapCellData{ Ch: '↓', FgColor: termbox.ColorGreen, BgColor: termbox.ColorBlack, CollidesPhysically: false, CollisionCallback: func() {}, }, } level := &Level{ Game: g, Entities: entities, TileMapString: LevelExitingVimTileMapString, TileData: tileData, InputBlocked: true, VimMode: normalMode, Init: func() { titleOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} title := NewWord(g.Stage, levelTitleCoordX, levelTitleCoordY, "Level 2 - EXITING VIM", titleOptions) explanationOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} explanation := NewWord(g.Stage, levelExplanationCoordX, levelExplanationCoordY, "You can't be a great Vim user without knowing how to exit.", explanationOptions) hintOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} hint := NewWord(g.Stage, levelHintCoordX, levelHintCoordY, "Type colon ':', then 'q', press enter", hintOptions) g.Stage.AddScreenEntity(title, explanation, hint) }, ColonLineCallbacks: make(map[string]func(*Game)), } exitTerms := []string{"q", "quit", "exit"} for _, term := range exitTerms { if _, ok := level.ColonLineCallbacks[term]; !ok { level.ColonLineCallbacks[term] = func(g *Game) { levelInstance := NewLevelTextEditing(g) g.Stage.SetLevel(levelInstance) } } } level.InitDefaults() return level } ================================================ FILE: level3_text_editing.go ================================================ package vimman import "github.com/nsf/termbox-go" const LevelTextEditingTileMapString = ` +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |~ | |~ | |~ | |~ 1- DELETION - Press "x" to delete the character under | |~ the cursor in normal mode. | |~ | |~ Yyou shalll noot paass | |~ | |~ 2- INSERTION - Move the cursor after the character | |~ where the text should be inserted, press i and type | |~ | |~ Yu shll nt pss | |~ | |~ 3- APPENDING - Move the curser before the character | |~ where the text should be appended, press a and type | |~ | |~ Yo shal no pas | |~ | |~ | |~ | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ ` func NewLevelTextEditing(g *Game) *Level { user := NewUser(g.Stage, 1, 1) var entities []Renderer entities = append(entities, user) tileData := TileMapCellDataMap{ '↓': TileMapCellData{ Ch: '↓', FgColor: termbox.ColorGreen, BgColor: termbox.ColorBlack, CollidesPhysically: false, CollisionCallback: func() {}, }, } level := &Level{ Game: g, Entities: entities, TileMapString: LevelTextEditingTileMapString, TileData: tileData, VimMode: normalMode, Init: func() { titleOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} title := NewWord(g.Stage, levelTitleCoordX, levelTitleCoordY, "Level 3 - TEXT EDITING", titleOptions) explanationOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} explanation := NewWord(g.Stage, levelExplanationCoordX, levelExplanationCoordY, "Delete, insert, append.", explanationOptions) hintOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} hint := NewWord(g.Stage, levelHintCoordX, levelHintCoordY, "Complete the 3 steps below to proceed to the next level.", hintOptions) g.Stage.AddScreenEntity(title, explanation, hint) }, } level.InitDefaults() return level } ================================================ FILE: level4_bomberman.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "time" ) const LevelBombermanTileMapString = ` ▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅ █ ☵☲ ☵☲ █ █☲◼◼ ◼◼ ◼◼ ◼◼ ◼◼ ◼◼ █ █ ☲☵☲☵ █ █ ◼◼☲◼◼ ◼◼ ◼◼ ◼◼ ◼◼ █ █ ☲☵ ☵☲☵ █ █ ◼◼ ◼◼ ◼◼ ◼◼ ◼◼ ◼◼☲█ █☲☵ ☲☵ ☲☵ ☲☵█ █ ◼◼☵◼◼ ◼◼ ◼◼ ◼◼ ◼◼☵█ █ ☲☵ exit ↓ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ` func NewLevelBomberman(g *Game) *Level { user := NewUser(g.Stage, 1, 1) var entities []Renderer entities = append(entities, user) tileData := TileMapCellDataMap{ 'b': TileMapCellData{ Ch: '💣', FgColor: termbox.ColorGreen, BgColor: termbox.ColorBlack, CollidesPhysically: true, CollisionCallback: nil, InitCallback: func(selfEntity *Entity) { bombOptions := WordOptions{InitCallback: nil, Fg: typedCharacterFg, Bg: typedCharacterBg, CollidesPhysically: true} bomb := NewWord(g.Stage, selfEntity.GetPositionX(), selfEntity.GetPositionY(), string('💣'), bombOptions) g.Stage.AddTypedEntity(bomb) go func() { <-time.After(1 * time.Second) posX := selfEntity.Position.x posY := selfEntity.Position.y positions := [][2]int{ {posX, posY}, {posX + 1, posY}, {posX, posY + 1}, {posX - 1, posY}, {posX, posY - 1}, } var positionsToBeCleared [][2]int for _, pos := range positions { if !g.Stage.Canvas.IsInsideOfBoundaries(pos[0], pos[1]) { return } // deliberately using reverse order in two dimensional array :/ if !ContainsRune([]rune{'◼', '▅', '█'}, g.Stage.LevelInstance.TileMap[pos[1]][pos[0]].Ch) { positionsToBeCleared = append(positionsToBeCleared, [2]int{pos[0], pos[1]}) } } // clear character and collision g.Stage.ClearTileMapCellsAt(positionsToBeCleared) }() }, }, '↓': TileMapCellData{ Ch: '↓', FgColor: termbox.ColorGreen, BgColor: termbox.ColorBlack, CollidesPhysically: false, CollisionCallback: func() { levelInstance := NewLevelExitingVim(g) g.Stage.SetLevel(levelInstance) }, }, } level := &Level{ Game: g, Entities: entities, TileMapString: LevelBombermanTileMapString, TileData: tileData, InputRunes: []rune{'b'}, BlockedKeys: []termbox.Key{termbox.KeyBackspace, termbox.KeyDelete}, VimMode: normalMode, TextShiftingDisabled: true, Init: func() { titleOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} title := NewWord(g.Stage, levelTitleCoordX, levelTitleCoordY, "Level 4 - VIMBERMAN", titleOptions) explanationOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} explanation := NewWord(g.Stage, levelExplanationCoordX, levelExplanationCoordY, "i: Insert Mode, esc: Back to Normal Mode", explanationOptions) hintOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: true} hint := NewWord(g.Stage, levelHintCoordX, levelHintCoordY, "Type b in Insert Mode to drop a bomb!", hintOptions) g.Stage.AddScreenEntity(title, explanation, hint) }, } level.InitDefaults() return level } ================================================ FILE: logger.go ================================================ package vimman import ( "fmt" "log" "os" "sync" ) type Logger struct { logs []string } var instance *Logger var once sync.Once var lg = GetLogger() func GetLogger() *Logger { once.Do(func() { instance = &Logger{ logs: make([]string, 0), } }) return instance } func (l *Logger) Log(strings ...string) { l.logs = append(l.logs, strings...) } func (l *Logger) LogValue(values ...interface{}) { var strings []string for _, string := range values { value := fmt.Sprintf("%v", string) strings = append(strings, value) } l.logs = append(l.logs, strings...) } func (l *Logger) DumpLogs() { for _, log := range l.logs { fmt.Println(log) } } func (l *Logger) WriteFile(text string) { if os.Getenv("DEBUG") == "1" { f, err := os.OpenFile("logfile.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening file: %v", err) } defer f.Close() log.SetOutput(f) log.Println(text) } } ================================================ FILE: stage.go ================================================ package vimman import ( "fmt" "github.com/nsf/termbox-go" "time" ) var levelConstructors = []func(*Game) *Level{ NewLevelBasicMovement, NewLevelExitingVim, NewLevelTextEditing, NewLevelBomberman, } // holds the current level meta data, canvas and entities type Stage struct { Game *Game Level int LevelInstance *Level Fps float64 CanvasEntities []Renderer // entities to be rendered in canvas ScreenEntities []Renderer // entities to be rendered outside of the canvas TypedEntities []Renderer BgCell *termbox.Cell Canvas Canvas Width int Height int pixelMode bool offsetx int offsety int ModeLabel *Word ColonLine *Word } func NewStage(g *Game, level int, fps float64, bgCell *termbox.Cell) *Stage { return &Stage{ Game: g, Level: level, Fps: fps, CanvasEntities: nil, ScreenEntities: nil, TypedEntities: nil, BgCell: bgCell, Canvas: nil, Width: 0, Height: 0, pixelMode: false, offsetx: 0, offsety: 0, } } func (s *Stage) AddCanvasEntity(e ...Renderer) { s.CanvasEntities = append(s.CanvasEntities, e...) } func (s *Stage) AddScreenEntity(e ...Renderer) { s.ScreenEntities = append(s.ScreenEntities, e...) } func (s *Stage) AddTypedEntity(e ...Renderer) { s.TypedEntities = append(s.TypedEntities, e...) } func (s *Stage) ClearCanvasEntities() { s.CanvasEntities = nil s.ScreenEntities = nil s.TypedEntities = nil } func (s *Stage) SetGame(game *Game) { s.Game = game } // this function handles all the rendering // sets and renders in order: // tilemap cells: the background cells // canvas entities: canvas entities, they have update methods that gets called on stage.update method // screen cells: cells outside of the canvas such as level label and instructions func (s *Stage) Render() { s.SetCanvasBackgroundCells() for i, _ := range s.CanvasEntities { e := s.CanvasEntities[i] // sets the cells for the entity, so that it overwrites the background cell(s) e.SetCells(s) } s.TermboxSetScreenCells() s.TermboxSetCanvasCells() s.TermboxSetTypedCells() s.TermboxSetCursorCell() termbox.Flush() } func (s *Stage) update(ev termbox.Event, delta time.Duration) { for _, e := range s.CanvasEntities { e.Update(s, ev, delta) } s.updateModeLabel() s.updateColonLine() } func (s *Stage) updateModeLabel() { s.ModeLabel.Content = fmt.Sprintf("-- %s MODE --", s.LevelInstance.VimMode) s.ModeLabel.Entity.Cells = ConvertStringToCells(s.ModeLabel.Content, s.ModeLabel.Fg, s.ModeLabel.Bg) } func (s *Stage) updateColonLine() { s.ColonLine.Entity.Cells = ConvertStringToCells(s.ColonLine.Content, s.ColonLine.Fg, s.ColonLine.Bg) } func (s *Stage) Init() { level := s.Level - 1 if level < 0 || level > len(levelConstructors) { level = 0 } s.SetLevel(levelConstructors[level](s.Game)) } func (s *Stage) SetLevel(levelInstance *Level) { s.Reset() s.LevelInstance = levelInstance s.LevelInstance.Init() s.LevelInstance.LoadTileMap() s.Resize(s.LevelInstance.GetTileMapDimensions()) for _, e := range s.LevelInstance.Entities { s.AddCanvasEntity(e) } modeLabelOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: false, Tags: []Tag{{"ModeLabel"}}} s.ModeLabel = NewWord(s, 0, s.Game.getScreenSizeY()-2, fmt.Sprintf("-- %s MODE --", levelInstance.VimMode), modeLabelOptions) colonLineOptions := WordOptions{InitCallback: nil, Fg: levelTitleFg, Bg: levelTitleBg, CenterHorizontally: false, Tags: []Tag{{"ColonLine"}}} s.ColonLine = NewWord(s, 0, s.Game.getScreenSizeY()-1, "", colonLineOptions) s.AddScreenEntity(s.ModeLabel, s.ColonLine) } func (s *Stage) Reset() { s.ClearCanvasEntities() s.Canvas = NewCanvas(10, 10) } func (s *Stage) Resize(w, h int) { s.Width = w s.Height = h if s.pixelMode { s.Height *= 2 } if s.LevelInstance.Height != 0 && s.LevelInstance.Width != 0 { s.Width = s.LevelInstance.Width s.Height = s.LevelInstance.Height } c := NewCanvas(s.Width, s.Height) // Copy old data that fits for i := 0; i < MinInt(s.Height, len(s.Canvas)); i++ { for j := 0; j < MinInt(s.Width, len(s.Canvas)); j++ { c[i][j] = s.Canvas[i][j] } } s.Canvas = c } func (s *Stage) GetDefaultBgCell() *TermBoxCell { return &TermBoxCell{s.BgCell, false, TileMapCellData{}} } func (s *Stage) GetRendererEntityByTag(wantedTag Tag) Renderer { for _, ce := range s.CanvasEntities { for _, tag := range ce.GetTags() { if tag.Name == wantedTag.Name { return ce } } } return nil } // sets the background cells to be rendered, this gets rendered first in the render method // so that other cells can be overwritten into the same location func (s *Stage) SetCanvasBackgroundCells() { for i, row := range s.Canvas { for j, _ := range row { if s.LevelInstance.TileMap[i][j].Cell != nil { // insert tile map cell s.Canvas.SetCellAt(i, j, s.LevelInstance.TileMap[i][j]) } else { //insert default bg cell s.Canvas.SetCellAt(i, j, s.GetDefaultBgCell()) } } } } // calls termbox.setCell, sets the coordinates and the cell attributes // this does the actual rendering of the characters, thanks to termbox-go <3 func (s *Stage) TermboxSetCell(x, y int, cell *TermBoxCell, offset bool) { if offset { offsetX, offsetY := s.LevelInstance.GetScreenOffset() x += offsetX y += offsetY } termbox.SetCell(x, y, cell.Ch, termbox.Attribute(cell.Fg), termbox.Attribute(cell.Bg)) } // sets the cells inside the canvas, offset is being applied in order to keep the canvas in center func (s *Stage) TermboxSetCanvasCells() { for i, row := range s.Canvas { for j, _ := range row { cell := row[j] // intentionally use j,i in reverse order s.TermboxSetCell(j, i, cell, true) } } } // sets the cells outside of the canvas, no offset is being applied func (s *Stage) TermboxSetScreenCells() { for _, e := range s.ScreenEntities { for j, _ := range e.GetCells() { cell := e.GetCells()[j] x := e.GetPositionX() offsetX := 0 if e.ShouldCenterHorizontally() { offsetX, _ = e.GetScreenOffset() } x = x + j + offsetX s.TermboxSetCell(x, e.GetPositionY(), cell, false) } } } func (s *Stage) TermboxSetTypedCells() { for _, e := range s.TypedEntities { for j, _ := range e.GetCells() { cell := e.GetCells()[j] s.TermboxSetCell(e.GetPositionX(), e.GetPositionY(), cell, true) } } } func (s *Stage) TermboxSetCursorCell() { cursorEntity := s.GetRendererEntityByTag(Tag{"Cursor"}) for j, _ := range cursorEntity.GetCells() { cell := cursorEntity.GetCells()[j] s.TermboxSetCell(cursorEntity.GetPositionX(), cursorEntity.GetPositionY(), cell, true) } } func (s *Stage) CheckCollision(x, y int) bool { return s.Canvas.CheckCollision(x, y) } // clear Canvas cell and TileMap cell at the given positions func (s *Stage) ClearTileMapCellsAt(positions [][2]int) { for _, pos := range positions { options := DefaultWordOptions() emptyChar := NewEmptyCharacter(s, pos[0], pos[1], options) s.AddTypedEntity(emptyChar) s.LevelInstance.TileMap[pos[1]][pos[0]].collidesPhysically = false } } ================================================ FILE: termbox_cell.go ================================================ package vimman import "github.com/nsf/termbox-go" type TermBoxCell struct { *termbox.Cell collidesPhysically bool cellData TileMapCellData } func EmptyTileMapCell() *TermBoxCell { data := CommonTileMapCellData[' '] cell := &TermBoxCell{ &termbox.Cell{data.Ch, data.FgColor, data.BgColor}, data.CollidesPhysically, data, } return cell } ================================================ FILE: tilemap.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "strings" ) func NewTileMapCell(ch rune, fn func(), lineNumber int) TileMapCellData { return TileMapCellData{ BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: ch, CollidesPhysically: false, CollisionCallback: fn, LineNumber: lineNumber, } } type TileMapCellData struct { Ch rune BgColor termbox.Attribute FgColor termbox.Attribute CollidesPhysically bool CollisionCallback func() InitCallback func(*Entity) LineNumber int } type TileMapCellDataMap map[rune]TileMapCellData var CommonTileMapCellData = TileMapCellDataMap{ '0': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: ' ', CollidesPhysically: false, CollisionCallback: func() {}, }, '↓': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '↓', CollidesPhysically: false, CollisionCallback: func() {}, }, '+': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '+', CollidesPhysically: true, CollisionCallback: func() {}, }, '-': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '-', CollidesPhysically: true, CollisionCallback: func() {}, }, '|': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '|', CollidesPhysically: true, CollisionCallback: func() {}, }, '█': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '█', CollidesPhysically: true, CollisionCallback: func() {}, }, '◼': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '◼', CollidesPhysically: true, CollisionCallback: func() {}, }, '▅': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '▅', CollidesPhysically: true, CollisionCallback: func() {}, }, '▀': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '▀', CollidesPhysically: true, CollisionCallback: func() {}, }, '☵': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '☵', CollidesPhysically: true, CollisionCallback: func() {}, }, '☲': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: '☲', CollidesPhysically: true, CollisionCallback: func() {}, }, ' ': { BgColor: termbox.ColorBlack, FgColor: termbox.ColorWhite, Ch: ' ', CollidesPhysically: false, CollisionCallback: func() {}, }, } func ParseLine(l string) []rune { var lineChars []rune //chars := strings.Split(l, " ") //line := strings.Join(chars, "") for _, char := range l { lineChars = append(lineChars, char) } return lineChars } func ParseTileMapString(tileMap string) [][]rune { var parsed [][]rune lines := strings.Split(tileMap, "\n") lines = lines[1 : len(lines)-1] for _, line := range lines { l := ParseLine(line) parsed = append(parsed, l) } return parsed } ================================================ FILE: user.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "reflect" "time" ) type Class int type User struct { *Entity } func (u *User) ShouldCenterHorizontally() bool { return false } func NewUser(s *Stage, x, y int) (u *User) { cells := []*TermBoxCell{ {&termbox.Cell{'▒', termbox.ColorGreen, bgColor}, false, TileMapCellData{}}, } tags := []Tag{{"Cursor"}} entityOptions := EntityOptions{9999, tags, nil} e := NewEntity(s, x, y, 1, 1, ' ', termbox.ColorBlue, termbox.ColorWhite, cells, false, entityOptions) u = &User{ Entity: e, } return } func (u *User) handleNormalModeEvents(s *Stage, event termbox.Event) { switch event.Ch { case 'k': nextY := u.GetPositionY() - 1 if !s.CheckCollision(u.GetPositionX(), nextY) { u.SetPositionY(nextY) } case 'j': nextY := u.GetPositionY() + 1 if !s.CheckCollision(u.GetPositionX(), nextY) { u.SetPositionY(nextY) } case 'l': nextX := u.GetPositionX() + 1 if !s.CheckCollision(nextX, u.GetPositionY()) { u.SetPositionX(nextX) } case 'h': nextX := u.GetPositionX() - 1 if !s.CheckCollision(nextX, u.GetPositionY()) { u.SetPositionX(nextX) } case 'i': if s.LevelInstance.VimMode != insertMode && !s.LevelInstance.InputBlocked { s.LevelInstance.VimMode = insertMode } case 'x': if ContainsTermboxKey(s.LevelInstance.BlockedKeys, termbox.KeyDelete) { return } if s.LevelInstance.InputBlocked { return } x := u.GetPositionX() y := u.GetPositionY() // keep the last element in place, insert an empty cell before the last character in the line tileMap := s.LevelInstance.TileMap lastElement := s.LevelInstance.TileMap[y][len(s.LevelInstance.TileMap[y])-1] tileMap[y] = append(tileMap[y][:x], tileMap[y][x+1:len(tileMap[y])-1]...) tileMap[y] = append(tileMap[y], EmptyTileMapCell(), lastElement) case ':': if s.LevelInstance.VimMode == normalMode { s.LevelInstance.VimMode = colonMode s.ColonLine.Content = ":" } } } func (u *User) handleInsertModeEvents(s *Stage, event termbox.Event) { // return on empty event if reflect.DeepEqual(event, termbox.Event{}) { return } switch event.Key { // switch to normal mode on esc key event case termbox.KeyEsc: s.LevelInstance.VimMode = normalMode return case termbox.KeyBackspace, termbox.KeyBackspace2: if ContainsTermboxKey(s.LevelInstance.BlockedKeys, termbox.KeyBackspace) || ContainsTermboxKey(s.LevelInstance.BlockedKeys, termbox.KeyBackspace2) { return } characterOptions := WordOptions{InitCallback: nil, Fg: typedCharacterFg, Bg: typedCharacterBg} character := NewEmptyCharacter(s, u.GetPositionX()-1, u.GetPositionY(), characterOptions) if !character.IsInsideOfCanvasBoundaries() { return } s.AddTypedEntity(character) u.SetPositionX(u.GetPositionX() - 1) default: // don't allow non-character events if event.Ch == 0 { return } // check if rune input is allowed if len(s.LevelInstance.InputRunes) > 0 { if !ContainsRune(s.LevelInstance.InputRunes, event.Ch) { return } } characterOptions := WordOptions{InitCallback: nil, Fg: typedCharacterFg, Bg: typedCharacterBg} character := NewWord(s, u.GetPositionX(), u.GetPositionY(), string(event.Ch), characterOptions) if !character.IsInsideOfCanvasBoundaries() { return } if !s.LevelInstance.TextShiftingDisabled { x := u.GetPositionX() y := u.GetPositionY() tileMap := s.LevelInstance.TileMap[y] lastElement := tileMap[len(tileMap)-1] tileMap = append(tileMap[:x], append([]*TermBoxCell{character.Cell}, tileMap[x:len(tileMap)-2]...)...) tileMap = append(tileMap, lastElement) } // type a character and add as typed entity s.AddTypedEntity(character) u.SetPositionX(u.GetPositionX() + 1) // at the end of the current line, move the cursor next line if s.Canvas.IsInLastColumn(u.GetPositionX()) { u.SetPosition(1, u.GetPositionY()+1) } if character.InitCallback != nil { character.InitCallback() } if s.LevelInstance.TileData[event.Ch].InitCallback != nil { s.LevelInstance.TileData[event.Ch].InitCallback(character.Entity) } } } func (u *User) handleColonModeEvents(s *Stage, event termbox.Event) { if event.Key == termbox.KeyEnter { s.LevelInstance.VimMode = normalMode if len(s.LevelInstance.ColonLineCallbacks) > 0 { if fn, ok := s.LevelInstance.ColonLineCallbacks[s.ColonLine.Content[1:]]; ok { fn(s.Game) } } } if event.Ch != 0 { s.ColonLine.Content = s.ColonLine.Content + string(event.Ch) } } func (u *User) Update(s *Stage, event termbox.Event, delta time.Duration) { switch s.LevelInstance.VimMode { case colonMode: u.handleColonModeEvents(s, event) case normalMode: u.handleNormalModeEvents(s, event) case insertMode: u.handleInsertModeEvents(s, event) } } ================================================ FILE: utils.go ================================================ package vimman import "github.com/nsf/termbox-go" func ContainsRune(s []rune, e rune) bool { for _, a := range s { if a == e { return true } } return false } func ContainsString(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } func ContainsTermboxKey(s []termbox.Key, e termbox.Key) bool { for _, a := range s { if a == e { return true } } return false } func MinInt(a, b int) int { if a < b { return a } return b } ================================================ FILE: word.go ================================================ package vimman import ( "github.com/nsf/termbox-go" "time" ) type Word struct { *Entity Content string Speed float64 Direction Direction CenterHorizontally bool Fg termbox.Attribute Bg termbox.Attribute } func (w *Word) ShouldCenterHorizontally() bool { return w.CenterHorizontally } type WordOptions struct { InitCallback func() Fg termbox.Attribute Bg termbox.Attribute CollidesPhysically bool CenterHorizontally bool Tags []Tag } func ConvertStringToCells(s string, fg termbox.Attribute, bg termbox.Attribute) []*TermBoxCell { var arr []*TermBoxCell for i := 0; i < len([]rune(s)); i++ { cell := &TermBoxCell{ Cell: &termbox.Cell{ []rune(s)[i], fg, bg, }, collidesPhysically: false, cellData: TileMapCellData{}} arr = append(arr, cell) } return arr } func NewWord(s *Stage, x, y int, content string, options WordOptions) *Word { fg, bg, collidesPhysically, centerHorizontally := options.Fg, options.Bg, options.CollidesPhysically, options.CenterHorizontally cells := ConvertStringToCells(content, fg, bg) entityOptions := EntityOptions{2000, options.Tags, nil} e := NewEntity(s, x, y, len(content), 1, ' ', fg, bg, cells, collidesPhysically, entityOptions) return &Word{ Entity: e, Content: content, Direction: horizontal, CenterHorizontally: centerHorizontally, Fg: fg, Bg: bg, } } func DefaultWordOptions() WordOptions { return WordOptions{InitCallback: nil, Fg: typedCharacterFg, Bg: typedCharacterBg, CollidesPhysically: false, CenterHorizontally: true} } func NewEmptyCharacter(s *Stage, x, y int, options WordOptions) *Word { return NewWord(s, x, y, string(" "), options) } func (w *Word) Update(s *Stage, event termbox.Event, delta time.Duration) { }