Jonathan D. Storm 1 miesiąc temu
commit
ae747cca5b
4 zmienionych plików z 685 dodań i 0 usunięć
  1. 16 0
      go.mod
  2. 23 0
      go.sum
  3. 557 0
      main.go
  4. 89 0
      main_test.go

+ 16 - 0
go.mod

@@ -0,0 +1,16 @@
+module graph_layout
+
+go 1.23.4
+
+require gioui.org v0.7.1
+
+require (
+	gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
+	gioui.org/shader v1.0.8 // indirect
+	github.com/go-text/typesetting v0.1.1 // indirect
+	golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
+	golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
+	golang.org/x/image v0.18.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
+)

+ 23 - 0
go.sum

@@ -0,0 +1,23 @@
+eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
+eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
+gioui.org v0.7.1 h1:l7OVj47n1z8acaszQ6Wlu+Rxme+HqF3q8b+Fs68+x3w=
+gioui.org v0.7.1/go.mod h1:5Kw/q7R1BWc5MKStuTNvhCgSrRqbfHc9Dzfjs4IGgZo=
+gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
+gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
+gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
+gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
+gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
+github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
+github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
+github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
+github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
+golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
+golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
+golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=

+ 557 - 0
main.go

@@ -0,0 +1,557 @@
+package main
+
+import (
+	"fmt"
+	"image"
+	"image/color"
+	"log"
+	"math/rand"
+	"os"
+	"strings"
+	"time"
+
+	"gioui.org/app"
+	"gioui.org/f32"
+	"gioui.org/font/gofont"
+	"gioui.org/io/key"
+	"gioui.org/layout"
+	"gioui.org/op"
+	"gioui.org/op/clip"
+	"gioui.org/op/paint"
+	"gioui.org/text"
+	"gioui.org/unit"
+	"gioui.org/widget/material"
+)
+
+type C = layout.Context
+type D = layout.Dimensions
+
+var darkPalette = material.Palette{
+	Bg:         color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff},
+	Fg:         color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
+	ContrastBg: color.NRGBA{R: 0x48, G: 0x85, B: 0x88, A: 0xff},
+	ContrastFg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
+}
+
+func main() {
+	go func() {
+		window := new(app.Window)
+		err := run(window)
+		if err != nil {
+			log.Fatal(err)
+		}
+		os.Exit(0)
+	}()
+
+	app.Main()
+}
+
+func run(window *app.Window) error {
+	theme := material.NewTheme()
+	theme.Palette = darkPalette
+	theme.Shaper =
+		text.NewShaper(text.WithCollection(gofont.Collection()))
+
+	g := Graph{}
+	g.random = rand.New(rand.NewSource(0))
+	g.offsets = g._offsets[:]
+	g.nodes = g._nodes[:0]
+	g.allocs = g._allocs[:]
+	g.edges = g._edges[:0]
+
+	//generateRandomGraph(&g)
+
+	title := material.Body1(theme, "")
+	title.Color = theme.Fg
+	title.Alignment = text.Start
+
+	simPeriod := 50 * time.Millisecond
+	lastStep := time.Now()
+	bumpShift := 1
+	lastSecond := time.Now()
+	frameCnt := 0
+	stepCnt := 0
+	fps, sps := 0, 0
+	lastFinish := time.Now()
+	var (
+		stepOut         strings.Builder
+		statusText      strings.Builder
+		ops             op.Ops
+		screensaverMode bool
+		finished        bool
+	)
+	for {
+		switch e := window.Event().(type) {
+		case app.DestroyEvent:
+			return e.Err
+
+		case app.FrameEvent:
+			gtx := app.NewContext(&ops, e)
+
+			// Handle input
+			var (
+				resetRequested,
+				bumpRequested bool
+			)
+			for {
+				event, ok := e.Source.Event(
+					key.Filter{Name: "B"},
+					key.Filter{Name: "R"},
+					key.Filter{Name: "S", Required: key.ModShift},
+					key.Filter{Name: "<", Required: key.ModShift},
+					key.Filter{Name: ">", Required: key.ModShift},
+				)
+				if !ok {
+					break
+				}
+				switch evt := event.(type) {
+				case key.Event:
+					switch evt.Name {
+					case "B":
+						if evt.State == key.Press {
+							bumpRequested = true
+						}
+					case "R":
+						if evt.State == key.Press {
+							resetRequested = true
+						}
+					case "S":
+						if evt.State == key.Press {
+							screensaverMode = true
+						}
+					case "<":
+						if evt.State == key.Press {
+							simPeriod += 50 * time.Millisecond
+							simPeriod = min(simPeriod, 10*time.Second)
+						}
+					case ">":
+						if evt.State == key.Press {
+							simPeriod -= 50 * time.Millisecond
+							simPeriod = max(simPeriod, 0)
+						}
+					}
+				}
+			}
+			if resetRequested ||
+				(screensaverMode &&
+					time.Since(lastFinish) >= 10*time.Second) {
+				(&g).Reset()
+				lastFinish = time.Now()
+				finished = false
+			}
+			paint.Fill(&ops, theme.Bg)
+
+			//margin := layout.UniformInset(unit.Dp(8))
+			layout.Flex{
+				Axis:      layout.Vertical,
+				Alignment: layout.Start,
+				Spacing:   layout.SpaceEnd,
+			}.Layout(gtx,
+				layout.Rigid(func(gtx C) D {
+					return (&g).Layout(gtx, theme)
+				}),
+				layout.Rigid(func(gtx C) D {
+					return title.Layout(gtx)
+				}),
+				layout.Rigid(func(gtx C) D {
+					return (&g).LayoutDist(gtx, theme)
+				}),
+			)
+
+			if time.Since(lastSecond) >= time.Second {
+				fps, sps = frameCnt, stepCnt
+				frameCnt, stepCnt = 0, 0
+				lastSecond = time.Now()
+			}
+
+			statusText.Reset()
+			statusText.WriteString(fmt.Sprintf("simulation rate: %s per step\n", simPeriod))
+			statusText.WriteString(fmt.Sprintf("fps=%d; sps=%d\n", fps, sps))
+
+			if time.Since(lastStep) >= simPeriod {
+				if int(g.order) == cap(g.nodes) && bumpRequested {
+					bump(&g, bumpShift)
+					bumpShift++
+				}
+				stepOut.Reset()
+				stepOut.WriteString((&g).Step())
+				//statusText.WriteString((&g).Semilattice())
+				lastStep = time.Now()
+
+				addRandomNode(&g)
+				stepCnt++
+			}
+			statusText.WriteString(stepOut.String())
+			title.Text = statusText.String()
+
+			if int(g.order) == cap(g.nodes) && !finished {
+				finished = true
+			}
+			e.Source.Execute(op.InvalidateCmd{})
+
+			e.Frame(gtx.Ops)
+			frameCnt++
+		}
+	}
+}
+
+func bump(g *Graph, shift int) int {
+	root := -1
+	for n := len(g.nodes) - 1; n >= 0; n-- {
+		if g.nodes[n] == 1 {
+			root = n
+			break
+		}
+	}
+	if root < 0 {
+		return root
+	}
+	g.nodes[root] <<= shift
+
+	nodes := make([]NodeId, 1, g.order)
+	nextNodes := make(map[NodeId]struct{}, g.order)
+	nodes[0] = NodeId(root)
+
+	for len(nodes) > 0 {
+		for _, n := range nodes {
+			for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
+				nextNodes[e] = struct{}{}
+			}
+		}
+		nodes = nodes[:len(nextNodes)]
+		i := 0
+		for n := range nextNodes {
+			delete(nextNodes, n)
+			nodes[i] = n
+			i++
+			if log2(g.nodes[n])+uint32(shift) >= 31 {
+				// Out of address space
+				continue
+			}
+			g.nodes[n] <<= shift
+		}
+	}
+	return root
+}
+
+func addRandomNode(g *Graph) (order uint32) {
+	order = g.order
+	if g.order == uint32(cap(g.nodes)) ||
+		g.size == uint32(cap(g.edges)) {
+		return
+	}
+	threshold := 0.02 * float64(g.order)
+
+	if g.random.ExpFloat64() <= threshold {
+		return
+	}
+	g.edges = g.edges[:cap(g.edges)]
+	g.nodes = g.nodes[:cap(g.nodes)]
+
+	i := int(g.order)
+	g.nodes[i] = 1
+	g.offsets[i] = EdgeId(g.size)
+
+	threshold = float64(2.0)
+
+	for j := 1; i+j < len(g.nodes); j++ {
+		g.edges[g.size] = NodeId(i + j)
+
+		if g.random.ExpFloat64() <= threshold {
+			continue
+		}
+		g.size++
+	}
+	g.order++
+	order = g.order
+	g.offsets[g.order] = EdgeId(g.size)
+	g.nodes = g.nodes[:g.order]
+	g.edges = g.edges[:g.size]
+
+	return
+}
+
+func generateRandomGraph(g *Graph) {
+	k := EdgeId(0) // Graph size
+
+	for n := 0; n < len(g.nodes); n++ {
+		g.nodes[n] = 1
+		g.offsets[n] = k
+
+		weight := float64(0)
+		for i := 1; n+i < len(g.nodes); i++ {
+			g.edges[k] = NodeId(n + i)
+
+			threshold :=
+				1.4 + weight*float64(n)/float64(len(g.nodes))
+
+			if g.random.ExpFloat64() <= threshold {
+				continue
+			}
+			weight++
+			k++
+		}
+	}
+	g.offsets[len(g.nodes)] = k
+	g.edges = g.edges[:k]
+}
+
+type NodeId uint32
+type EdgeId uint32
+
+const GraphOrder = 64
+
+type Graph struct {
+	_offsets [GraphOrder + 1]EdgeId
+	_nodes   [GraphOrder]uint32
+	_allocs  [GraphOrder]uint32
+	_edges   [(GraphOrder * (GraphOrder - 1)) >> 2]NodeId
+	random   *rand.Rand
+	offsets  []EdgeId
+	nodes    []uint32
+	allocs   []uint32 // Address allocation state, per node
+	edges    []NodeId
+	size     uint32 // Number of edges added to graph
+	order    uint32 // Number of nodes added to graph
+}
+
+func (g *Graph) Reset() {
+	for o := range g.offsets {
+		g.offsets[o] = 0
+	}
+	for n := range g.nodes {
+		g.nodes[n] = 0
+	}
+	for a := range g.allocs {
+		g.allocs[a] = 0
+	}
+	for e := range g.edges {
+		g.edges[e] = 0
+	}
+	g.nodes = g.nodes[:0]
+	g.edges = g.edges[:0]
+	g.order, g.size = 0, 0
+}
+
+func (g *Graph) Semilattice() string {
+	var b strings.Builder
+	for n := 0; n < len(g.nodes); n++ {
+		b.WriteString(fmt.Sprintf("nodeAddr=%d neighbors=", g.nodes[n]))
+
+		for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
+			if uint32(e) >= g.order {
+				continue
+			}
+			b.WriteString(fmt.Sprintf("%d, ", g.nodes[e]))
+		}
+		b.WriteString("\n")
+	}
+	return b.String()
+}
+
+func (g *Graph) Step() string {
+	for n := range g.nodes {
+		if g.allocs[n] == 0 {
+			g.allocs[n] = 1 // Seed allocator
+		}
+		shift := log2(g.nodes[n]) + 1
+
+		// Assign addresses to neighbors
+		for _, e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
+			if uint32(e) >= g.order ||
+				g.nodes[n] < g.nodes[e] {
+				continue
+			}
+			if log2(g.allocs[n])+shift >= 30 {
+				// Out of address space
+				continue
+			}
+			nextAddr := (g.allocs[n] << shift) | g.nodes[n]
+			g.nodes[e] = nextAddr
+			g.allocs[n]++
+		}
+	}
+	return fmt.Sprintf("nodes=%v\n", g.nodes)
+}
+
+func (g *Graph) Layout(
+	gtx layout.Context,
+	th *material.Theme,
+) D {
+	squareMax := min(
+		gtx.Constraints.Max.X,
+		gtx.Constraints.Max.Y,
+	)
+	convFactor := float32(squareMax) / 65535.0
+	circle := clip.Ellipse{Max: image.Pt(16, 16)}
+
+	defer op.Offset(image.Pt(10, 10)).Push(gtx.Ops).Pop()
+
+	var path clip.Path
+	for n := 0; n < len(g.nodes); n++ {
+		if g.nodes[n] == 1 { // Ignore root and orphaned nodes
+			continue
+		}
+		x1, y1 := idToPt(g.nodes[n], convFactor)
+
+		for e := range g.edges[g.offsets[n]:g.offsets[n+1]] {
+			if uint32(e) >= g.order {
+				continue
+			}
+			color := th.Fg
+			if distance(g.nodes[n], g.nodes[e]) > 4 {
+				color = th.ContrastBg
+			}
+			x2, y2 := idToPt(g.nodes[e], convFactor)
+
+			path.Begin(gtx.Ops)
+			path.MoveTo(f32.Pt(float32(x1), float32(y1)))
+			path.LineTo(f32.Pt(float32(x2), float32(y2)))
+			paint.FillShape(gtx.Ops, color,
+				clip.Stroke{
+					Path:  path.End(),
+					Width: float32(unit.Dp(1)),
+				}.Op(),
+			)
+		}
+		shift := int(log2(g.nodes[n]) >> 3)
+		scaled := circle
+		scaled.Max.X = scaled.Max.X >> shift
+		scaled.Max.Y = scaled.Max.Y >> shift
+		halfW, halfH := scaled.Max.X>>1, scaled.Max.Y>>1
+
+		pos := image.Pt(gtx.Dp(x1)-halfW, gtx.Dp(y1)-halfH)
+
+		stack := op.Offset(pos).Push(gtx.Ops)
+		paint.FillShape(gtx.Ops, th.Fg, scaled.Op(gtx.Ops))
+		stack.Pop()
+	}
+	return layout.Dimensions{Size: image.Pt(squareMax, squareMax)}
+}
+
+func (g *Graph) LayoutDist(
+	gtx layout.Context,
+	th *material.Theme,
+) D {
+	dims := f32.Pt(400, 100)
+	width := gtx.Dp(unit.Dp(dims.X))
+	height := gtx.Dp(unit.Dp(dims.Y))
+
+	defer op.Affine( // Flip y-axis and scale
+		f32.Affine2D{}.
+			Scale(f32.Pt(dims.X/2, dims.Y/2), f32.Pt(1.0, -1.0)),
+	).Push(gtx.Ops).Pop()
+
+	barWidth := 0
+	if g.order > 0 {
+		pxPerBucket := float32(width) / float32(g.order)
+		barWidth = gtx.Dp(unit.Dp(pxPerBucket))
+	}
+	color := color.NRGBA{R: 0xaa, G: 0, B: 0xaa, A: 0xff}
+
+	maxOutDegree := 0
+	for n := range g.nodes {
+		d := g.offsets[n+1] - g.offsets[n]
+		maxOutDegree = max(maxOutDegree, int(d))
+	}
+	convFactor := float64(height) / float64(maxOutDegree)
+
+	for n := range g.nodes {
+		cnt := g.offsets[n+1] - g.offsets[n]
+		y := gtx.Dp(unit.Dp(float64(cnt) * convFactor))
+		stack := clip.Rect{
+			Min: image.Pt(n*barWidth, 0),
+			Max: image.Pt((n+1)*barWidth, y),
+		}.Push(gtx.Ops)
+
+		paint.ColorOp{Color: color}.Add(gtx.Ops)
+		paint.PaintOp{}.Add(gtx.Ops)
+		stack.Pop()
+	}
+	return layout.Dimensions{Size: image.Pt(width, height)}
+}
+
+func distance(a, b uint32) uint32 {
+	var s, e uint32 = 1, 2
+	for s < 32 && a&(e-1) == b&(e-1) {
+		s++
+		e <<= 1
+	}
+	s -= 1
+	return ones(a>>s) + ones(b>>s)
+}
+
+func ones(x uint32) uint32 {
+	var c uint32
+	for c = 0; x != 0; c++ {
+		x &= x - 1 // clear the least significant bit set
+	}
+	return c
+}
+
+func idToPt(id uint32, convFactor float32) (x, y unit.Dp) {
+	x0, y0 := demorton(reverse32(id))
+
+	x, y = unit.Dp(float32(x0)*convFactor),
+		unit.Dp(float32(y0)*convFactor)
+
+	return
+}
+
+func log2(x uint32) uint32 {
+	b := []uint32{0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000}
+	S := []uint32{1, 2, 4, 8, 16}
+
+	var r uint32 = 0
+
+	for i := 4; i >= 0; i-- {
+		if x&b[i] != 0 {
+			x >>= S[i]
+			r |= S[i]
+		}
+	}
+	return r
+}
+
+func reverse32(x uint32) uint32 {
+	var rev uint32
+	var mask uint32 = 0xff
+
+	for i := 0; i < 4; i++ {
+		rshift := 8 * i
+		lshift := 8 * (3 - i)
+		rev |= uint32(reverse((x&mask)>>rshift)) << lshift
+		mask <<= 8
+	}
+	return rev
+}
+
+func reverse(b uint32) byte {
+	b = (b * 0x0802 & 0x22110) | (b * 0x8020 & 0x88440)
+	b *= 0x10101
+	b >>= 16
+	return byte(b)
+}
+
+func demorton(z uint32) (x, y uint16) {
+	res := (uint64(z) | (uint64(z) << 31)) & 0x5555555555555555
+	res = (res | (res >> 1)) & 0x3333333333333333
+	res = (res | (res >> 2)) & 0x0f0f0f0f0f0f0f0f
+	res = (res | (res >> 4)) & 0x00ff00ff00ff00ff
+	res = res | (res >> 8)
+	x = uint16(res)
+	y = uint16(res >> 32)
+	return
+}
+
+func morton(x, y uint16) uint32 {
+	var res uint64
+
+	res = uint64(x) | (uint64(y) << 32)
+	res = (res | (res << 8)) & 0x00ff00ff00ff00ff
+	res = (res | (res << 4)) & 0x0f0f0f0f0f0f0f0f
+	res = (res | (res << 2)) & 0x3333333333333333
+	res = (res | (res << 1)) & 0x5555555555555555
+
+	return uint32(res | (res >> 31))
+}

