diff --git a/go.mod b/go.mod index 3064264..7ece437 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,17 @@ module git.saintnet.tech/freego go 1.17 + +require ( + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect + github.com/hajimehoshi/ebiten v1.12.12 // indirect + github.com/hajimehoshi/ebiten/v2 v2.2.5 // indirect + github.com/jezek/xgb v1.0.0 // indirect + golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect + golang.org/x/exp/shiny v0.0.0-20220218215828-6cf2b201936e // indirect + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect + golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/text v0.3.6 // indirect +) diff --git a/input.go b/input.go new file mode 100644 index 0000000..a089449 --- /dev/null +++ b/input.go @@ -0,0 +1,206 @@ +package main + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +// Dir represents a direction. +type Dir int + +// represents the valid directions +const ( + DirUp Dir = iota + DirRight + DirDown + DirLeft +) + +type mouseState int + +const ( + mouseStateNone mouseState = iota + mouseStatePressing + mouseStateSettled +) + +type touchState int + +const ( + touchStateNone touchState = iota + touchStatePressing + touchStateSettled + touchStateInvalid +) + +// String returns a string representing the direction. +func (d Dir) String() string { + switch d { + case DirUp: + return "Up" + case DirRight: + return "Right" + case DirDown: + return "Down" + case DirLeft: + return "Left" + } + panic("not reach") +} + +// Vector returns a [-1, 1] value for each axis. +func (d Dir) Vector() (x, y int) { + switch d { + case DirUp: + return 0, -1 + case DirRight: + return 1, 0 + case DirDown: + return 0, 1 + case DirLeft: + return -1, 0 + } + panic("not reach") +} + +// Input represents the current key states. +type Input struct { + mouseState mouseState + mouseInitPosX int + mouseInitPosY int + mouseDir Dir + + touches []ebiten.TouchID + touchState touchState + touchID ebiten.TouchID + touchInitPosX int + touchInitPosY int + touchLastPosX int + touchLastPosY int + touchDir Dir +} + +// NewInput generates a new Input object. +func NewInput() *Input { + return &Input{} +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func vecToDir(dx, dy int) (Dir, bool) { + if abs(dx) < 4 && abs(dy) < 4 { + return 0, false + } + if abs(dx) < abs(dy) { + if dy < 0 { + return DirUp, true + } + return DirDown, true + } + if dx < 0 { + return DirLeft, true + } + return DirRight, true +} + +// Update updates the current input states. +func (i *Input) Update() { + switch i.mouseState { + case mouseStateNone: + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + x, y := ebiten.CursorPosition() + i.mouseInitPosX = x + i.mouseInitPosY = y + i.mouseState = mouseStatePressing + } + case mouseStatePressing: + if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + x, y := ebiten.CursorPosition() + dx := x - i.mouseInitPosX + dy := y - i.mouseInitPosY + d, ok := vecToDir(dx, dy) + if !ok { + i.mouseState = mouseStateNone + break + } + i.mouseDir = d + i.mouseState = mouseStateSettled + } + case mouseStateSettled: + i.mouseState = mouseStateNone + } + + i.touches = ebiten.AppendTouchIDs(i.touches[:0]) + switch i.touchState { + case touchStateNone: + if len(i.touches) == 1 { + i.touchID = i.touches[0] + x, y := ebiten.TouchPosition(i.touches[0]) + i.touchInitPosX = x + i.touchInitPosY = y + i.touchLastPosX = x + i.touchLastPosX = y + i.touchState = touchStatePressing + } + case touchStatePressing: + if len(i.touches) >= 2 { + break + } + if len(i.touches) == 1 { + if i.touches[0] != i.touchID { + i.touchState = touchStateInvalid + } else { + x, y := ebiten.TouchPosition(i.touches[0]) + i.touchLastPosX = x + i.touchLastPosY = y + } + break + } + if len(i.touches) == 0 { + dx := i.touchLastPosX - i.touchInitPosX + dy := i.touchLastPosY - i.touchInitPosY + d, ok := vecToDir(dx, dy) + if !ok { + i.touchState = touchStateNone + break + } + i.touchDir = d + i.touchState = touchStateSettled + } + case touchStateSettled: + i.touchState = touchStateNone + case touchStateInvalid: + if len(i.touches) == 0 { + i.touchState = touchStateNone + } + } +} + +// Dir returns a currently pressed direction. +// Dir returns false if no direction key is pressed. +func (i *Input) Dir() (Dir, bool) { + if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) { + return DirUp, true + } + if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) { + return DirLeft, true + } + if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) { + return DirRight, true + } + if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) { + return DirDown, true + } + if i.mouseState == mouseStateSettled { + return i.mouseDir, true + } + if i.touchState == touchStateSettled { + return i.touchDir, true + } + return 0, false +} diff --git a/main.go b/main.go index abbe74f..374ccb2 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,128 @@ package main -import "fmt" +import ( + "errors" + "fmt" + "log" + "time" + "github.com/hajimehoshi/ebiten/v2" +) + +func dummyGame() (*Game, error) { + g := &Game{ + board: NewBoard(4), + state: gameSetup, + } + //Setup terrain + terrain := []struct { + x, y, t int + }{ + {1, 1, 1}, + {2, 2, 1}, + } + for _, tt := range terrain { + res, err := g.board.AddTerrain(tt.x, tt.y, tt.t) + if err != nil { + return nil, err + } + if !res { + return nil, errors.New("Error creating terrain") + } + } + pieces := []struct { + x, y int + p *Piece + }{ + {0, 0, NewPiece(Flag, Blue)}, + {3, 0, NewPiece(Spy, Blue)}, + {2, 0, NewPiece(Captain, Blue)}, + {3, 1, NewPiece(Marshal, Blue)}, + {0, 1, NewPiece(Bomb, Blue)}, + + {1, 2, NewPiece(Flag, Red)}, + {3, 2, NewPiece(Spy, Red)}, + {2, 3, NewPiece(Captain, Red)}, + {0, 2, NewPiece(Marshal, Red)}, + {0, 3, NewPiece(Bomb, Red)}, + } + for _, tt := range pieces { + res, err := g.SetupPiece(tt.x, tt.y, tt.p) + if err != nil { + return nil, fmt.Errorf("Piece %v,%v:%v", tt.x, tt.y, err) + } + if !res { + return nil, errors.New("error placing dummy piece") + } + } + _, err := g.SetupPiece(0, 0, NewPiece(Flag, Blue)) + if err != nil { + return nil, err + } + + return g, nil +} func main() { //red := NewDummyPlayer(Red) //blue := NewDummyPlayer(Blue) - g := NewGame() - g.state = gameSetup - printboardcolours(g) + g, err := dummyGame() + if err != nil { + panic(err) + } + go func(g *Game) { + r := NewDummyPlayer(Red) + b := NewDummyPlayer(Blue) + g.Start() + var moves = []struct { + input string + player Player + res bool + }{ + {"c3-b3", r, true}, + {"c0-c1", b, true}, + {"d2xd1", r, true}, + {"c1-d1", b, true}, + {"b3-c3", r, true}, + {"d1xd2", b, true}, + } + for i, tt := range moves { + time.Sleep(3 * time.Second) + log.Printf("playing move %v\n", i) + raw, err := NewRawCommand(tt.input) + if err != nil { + panic(err) + } + parsed, err := g.Parse(tt.player, raw) + if err != nil { + panic(err) + } + res, err := g.Mutate(parsed) + if err != nil { + panic(err) + } + if res { + log.Printf("move %v successful\n", i) + } + + } + + }(g) + viewer(g) return } +func viewer(g *Game) { + v, err := NewViewer(g) + if err != nil { + panic(err) + } + ebiten.SetWindowSize(800, 640) + ebiten.SetWindowTitle("Freego") + if err := ebiten.RunGame(v); err != nil { + panic(err) + } +} + func addpiece(game *Game, rank int, c Colour, x int, y int) { res, err := game.SetupPiece(x, y, NewPieceFromInt(rank, c)) if err != nil { diff --git a/view_board.go b/view_board.go new file mode 100644 index 0000000..9798c95 --- /dev/null +++ b/view_board.go @@ -0,0 +1,91 @@ +package main + +import ( + "errors" + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/v2" +) + +var errorTaskTerminated = errors.New("freego: task terminated") + +type task func() error + +// ViewBoard represents the game board. +type ViewBoard struct { + size int + tiles map[*ViewTile]struct{} + tasks []task +} + +// NewViewBoard generates a new ViewBoard with giving a size. +func NewViewBoard(g *Game) (*ViewBoard, error) { + size := g.board.size + b := &ViewBoard{ + size: size, + tiles: map[*ViewTile]struct{}{}, + } + for i := 0; i < size; i++ { + for j := 0; j < size; j++ { + b.tiles[NewViewTile(i, j, g.board.board[i][j])] = struct{}{} + } + } + + return b, nil +} + +//func (b *ViewBoard) tileAt(x, y int) *Tile { +// return tileAt(b.tiles, x, y) +//} + +// Update updates the board state. +func (b *ViewBoard) Update(input *Input) error { + for t := range b.tiles { + if err := t.Update(); err != nil { + return err + } + } + if 0 < len(b.tasks) { + t := b.tasks[0] + if err := t(); err == errorTaskTerminated { + b.tasks = b.tasks[1:] + } else if err != nil { + return err + } + return nil + } + if dir, ok := input.Dir(); ok { + //if err := b.Move(dir); err != nil { + // return err + //} + log.Println(dir) + } + return nil +} + +// Draw draws the board to the given boardImage. +func (b *ViewBoard) Draw(boardImage *ebiten.Image) { + boardImage.Fill(color.RGBA{0xbb, 0xad, 0xa0, 0xff}) + for j := 0; j < b.size; j++ { + for i := 0; i < b.size; i++ { + //v := 0 + op := &ebiten.DrawImageOptions{} + x := i*tileSize + (i+1)*tileMargin + y := j*tileSize + (j+1)*tileMargin + op.GeoM.Translate(float64(x), float64(y)) + //op.ColorM.ScaleWithColor(tileBackgroundColor(v)) + boardImage.DrawImage(tileImage, op) + } + } + for t := range b.tiles { + t.Draw(boardImage) + } +} + +// Size returns the board size. +func (b *ViewBoard) Size() (int, int) { + x := b.size*tileSize + (b.size+1)*tileMargin + y := x + return x, y +} diff --git a/view_tile.go b/view_tile.go new file mode 100644 index 0000000..aed8fb9 --- /dev/null +++ b/view_tile.go @@ -0,0 +1,129 @@ +package main + +import ( + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/examples/resources/fonts" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" +) + +//ViewTile is a tile in the viewer +type ViewTile struct { + gameTile *Tile + + // next represents a next tile information after moving. + // next is empty when the tile is not about to move. + //next TileData + + //movingCount int + //startPoppingCount int + //poppingCount int +} + +var ( + tileImage = ebiten.NewImage(tileSize, tileSize) +) + +var ( + mplusSmallFont font.Face + mplusNormalFont font.Face + mplusBigFont font.Face +) + +const ( + tileSize = 80 + tileMargin = 4 +) + +func init() { + tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) + if err != nil { + log.Fatal(err) + } + + const dpi = 72 + mplusSmallFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 24, + DPI: dpi, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatal(err) + } + mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 32, + DPI: dpi, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatal(err) + } + mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 48, + DPI: dpi, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatal(err) + } +} + +//NewViewTile creates a new view tile +func NewViewTile(x, y int, t *Tile) *ViewTile { + return &ViewTile{ + gameTile: t, + } +} + +// Update updates the tile's animation states. +func (t *ViewTile) Update() error { + return nil +} + +// Draw draws the current tile to the given boardImage. +func (t *ViewTile) Draw(boardImage *ebiten.Image) { + j, i := t.gameTile.x, t.gameTile.y + v := t.gameTile.entity + if v == nil && t.gameTile.Passable() { + return + } + op := &ebiten.DrawImageOptions{} + x := i*tileSize + (i+1)*tileMargin + y := j*tileSize + (j+1)*tileMargin + op.GeoM.Translate(float64(x), float64(y)) + //op.ColorM.ScaleWithColor(tileBackgroundColor(v)) + boardImage.DrawImage(tileImage, op) + str := "NA" + if v == nil { + str = "river" + } else { + str = v.Rank.String() + } + + f := mplusBigFont + switch { + case 3 < len(str): + f = mplusSmallFont + case 2 < len(str): + f = mplusNormalFont + } + + bound, _ := font.BoundString(f, str) + w := (bound.Max.X - bound.Min.X).Ceil() + h := (bound.Max.Y - bound.Min.Y).Ceil() + x = x + (tileSize-w)/2 + y = y + (tileSize-h)/2 + h + pieceColor := color.RGBA{0xf9, 0xf6, 0xf2, 0xff} + if v != nil { + if t.gameTile.entity.Owner == Red { + pieceColor = color.RGBA{0xff, 0x0, 0x0, 0xff} + } else if t.gameTile.entity.Owner == Blue { + pieceColor = color.RGBA{0x0, 0x0, 0xff, 0xff} + } + } + text.Draw(boardImage, str, f, x, y, pieceColor) +} diff --git a/viewer.go b/viewer.go new file mode 100644 index 0000000..df6c3ee --- /dev/null +++ b/viewer.go @@ -0,0 +1,60 @@ +package main + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" +) + +//Viewer is a graphical representation of a freego game +type Viewer struct { + input *Input + board *ViewBoard + boardImage *ebiten.Image + gameState *Game +} + +//NewViewer creates a new viewer +func NewViewer(g *Game) (*Viewer, error) { + v := &Viewer{ + input: NewInput(), + gameState: g, + } + var err error + v.board, err = NewViewBoard(g) + if err != nil { + panic(err) + } + return v, nil +} + +// Layout implements ebiten.Game's Layout. +func (v *Viewer) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return 800, 640 +} + +// Update updates the current game state. +func (v *Viewer) Update() error { + v.input.Update() + if err := v.board.Update(v.input); err != nil { + return err + } + return nil +} + +// Draw draws the current game to the given screen. +func (v *Viewer) Draw(screen *ebiten.Image) { + if v.boardImage == nil { + w, h := v.board.Size() + v.boardImage = ebiten.NewImage(w, h) + } + screen.Fill(color.RGBA{0xfa, 0xf8, 0xef, 0xff}) + v.board.Draw(v.boardImage) + op := &ebiten.DrawImageOptions{} + sw, sh := screen.Size() + bw, bh := v.boardImage.Size() + x := (sw - bw) / 2 + y := (sh - bh) / 2 + op.GeoM.Translate(float64(x), float64(y)) + screen.DrawImage(v.boardImage, op) +}