diff --git a/TODO b/TODO index 7421375..bba99fe 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,15 @@ +Basics: + - Items: + - place + - pickup + - drop + - use + - Targeting Screen + - Character Screen + - Common letter/string selector for menus to separate struct/file (use it in inventory, title at least) + - Move scrollBar ro separate struct/file/function + + Assets and i18n: - move tile settings to json, generate from there (part of prefabs) - prepare all interface entries for i18n @@ -15,13 +27,13 @@ Dungeon and branches: - global map of valley Prefabs: + load prefabs - - compose from gens and prefabs + + compose from gens and prefabs - editor for prefabs - Mapgen + Mapgen: - use delaunay -> minimum spanning tree for room connection (Краскал в gonum) github.com/algds/kruskals - MST github.com/esimov/triangle - delaunay - - или граф относительных окрестностей + - или граф относительных окрестностей (?) Combat: - generate skeleton / intesines / muscle / eyes&ears & fingers from templates @@ -31,11 +43,6 @@ Combat: - damage from skill / mass / speed / material density - no hitpoints! blood is the life source -Items: - - pickup - - drop - - use - Mobs: basic: - place mobs diff --git a/delaunay_test.go b/delaunay_test.go new file mode 100644 index 0000000..42864bb --- /dev/null +++ b/delaunay_test.go @@ -0,0 +1,37 @@ +package alchemyst_go + +import ( + "lab.zaar.be/thefish/alchemyst-go/engine/types" + "lab.zaar.be/thefish/alchemyst-go/util/delaunay" + "testing" +) + +func TestDelaunay(t *testing.T) { + + coords := []types.Coords{ + {10,10}, + {10,60}, + {30,10}, + {40,20}, + {60,10}, + {40,60}, + } + + expected := []types.Edge{ + {types.Coords{10, 60}, types.Coords{10, 10}}, + {types.Coords{30, 10}, types.Coords{40, 20}}, + {types.Coords{60, 10}, types.Coords{40, 60}}, + {types.Coords{40, 20}, types.Coords{40, 60}}, + {types.Coords{10, 10,}, types.Coords{30, 10}}, + } + + result := delaunay.GetMst(coords, 100, 100) + + for idx, _ := range result { + if result[idx] != expected[idx] { + t.Errorf("Failed, expected %v. got %v", result[idx], expected[idx]) + } + + } + t.Log("output: ", result) +} diff --git a/engine/gamemap/level.go b/engine/gamemap/level.go index c099c73..d932ea5 100644 --- a/engine/gamemap/level.go +++ b/engine/gamemap/level.go @@ -3,7 +3,7 @@ package gamemap import ( "lab.zaar.be/thefish/alchemyst-go/engine/ecs" "lab.zaar.be/thefish/alchemyst-go/engine/types" - "lab.zaar.be/thefish/alchemyst-go/util" + "lab.zaar.be/thefish/alchemyst-go/util/appctx" ) //fixme move to config @@ -13,12 +13,12 @@ var mapHeight = 90 type Level struct { types.Rect - ctx util.ClientCtx - Name string - Branch string - Depth int - Objects *[]ecs.Entity - Tiles []*Tile + ctx appctx.ClientCtx + Name string + Branch string + Depth int + Objects *[]ecs.Entity + Tiles []*Tile } func (l *Level) GetTile (coords types.Coords) *Tile { @@ -63,7 +63,7 @@ func (l *Level) Put (x, y int, tileFunc interface{}) { } } -func NewLevel(ctx util.ClientCtx, branch string, depth int) *Level { +func NewLevel(ctx appctx.ClientCtx, branch string, depth int) *Level { l := &Level{ ctx: ctx, Name: branch + string(depth), diff --git a/engine/gamemap/mapgens/default.go b/engine/gamemap/mapgens/default.go index 4a05769..4dd901f 100644 --- a/engine/gamemap/mapgens/default.go +++ b/engine/gamemap/mapgens/default.go @@ -9,8 +9,8 @@ import ( //fixme move to config var minRoomSize = 3 -var maxRoomSize = 22 -var maxrooms = 20 +var maxRoomSize = 15 +var maxrooms = 50 var fges = map[int]types.RectFill{ 1: types.RectFill{ diff --git a/engine/gamemap/prefab.go b/engine/gamemap/prefab.go index 8d9141e..8b641bd 100644 --- a/engine/gamemap/prefab.go +++ b/engine/gamemap/prefab.go @@ -6,7 +6,7 @@ import ( "lab.zaar.be/thefish/alchemyst-go/engine/items" "lab.zaar.be/thefish/alchemyst-go/engine/mob" "lab.zaar.be/thefish/alchemyst-go/engine/types" - "lab.zaar.be/thefish/alchemyst-go/util" + "lab.zaar.be/thefish/alchemyst-go/util/appctx" ) type PrefabFile struct { @@ -42,10 +42,10 @@ func LoadPrefabFile(filename string) (*PrefabFile, error) { } type PrefabLoader struct { - ctx util.ClientCtx + ctx appctx.ClientCtx } -func NewPrefabLoader(ctx util.ClientCtx) PrefabLoader { +func NewPrefabLoader(ctx appctx.ClientCtx) PrefabLoader { return PrefabLoader{ctx: ctx} } @@ -103,9 +103,9 @@ func (pfbl PrefabLoader) PrefabRoomsList() []Room { room.Connectors = append(room.Connectors, types.Coords{i,j}) } else { f, ok = TileTypeMap[shortName] - } - if (!ok) { - pfbl.ctx.Logger().Warn().Msgf("Unknown tile: %s", shortName) + if (!ok) { + pfbl.ctx.Logger().Warn().Msgf("Unknown tile: %s", shortName) + } } room.Geometry[i+ j*room.W] = f } diff --git a/engine/types/egde.go b/engine/types/egde.go new file mode 100644 index 0000000..3a29e93 --- /dev/null +++ b/engine/types/egde.go @@ -0,0 +1,6 @@ +package types + +type Edge struct { + From Coords + To Coords +} diff --git a/engine/types/screen.go b/engine/types/screen.go index 8d1dc43..5b0693d 100644 --- a/engine/types/screen.go +++ b/engine/types/screen.go @@ -1,7 +1,7 @@ package types import ( - "lab.zaar.be/thefish/alchemyst-go/util" + "lab.zaar.be/thefish/alchemyst-go/util/appctx" ) type Screen interface { @@ -13,14 +13,14 @@ type Screen interface { } type ScreenManager struct { - ctx util.ClientCtx - Screens map[string]Screen - CurrentScreen Screen + ctx appctx.ClientCtx + Screens map[string]Screen + CurrentScreen Screen PreviousScreen Screen } // NewScreenManager is a convenience/constructor method to properly initialize a new ScreenManager -func NewScreenManager(ctx util.ClientCtx) *ScreenManager { +func NewScreenManager(ctx appctx.ClientCtx) *ScreenManager { manager := ScreenManager{ ctx:ctx, Screens: make(map[string]Screen), diff --git a/go.mod b/go.mod index fc2333a..3f31c8f 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ module lab.zaar.be/thefish/alchemyst-go go 1.12 require ( + github.com/algds/kruskals v0.0.0-20190413211421-a585973d6c2d + github.com/algds/uf v0.0.0-20190413195204-b738ae1ba607 // indirect github.com/gammazero/deque v0.0.0-20190521012701-46e4ffb7a622 - github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/rs/zerolog v1.15.0 lab.zaar.be/thefish/bearlibterminal v0.0.0-20191018101635-dd37bbc90d77 ) diff --git a/go.sum b/go.sum index 50710e0..bf4bd83 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ +github.com/algds/kruskals v0.0.0-20190413211421-a585973d6c2d h1:m2rYLmJHZ3Ovi1CHt/WMpLyzROYGo8mPv8Xyyg/CIvE= +github.com/algds/kruskals v0.0.0-20190413211421-a585973d6c2d/go.mod h1:hWZ485ODixj1hNmb5Yc1+cArW+TUDXuiN4GAQ2Ly1FA= +github.com/algds/uf v0.0.0-20190413195204-b738ae1ba607 h1:HUvvlFhycn8608ZhNng9Tzsq4oy7dXnsmg5O4A8awaQ= +github.com/algds/uf v0.0.0-20190413195204-b738ae1ba607/go.mod h1:tUu0GH4rWbae7BeKBuU/Wbdmp0MkIQwtniqtXBfFgJ0= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/gammazero/deque v0.0.0-20190521012701-46e4ffb7a622 h1:lxbhOGZ9pU3Kf8P6lFluUcE82yVZn2EqEf4+mWRNPV0= github.com/gammazero/deque v0.0.0-20190521012701-46e4ffb7a622/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w= -github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o= -github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= diff --git a/ui/keyinput.go b/ui/keyinput.go index 5296710..4f1e6ea 100644 --- a/ui/keyinput.go +++ b/ui/keyinput.go @@ -27,7 +27,7 @@ func ReadKey() (string, int) { if blt.Check(blt.TK_CONTROL) != 0 { pressed = "Ctrl+" + pressed } - //ctx.Logger().Debug().Msg(pressed) + //appctx.Logger().Debug().Msg(pressed) } return pressed, key diff --git a/util/context.go b/util/appctx/context.go similarity index 65% rename from util/context.go rename to util/appctx/context.go index 96efdf3..3a72699 100644 --- a/util/context.go +++ b/util/appctx/context.go @@ -1,9 +1,10 @@ -package util +package appctx import ( "context" "fmt" "github.com/rs/zerolog" + "lab.zaar.be/thefish/alchemyst-go/util" ) const ( @@ -15,15 +16,15 @@ type ClientCtx struct { ctx context.Context } -func NewClientContext(config *Config, logger *zerolog.Logger) ClientCtx { +func NewClientContext(config *util.Config, logger *zerolog.Logger) ClientCtx { ctx := context.Context(context.TODO()) ctx = context.WithValue(ctx, configKey, config) ctx = context.WithValue(ctx, loggerKey, logger) return ClientCtx{ctx: ctx} } -func (c *ClientCtx) Config() *Config { - cfg, ok := c.ctx.Value(configKey).(*Config) +func (c *ClientCtx) Config() *util.Config { + cfg, ok := c.ctx.Value(configKey).(*util.Config) if !ok { panic(fmt.Errorf("no access to config from context")) } @@ -38,7 +39,7 @@ func (c *ClientCtx) Logger() *zerolog.Logger { return logger } -//func FromContext(ctx context.Context) (*User, bool) { -// u, ok := ctx.Value(userKey).(*User) +//func FromContext(appctx context.Context) (*User, bool) { +// u, ok := appctx.Value(userKey).(*User) // return u, ok //} diff --git a/util/delaunay/delaunay.go b/util/delaunay/delaunay.go new file mode 100644 index 0000000..58ac466 --- /dev/null +++ b/util/delaunay/delaunay.go @@ -0,0 +1,256 @@ +package delaunay + +//Original algo courtesy of github.com/esimov/triangle + +import "lab.zaar.be/thefish/alchemyst-go/engine/types" + +var nodeId = 0 +var nodeList = make(map[types.Coords]Node, 0) +// Node defines a struct having as components the node X and Y coordinate position. +type Node struct { + Id int + types.Coords +} + +// Struct which defines a circle geometry element. +type circle struct { + types.Coords + Radius int +} + +// newNode creates a new node. +func newNode(coords types.Coords) Node { + if n, ok := nodeList[coords]; ok { + return n + } + neue := Node{Id: nodeId, Coords: coords} + nodeList[coords] = neue + nodeId++ + return neue +} + +// isEq check if two Nodes are approximately equals. +func (this Node) isEq(other Node) bool { + dx := this.X - other.X + dy := this.Y - other.Y + + if dx < 0 { + dx = -dx + } + if dy < 0 { + dy = -dy + } + if float64(dx) < 0.0001 && float64(dy) < 0.0001 { + return true + } + return false +} + +// Edge struct having as component the node list. +type Edge struct { + Nodes []Node +} + +// newEdge creates a new Edge. +func newEdge(this, next Node) []Node { + nodes := []Node{this, next} + return nodes +} + +// isEq check if two Edge are approximately equals. +func (e Edge) isEq(edge Edge) bool { + na := e.Nodes + nb := edge.Nodes + na0, na1 := na[0], na[1] + nb0, nb1 := nb[0], nb[1] + + if (na0.isEq(nb0) && na1.isEq(nb1)) || + (na0.isEq(nb1) && na1.isEq(nb0)) { + return true + } + return false +} + +// Triangle struct defines the basic components of a triangle. +// It's constructed by Nodes, it's Edges and the circumcircle which describes the triangle circumference. +type Triangle struct { + Nodes []Node + Edges []Edge + circle circle +} + +var t = Triangle{} + +// newTriangle creates a new triangle which circumcircle encloses the point to be added. +func (t Triangle) newTriangle(p0, p1, p2 Node) Triangle { + t.Nodes = []Node{p0, p1, p2} + t.Edges = []Edge{{newEdge(p0, p1)}, {newEdge(p1, p2)}, {newEdge(p2, p0)}} + + // Create a circumscribed circle of this triangle. + // The circumcircle of a triangle is the circle which has the three vertices of the triangle lying on its circumference. + circle := t.circle + ax, ay := p1.X-p0.X, p1.Y-p0.Y + bx, by := p2.X-p0.X, p2.Y-p0.Y + + m := p1.X*p1.X - p0.X*p0.X + p1.Y*p1.Y - p0.Y*p0.Y + u := p2.X*p2.X - p0.X*p0.X + p2.Y*p2.Y - p0.Y*p0.Y + s := 1.0 / (2.0 * (float64(ax*by) - float64(ay*bx))) + + circle.Coords.X = int(float64((p2.Y-p0.Y)*m+(p0.Y-p1.Y)*u) * s) + circle.Coords.Y = int(float64((p0.X-p2.X)*m+(p1.X-p0.X)*u) * s) + + // Calculate the distance between the node points and the triangle circumcircle. + dx := p0.X - circle.Coords.X + dy := p0.Y - circle.Coords.Y + + // Calculate the circle radius. + circle.Radius = dx*dx + dy*dy + t.circle = circle + + return t +} + +// Delaunay defines the main components for the triangulation. +type Delaunay struct { + width int + height int + triangles []Triangle +} + +// Init initialize the delaunay structure. +func (d *Delaunay) Init(width, height int) *Delaunay { + d.width = width + d.height = height + + d.triangles = nil + d.clear() + + return d +} + +var supertriangle1, supertriangle2 Triangle +// clear method clears the delaunay triangles slice. +func (d *Delaunay) clear() { + p0 := newNode(types.Coords{0,0}) + p1 := newNode(types.Coords{d.width, 0}) + p2 := newNode(types.Coords{d.width, d.height}) + p3 := newNode(types.Coords{0, d.height}) + + // Create the supertriangle, an artificial triangle which encompasses all the points. + // At the end of the triangulation process any triangles which share Edges with the supertriangle are deleted from the triangle list. + supertriangle1 = t.newTriangle(p0,p1,p2) + supertriangle2 = t.newTriangle(p0,p2,p3) + d.triangles = []Triangle{supertriangle1, supertriangle2} +} + +// Insert will insert new triangles into the triangles slice. +func (d *Delaunay) Insert(points []types.Coords) *Delaunay { + var ( + i, j, k int + x, y, dx, dy int + distSq int + polygon []Edge + edges []Edge + temps []Triangle + ) + + for k = 0; k < len(points); k++ { + x = points[k].X + y = points[k].Y + + triangles := d.triangles + edges = nil + temps = nil + + for i = 0; i < len(d.triangles); i++ { + t := triangles[i] + + //Check whether the points are inside the triangle circumcircle. + circle := t.circle + dx = circle.Coords.X - x + dy = circle.Coords.Y - y + distSq = dx*dx + dy*dy + + if distSq < circle.Radius { + // Save triangle Edges in case they are included. + edges = append(edges, t.Edges[0], t.Edges[1], t.Edges[2]) + } else { + // If not included carry over. + temps = append(temps, t) + } + } + + polygon = nil + // Check duplication of Edges, delete if duplicates. + edgesLoop: + for i = 0; i < len(edges); i++ { + edge := edges[i] + for j = 0; j < len(polygon); j++ { + // Remove identical Edges. + if edge.isEq(polygon[j]) { + // Remove polygon from the polygon slice. + polygon = append(polygon[:j], polygon[j+1:]...) + continue edgesLoop + } + } + // Insert new Edge into the polygon slice. + polygon = append(polygon, edge) + + } + for i = 0; i < len(polygon); i++ { + edge := polygon[i] + temps = append(temps, t.newTriangle(edge.Nodes[0], edge.Nodes[1], newNode(types.Coords{x, y}))) + } + d.triangles = temps + } + + //Clean up supertriangles + z := 0 // output index + var valid bool + cleanList := make([]Triangle, 0) + for _, triangle := range d.triangles { + valid = true + for _, checkedNode := range triangle.Nodes { + for _, superNode := range supertriangle1.Nodes { + if checkedNode == superNode { + valid = false + break + } + } + for _, superNode := range supertriangle2.Nodes { + if checkedNode == superNode { + valid = false + break + } + } + if !valid { + break + } + } + if !valid { + continue + } + cleanList = append(cleanList, triangle) + z++ + } + d.triangles = cleanList + + //update node indices + return d +} + +// GetTriangles return the generated triangles. +func (d *Delaunay) GetTriangles() []Triangle { + return d.triangles +} + +func (d *Delaunay) GetEdges() []Edge { + edges := make([]Edge, 0) + for _, trs := range d.triangles { + for _, e := range trs.Edges { + edges = append(edges, e) + } + + } + return edges +} diff --git a/util/delaunay/mst.go b/util/delaunay/mst.go new file mode 100644 index 0000000..3715d13 --- /dev/null +++ b/util/delaunay/mst.go @@ -0,0 +1,61 @@ +package delaunay + +import ( + "lab.zaar.be/thefish/alchemyst-go/engine/types" + "lab.zaar.be/thefish/alchemyst-go/util/kruskals" +) +//FIXME ПЕРЕПИСАТЬ К ЕБЕНЯМ ДЕЛОНЕ. Это пиздец и си с крестами головного мозга. Надо сделать по-человечески. + +func GetTriangles(coords []types.Coords, w, h int) []types.Edge { + d := &Delaunay{} + edges := d.Init(100, 100).Insert(coords).GetEdges() + output := make([]types.Edge, 0) + for _, e := range edges{ + output = append(output, types.Edge{e.Nodes[0].Coords, e.Nodes[1].Coords}) + } + return output +} + +func GetMst(coords []types.Coords, w, h int) []types.Edge { + + d := &Delaunay{} + + edges := d.Init(w, h).Insert(coords).GetEdges() + + //MST + //fixme гребаный костыль + nodeMap := make(map[int]int, 0) + idx := 0 + nodeList := make(map[int]Node) + for _, e := range edges{ + if _, ok := nodeMap[e.Nodes[0].Id]; !ok{ + nodeMap[e.Nodes[0].Id] = idx + nodeList[idx] = e.Nodes[0] + idx++ + } + if _, ok := nodeMap[e.Nodes[1].Id]; !ok{ + nodeMap[e.Nodes[1].Id] = idx + nodeList[idx] = e.Nodes[1] + idx++ + } + } + //graph := make([]kruskals.WeightedEdge, len(edges)) + graph := make([]kruskals.WeightedEdge, 0) + for _, e := range edges{ + //graph[i] = kruskals.SimpleWeightedEdge{e.Nodes[0].Id, e.Nodes[1].Id, int(e.Nodes[0].Coords.DistanceTo(e.Nodes[1].Coords))} + graph = append( + graph, + kruskals.SimpleWeightedEdge{ + nodeMap[e.Nodes[0].Id], + nodeMap[e.Nodes[1].Id], + int(e.Nodes[0].Coords.DistanceTo(e.Nodes[1].Coords))}, + ) + } + + result := kruskals.MinimumSpanningTree(graph) + output := make([]types.Edge, 0) + for _, we := range result{ + output = append(output, types.Edge{nodeList[we.From()].Coords, nodeList[we.To()].Coords}) + } + return output +} diff --git a/util/delenay.go b/util/delenay.go deleted file mode 100644 index c7d8682..0000000 --- a/util/delenay.go +++ /dev/null @@ -1 +0,0 @@ -package util diff --git a/util/kruskals/algorithm.go b/util/kruskals/algorithm.go new file mode 100644 index 0000000..caf3c3a --- /dev/null +++ b/util/kruskals/algorithm.go @@ -0,0 +1,60 @@ +package kruskals + +import ( + "lab.zaar.be/thefish/alchemyst-go/util/uf" + "sort" + +) + +// WeightedEdge is an undirected edge between vertices and a weight. +type WeightedEdge interface { + // From returns the integer identifier of the first vertex. + From() int + // To returns the integer identifier of the second vertex. + To() int + // Weight returns the integer identifier of the weight/cost. + Weight() int +} + +// SimpleWeightedEdge is a simple implementation of the interface. +// Initialize with 'F' (From), 'T' (To), 'W' (Weight). +type SimpleWeightedEdge struct { + F, T, W int +} + +// From returns the integer identifier of the first vertex. +func (s SimpleWeightedEdge) From() int { + return s.F +} + +// To returns the integer identifier of the second vertex. +func (s SimpleWeightedEdge) To() int { + return s.T +} + +// Weight returns the integer identifier of the weight/cost. +func (s SimpleWeightedEdge) Weight() int { + return s.W +} + +// MinimumSpanningTree returns the fewest edges to span across the graph +// with the minimum cost based on weights. +func MinimumSpanningTree(edges []WeightedEdge) []WeightedEdge { + vertices := make(map[int]struct{}) + for _, e := range edges { + vertices[e.From()] = struct{}{} + vertices[e.To()] = struct{}{} + } + sort.Slice(edges, func(i, j int) bool { + return edges[i].Weight() < edges[j].Weight() + }) + u := uf.New(len(vertices)) + result := make([]WeightedEdge, 0, len(edges)) + for _, e := range edges { + if !u.Connected(e.From(), e.To()) { + u.Union(e.From(), e.To()) + result = append(result, e) + } + } + return result +} diff --git a/util/kruskals/algorithm_test.gp.go b/util/kruskals/algorithm_test.gp.go new file mode 100644 index 0000000..813b405 --- /dev/null +++ b/util/kruskals/algorithm_test.gp.go @@ -0,0 +1,47 @@ +package kruskals + +import ( +"reflect" +"testing" +) + +var testcases = []struct { + graph, mst []WeightedEdge +}{ + { + []WeightedEdge{ + SimpleWeightedEdge{0, 3, 3}, + SimpleWeightedEdge{3, 1, 30}, + SimpleWeightedEdge{0, 1, 20}, + SimpleWeightedEdge{0, 4, 10}, + SimpleWeightedEdge{1, 4, 5}, + SimpleWeightedEdge{4, 2, 20}, + SimpleWeightedEdge{1, 2, 50}, + SimpleWeightedEdge{3, 0, 3}, + }, + []WeightedEdge{ + SimpleWeightedEdge{0, 3, 3}, + SimpleWeightedEdge{1, 4, 5}, + SimpleWeightedEdge{0, 4, 10}, + SimpleWeightedEdge{4, 2, 20}, + }, + }, +} + +func TestMinimumSpanningTree(t *testing.T) { + t.Parallel() + for _, tc := range testcases { + if result := MinimumSpanningTree(tc.graph); !reflect.DeepEqual(result, tc.mst) { + t.Errorf("Expected %v, got %v", tc.mst, result) + } + } +} + +func BenchmarkMinimumSpanningTree(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, tc := range testcases { + MinimumSpanningTree(tc.graph) + } + } +} + diff --git a/util/uf/uf.go b/util/uf/uf.go new file mode 100644 index 0000000..35ea7b4 --- /dev/null +++ b/util/uf/uf.go @@ -0,0 +1,72 @@ +package uf + +import "fmt" + +// Interface is how you use the union-find data structure. +type Interface interface { + Find(p int) int + Count() int + Connected(p, q int) bool + Union(p, q int) + validate(p int) +} + +type instance struct { + parent []int + rank []byte + count int +} + +func (i *instance) Find(p int) int { + i.validate(p) + for p != i.parent[p] { + i.parent[p] = i.parent[i.parent[p]] + p = i.parent[p] + } + return p +} + +func (i *instance) Count() int { + return i.count +} + +func (i *instance) Connected(p, q int) bool { + return i.Find(p) == i.Find(q) +} + +func (i *instance) Union(p, q int) { + rootP := i.Find(p) + rootQ := i.Find(q) + if rootP == rootQ { + return + } + switch { + case i.rank[rootP] < i.rank[rootQ]: + i.parent[rootP] = rootQ + case i.rank[rootP] > i.rank[rootQ]: + i.parent[rootQ] = rootP + default: + i.parent[rootQ] = rootP + i.rank[rootP]++ + } + i.count-- +} + +func (i *instance) validate(p int) { + if n := len(i.parent); p < 0 || p >= n { + panic(fmt.Sprintf("index %d is not between 0 and %d", p, n-1)) + } +} + +// New creates a new instance of a union-find interface. +func New(n int) Interface { + created := &instance{ + parent: make([]int, n), + rank: make([]byte, n), + count: n, + } + for i := 0; i < n; i++ { + created.parent[i] = i + } + return created +}