+ 89 - 0
main_test.go

@@ -0,0 +1,89 @@
+package main
+
+import (
+	"testing"
+)
+
+func TestMorton(t *testing.T) {
+	var x, y uint16 = 0x0000, 0xffff
+	var expected uint32 = 0xaaaaaaaa
+
+	actual := morton(x, y)
+	if actual != expected {
+		t.Errorf("expected %b; got M(%b, %b) = %b", expected, x, y, actual)
+	}
+	a, b := demorton(expected)
+	if a != x || b != y {
+		t.Errorf("expected (%b, %b); got (%b, %b)", x, y, a, b)
+	}
+}
+
+func TestReverse(t *testing.T) {
+	var x byte = 0b1101
+	var expected byte = 0b10110000
+
+	actual := reverse(uint32(x))
+	if actual != expected {
+		t.Errorf("expected %b; got %b", expected, actual)
+	}
+}
+
+func TestReverse32(t *testing.T) {
+	var x uint32 = 0xaaaaaaaa
+	var expected uint32 = 0x55555555
+
+	actual := reverse32(x)
+	if actual != expected {
+		t.Errorf("expected %b; got %b", expected, actual)
+	}
+
+	x = 0x00000001
+	expected = 0x80000000
+
+	actual = reverse32(x)
+	if actual != expected {
+		t.Errorf("expected %b; got %b", expected, actual)
+	}
+}
+
+func TestLog2(t *testing.T) {
+	var x uint32 = 0x80000000
+	var expected uint32 = 31
+
+	actual := log2(x)
+	if actual != expected {
+		t.Errorf("expected %d; got %d", expected, actual)
+	}
+
+	x = 3
+	expected = 1
+	actual = log2(x)
+	if actual != expected {
+		t.Errorf("expected %d; got %d", expected, actual)
+	}
+}
+
+func TestOnes(t *testing.T) {
+	var x uint32 = 0xaaaaaaaa
+	var expected uint32 = 16
+
+	actual := ones(x)
+	if actual != expected {
+		t.Errorf("expected %d; got %d", expected, actual)
+	}
+}
+
+func TestDistance(t *testing.T) {
+	a := uint32(0b0000000_00000101_01000101_01111001)
+	b := uint32(0b0000000_00000010_01000111_11011001)
+	expected := uint32(14)
+
+	actual := distance(a, b)
+	if actual != expected {
+		t.Errorf("expected %d; got %d", expected, actual)
+	}
+	actual = distance(b, a)
+	if actual != expected {
+		t.Errorf("expected %d; got %d", expected, actual)
+	}
+}