add basic viewer

This commit is contained in:
stryan 2022-02-21 14:22:13 -05:00
parent d41e310aa3
commit 274cda86bb
6 changed files with 616 additions and 4 deletions

14
go.mod
View File

@ -1,3 +1,17 @@
module git.saintnet.tech/freego module git.saintnet.tech/freego
go 1.17 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
)

206
input.go Normal file
View File

@ -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
}

120
main.go
View File

@ -1,16 +1,128 @@
package main 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() { func main() {
//red := NewDummyPlayer(Red) //red := NewDummyPlayer(Red)
//blue := NewDummyPlayer(Blue) //blue := NewDummyPlayer(Blue)
g := NewGame() g, err := dummyGame()
g.state = gameSetup if err != nil {
printboardcolours(g) 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 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) { func addpiece(game *Game, rank int, c Colour, x int, y int) {
res, err := game.SetupPiece(x, y, NewPieceFromInt(rank, c)) res, err := game.SetupPiece(x, y, NewPieceFromInt(rank, c))
if err != nil { if err != nil {

91
view_board.go Normal file
View File

@ -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
}

129
view_tile.go Normal file
View File

@ -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)
}

60
viewer.go Normal file
View File

@ -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)
}