diff --git a/README.md b/README.md index 444e737..6ecf88f 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ [![codecov](https://codecov.io/gh/mrsombre/codingame-framework/graph/badge.svg?token=I8RYIUSN6Q)](https://codecov.io/gh/mrsombre/codingame-framework) -A set of algorithms and data structures for solving puzzles. - -Feel free to follow me on [Codingame Profile](https://www.codingame.com/profile/9dd9f9f38412d78eaf21718bf6e87ca0626964). +A collection of algorithms and data structures designed for solving programming puzzles. ### Golang Version @@ -21,3 +19,6 @@ go test -cover ./... ```shell go test -bench=. -benchmem -run=^$ > bench.out ``` + +--- +You are welcome to follow my [Codingame profile](https://www.codingame.com/profile/9dd9f9f38412d78eaf21718bf6e87ca0626964) diff --git a/all_test.go b/all_test.go index 14d5b2b..248ade3 100644 --- a/all_test.go +++ b/all_test.go @@ -6,6 +6,4 @@ var ( GlobalB bool GlobalI int GlobalF float64 - - GlobalPoint Point ) diff --git a/bench.out b/bench.out index c9fbb35..6226659 100644 --- a/bench.out +++ b/bench.out @@ -2,8 +2,9 @@ goos: linux goarch: amd64 pkg: github.com/mrsombre/codingame-framework cpu: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz -BenchmarkLine_IsCollision-8 271175239 4.403 ns/op 0 B/op 0 allocs/op -BenchmarkLine_LinesIntersection-8 351207843 3.375 ns/op 0 B/op 0 allocs/op -BenchmarkLine_IsPointOnLine-8 515632399 2.412 ns/op 0 B/op 0 allocs/op +BenchmarkIsPointOnLine 531822363 2.250 ns/op 0 B/op 0 allocs/op +BenchmarkClosestPoint 100000000 10.82 ns/op 0 B/op 0 allocs/op +BenchmarkLinesIntersection 353300368 3.435 ns/op 0 B/op 0 allocs/op +BenchmarkLine_IsCollision 273297457 4.427 ns/op 0 B/op 0 allocs/op PASS -ok github.com/mrsombre/codingame-framework 4.666s +ok github.com/mrsombre/codingame-framework 5.733s diff --git a/command.go b/command.go new file mode 100644 index 0000000..542a24d --- /dev/null +++ b/command.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "io" + "os" +) + +// Command is an interface for game commands. + +var commandOutput io.Writer = os.Stdout + +type Command interface { + String() string +} + +type Commands []Command + +type MockCommand struct { + Param1 float64 + Param2 float64 +} + +func (c MockCommand) String() string { + return fmt.Sprintf("%.f %.f", c.Param1, c.Param2) +} + +func ExecuteCommands(commands Commands) { + for _, command := range commands { + fmt.Fprintln(commandOutput, command) + } +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..416ecbc --- /dev/null +++ b/command_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockWriter struct { + data []byte +} + +func (w *mockWriter) Write(p []byte) (n int, err error) { + w.data = p + return len(p), nil +} + +func TestMockCommand_String(t *testing.T) { + cmd := MockCommand{1, 2} + assert.Equal(t, "1 2", cmd.String()) +} + +func TestExecuteCommand(t *testing.T) { + commandOutput = &mockWriter{} + ExecuteCommands(Commands{MockCommand{1, 2}}) + + want := "1 2\n" + got := commandOutput.(*mockWriter).data + assert.Equal(t, want, string(got)) +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..4f5f707 --- /dev/null +++ b/data.go @@ -0,0 +1,68 @@ +package main + +// Export/Import of data in the form of string arrays. +// This can be used to unload the conditions of a problem (input) +// in a compressed form into the debug console and unpack it in the IDE. + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" +) + +// DataExport serializes and compresses a slice of strings, +// returning a base64 encoded string. +func DataExport(data []string) string { + var err error + + jsonData, err := json.Marshal(data) + if err != nil { + panic(err) + } + + var gzBuf bytes.Buffer + gz := gzip.NewWriter(&gzBuf) + if _, err = gz.Write(jsonData); err != nil { + panic(err) + } + if err = gz.Close(); err != nil { + panic(err) + } + + return base64.StdEncoding.EncodeToString(gzBuf.Bytes()) +} + +// DataImport decodes a base64 string, decompresses it, +// and deserializes the JSON data into a slice of strings. +func DataImport(encodedData string) []string { + var err error + + gzData, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + panic(err) + } + + gz, err := gzip.NewReader(bytes.NewBuffer(gzData)) + if err != nil { + panic(err) + } + defer func(gz *gzip.Reader) { + err = gz.Close() + if err != nil { + panic(err) + } + }(gz) + + var jsonData bytes.Buffer + if _, err = jsonData.ReadFrom(gz); err != nil { + panic(err) + } + + var data []string + if err = json.Unmarshal(jsonData.Bytes(), &data); err != nil { + panic(err) + } + + return data +} diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..6dce732 --- /dev/null +++ b/data_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var dataExportTests = []string{ + "123", + "abc", +} + +func TestDataExport(t *testing.T) { + data := DataExport(dataExportTests) + assert.Equal(t, dataImportTests, data) +} + +var dataImportTests = `H4sIAAAAAAAA/4pWMjQyVtJRSkxKVooFBAAA//9iXM2zDQAAAA==` + +func TestDataImport(t *testing.T) { + data := DataImport(dataImportTests) + assert.Equal(t, dataExportTests, data) +} diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..aa60f30 --- /dev/null +++ b/debug.go @@ -0,0 +1,39 @@ +package main + +// A set of helper methods for outputting debug information +// to the Stderr stream in text or JSON format. + +import ( + "encoding/json" + "fmt" + "os" +) + +var debug = true +var debugOutput = os.Stderr + +func asText(a ...any) { + if !debug { + return + } + fmt.Fprintln(debugOutput, a...) +} + +func asJson(a any) { + if !debug { + return + } + b, _ := json.Marshal(a) + asText(string(b)) +} + +func asJsonPretty(a any) { + if !debug { + return + } + b, _ := json.MarshalIndent(a, ``, ` `) + asText(string(b)) +} + +func u(a ...any) { +} diff --git a/geometry_line.go b/geometry_line.go index de0b851..1bf968c 100644 --- a/geometry_line.go +++ b/geometry_line.go @@ -58,44 +58,117 @@ func (ln Line) Segment(length float64) Line { if length == 0 { return Line{ln.From, ln.From} } - - v := ln.Vector() - vl := ln.Length() - ux := v.X / vl - uy := v.Y / vl + nv := ln.Vector().Normalize(ln.Length()) return Line{ ln.From, NewPoint( - ln.From.X+ux*length, - ln.From.Y+uy*length, + ln.From.X+nv.X*length, + ln.From.Y+nv.Y*length, ), } } +// isPointOnLine tests if the Point is on the Line or Line segment. +func isPointOnLine(line Line, point Point, isSegment bool) bool { + lv := line.Vector() + pv := point.Sub(line.From) + + pcp := lv.CrossProduct(pv) + if pcp != 0 { + return false + } + + if isSegment { + dp := pv.DotProduct(lv) + return dp >= 0 && dp <= lv.SquareLength() + } + + return true +} + // IsPointOnLine tests if the Point is on the Line. -func (ln Line) IsPointOnLine(p Point) bool { - return isPointOnLine(ln, p, false) +func (ln Line) IsPointOnLine(t Point) bool { + return isPointOnLine(ln, t, false) } // IsPointOnSegment tests if the Point is on the Line segment. -func (ln Line) IsPointOnSegment(p Point) bool { - return isPointOnLine(ln, p, true) +func (ln Line) IsPointOnSegment(t Point) bool { + return isPointOnLine(ln, t, true) +} + +// closestPoint returns the closest Point on the Line or Line segment to the given Point. +func closestPoint(line Line, point Point, isSegment bool) Point { + nv := line.Vector().Normalize(line.Length()) + dp := point.Sub(line.From).DotProduct(nv) + + if isSegment { + if dp <= 0 { + return line.From + } + if dp >= line.Length() { + return line.To + } + } + + return NewPoint( + line.From.X+nv.X*dp, + line.From.Y+nv.Y*dp, + ) +} + +// ClosestPointToLine returns the closest Point on the Line. +func (ln Line) ClosestPointToLine(t Point) Point { + return closestPoint(ln, t, false) +} + +// ClosestPointToSegment returns the closest Point on the Line segment. +func (ln Line) ClosestPointToSegment(t Point) Point { + return closestPoint(ln, t, true) +} + +// linesIntersection returns the intersection point of two Lines or Line segments. +func linesIntersection(lineA, lineB Line, isSegmentA, isSegmentB bool) (Point, bool) { + av := lineA.Vector() + bv := lineB.Vector() + + vcp := av.CrossProduct(bv) + if vcp == 0 { + return Point{}, false + } + + sv := lineB.From.Sub(lineA.From) + acp := sv.CrossProduct(av) + bcp := sv.CrossProduct(bv) + t := bcp / vcp + u := acp / vcp + + if isSegmentA && (t < 0 || t > 1) { + return Point{}, false + } + if isSegmentB && (u < 0 || u > 1) { + return Point{}, false + } + + return Point{ + X: lineA.From.X + t*av.X, + Y: lineA.From.Y + t*av.Y, + }, true } // LinesIntersection returns the crossing Point of two Lines. -func (ln Line) LinesIntersection(tl Line) (Point, bool) { - return linesIntersection(ln, tl, false, false) +func (ln Line) LinesIntersection(t Line) (Point, bool) { + return linesIntersection(ln, t, false, false) } // SegmentsIntersection returns the crossing Point of two Line segments. -func (ln Line) SegmentsIntersection(tl Line) (Point, bool) { - return linesIntersection(ln, tl, true, true) +func (ln Line) SegmentsIntersection(t Line) (Point, bool) { + return linesIntersection(ln, t, true, true) } // LineSegmentIntersection returns the crossing Point of the Line and the Line segment. -func (ln Line) LineSegmentIntersection(tl Line) (Point, bool) { - return linesIntersection(ln, tl, false, true) +func (ln Line) LineSegmentIntersection(t Line) (Point, bool) { + return linesIntersection(ln, t, false, true) } // Rotate returns the Line rotated by the given angle. @@ -110,26 +183,24 @@ func (ln Line) Rotate(angle float64) Line { } // IsCollision tests whether a moving object collides with another moving object within a given radius. -func (ln Line) IsCollision(tl Line, radius float64) bool { - tv := tl.Vector() - lv := ln.Vector() - dx := tl.From.Sub(ln.From) - vx := tv.Sub(lv) +func (ln Line) IsCollision(t Line, radius float64) bool { + vx := t.Vector().Sub(ln.Vector()) + dx := t.From.Sub(ln.From) - a := vx.X*vx.X + vx.Y*vx.Y + a := vx.SquareLength() if a <= 0 { return false } - b := 2 * (dx.X*vx.X + dx.Y*vx.Y) - c := dx.X*dx.X + dx.Y*dx.Y - radius*radius + b := 2 * dx.DotProduct(vx) + c := dx.SquareLength() - radius*radius d := b*b - 4*a*c if d < 0 { return false } - t := (-b - math.Sqrt(d)) / (2 * a) - if t <= 0 || t > 1 { + tc := (-b - math.Sqrt(d)) / (2 * a) + if tc <= 0 || tc > 1 { return false } diff --git a/geometry_line_test.go b/geometry_line_test.go index bb177e9..d27a4d6 100644 --- a/geometry_line_test.go +++ b/geometry_line_test.go @@ -16,8 +16,11 @@ func TestLine_IsSame(t *testing.T) { } func TestLine_Length(t *testing.T) { - line := NewLine(Point{0, 0}, Point{300, 400}) - assert.EqualValues(t, 500, line.Length()) + var ln Line + ln = NewLine(Point{0, 0}, Point{300, 400}) + assert.EqualValues(t, 500, ln.Length()) + ln = NewLine(Point{300, 400}, Point{-300, -400}) + assert.EqualValues(t, 1000, ln.Length()) } func TestLine_Vector(t *testing.T) { @@ -171,98 +174,166 @@ func TestLine_Segment(t *testing.T) { func TestLine_IsPointOnLine(t *testing.T) { tests := []struct { - name string - line Line - p Point - want bool + name string + line Line + point Point + want bool }{ { - name: `horizontal`, - line: Line{Point{0, 0}, Point{300, 0}}, - p: Point{150, 0}, - want: true, + name: `horizontal`, + line: Line{Point{0, 0}, Point{300, 0}}, + point: Point{150, 0}, + want: true, }, { - name: `vertical`, - line: Line{Point{0, 0}, Point{0, 300}}, - p: Point{0, 150}, - want: true, + name: `vertical`, + line: Line{Point{0, 0}, Point{0, 300}}, + point: Point{0, 150}, + want: true, }, { - name: `diagonal ascending`, - line: Line{Point{0, 0}, Point{300, 300}}, - p: Point{150, 150}, - want: true, + name: `diagonal ascending`, + line: Line{Point{0, 0}, Point{300, 300}}, + point: Point{150, 150}, + want: true, }, { - name: `diagonal descending`, - line: Line{Point{0, 300}, Point{300, 0}}, - p: Point{150, 150}, - want: true, + name: `diagonal descending`, + line: Line{Point{0, 300}, Point{300, 0}}, + point: Point{150, 150}, + want: true, }, { - name: `diagonal reverse`, - line: Line{Point{300, 400}, Point{0, 0}}, - p: Point{600, 800}, - want: true, + name: `diagonal reverse`, + line: Line{Point{300, 400}, Point{0, 0}}, + point: Point{600, 800}, + want: true, }, { - name: `false`, - line: Line{Point{0, 0}, Point{300, 300}}, - p: Point{150, 100}, - want: false, + name: `false`, + line: Line{Point{0, 0}, Point{300, 300}}, + point: Point{150, 100}, + want: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.IsPointOnLine(tc.p)) + assert.Equal(t, tc.want, tc.line.IsPointOnLine(tc.point)) }) } } func TestLine_IsPointOnSegment(t *testing.T) { tests := []struct { - name string - line Line - p Point - want bool + name string + line Line + point Point + want bool }{ { - name: `horizontal out of bounds`, - line: Line{Point{0, 0}, Point{300, 0}}, - p: Point{450, 0}, - want: false, + name: `horizontal out of bounds`, + line: Line{Point{0, 0}, Point{300, 0}}, + point: Point{450, 0}, + want: false, }, { - name: `vertical out of bounds`, - line: Line{Point{0, 0}, Point{0, 300}}, - p: Point{0, 450}, - want: false, + name: `vertical out of bounds`, + line: Line{Point{0, 0}, Point{0, 300}}, + point: Point{0, 450}, + want: false, }, { - name: `diagonal ascending out of bounds`, - line: Line{Point{0, 0}, Point{300, 300}}, - p: Point{450, 450}, - want: false, + name: `diagonal ascending out of bounds`, + line: Line{Point{0, 0}, Point{300, 300}}, + point: Point{450, 450}, + want: false, }, { - name: `diagonal descending out of bounds`, - line: Line{Point{0, 300}, Point{300, 0}}, - p: Point{450, 450}, - want: false, + name: `diagonal descending out of bounds`, + line: Line{Point{0, 300}, Point{300, 0}}, + point: Point{450, 450}, + want: false, }, { - name: `false in bounds`, - line: Line{Point{0, 0}, Point{300, 300}}, - p: Point{150, 250}, - want: false, + name: `false in bounds`, + line: Line{Point{0, 0}, Point{300, 300}}, + point: Point{150, 250}, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.line.IsPointOnSegment(tc.point)) + }) + } +} + +func TestLine_ClosestPointToLine(t *testing.T) { + tests := []struct { + name string + line Line + point Point + want Point + }{ + { + name: `horizontal`, + line: Line{Point{0, 0}, Point{300, 0}}, + point: Point{400, 100}, + want: Point{400, 0}, + }, + { + name: `vertical`, + line: Line{Point{0, 0}, Point{0, 300}}, + point: Point{100, 400}, + want: Point{0, 400}, + }, + { + name: `diagonal`, + line: Line{Point{0, 0}, Point{100, 50}}, + point: Point{400, 0}, + want: Point{320, 160}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.line.IsPointOnSegment(tc.p)) + assert.Equal(t, tc.want, tc.line.ClosestPointToLine(tc.point)) + }) + } +} + +func TestLine_ClosestPointToSegment(t *testing.T) { + tests := []struct { + name string + line Line + point Point + want Point + }{ + { + name: `horizontal`, + line: Line{Point{0, 0}, Point{300, 0}}, + point: Point{400, 100}, + want: Point{300, 0}, + }, + { + name: `vertical`, + line: Line{Point{0, 0}, Point{0, 300}}, + point: Point{100, 400}, + want: Point{0, 300}, + }, + { + name: `diagonal`, + line: Line{Point{0, 0}, Point{100, 50}}, + point: Point{400, 0}, + want: Point{100, 50}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.line.ClosestPointToSegment(tc.point)) }) } } @@ -596,6 +667,42 @@ func TestNewLine(t *testing.T) { // Benchmarks +var ( + GlobalLine Line +) + +func BenchmarkIsPointOnLine(b *testing.B) { + r := false + line := Line{Point{0, 0}, Point{300, 300}} + point := Point{150, 150} + for i := 0; i < b.N; i++ { + r = isPointOnLine(line, point, true) + } + GlobalB = r +} + +func BenchmarkClosestPoint(b *testing.B) { + t := Point{100, 100} + p := Point{} + ln := Line{Point{0, 0}, Point{300, 400}} + for i := 0; i < b.N; i++ { + p = closestPoint(ln, t, true) + } + GlobalPoint = p +} + +func BenchmarkLinesIntersection(b *testing.B) { + p := Point{} + r := false + al := Line{Point{0, 0}, Point{300, 400}} + bl := Line{Point{0, 400}, Point{300, 0}} + for i := 0; i < b.N; i++ { + p, r = linesIntersection(al, bl, true, true) + } + GlobalPoint = p + GlobalB = r +} + func BenchmarkLine_IsCollision(b *testing.B) { r := false al := Line{Point{0, 0}, Point{600, 800}} diff --git a/geometry_movement.go b/geometry_movement.go new file mode 100644 index 0000000..d11a242 --- /dev/null +++ b/geometry_movement.go @@ -0,0 +1,30 @@ +package main + +import ( + "math" +) + +const ( + // angles + angleForward = 0 + angleLeft = 90 + angleRight = -90 + angleBack = 180 +) + +// MovingDistance calculates the distance traveled by an object. +// formula: s = ut + 1/2at^2 +func MovingDistance(speed, acceleration, time float64) float64 { + return (speed * time) + (0.5 * acceleration * time * time) +} + +// MovingVector calculates the vector of a moving object with static angle coordinate system. +// see https://www.codingame.com/training/easy/mars-lander-episode-1 +func MovingVector(angle, power float64) Point { + rad := angle * (math.Pi / 180) + + return Point{ + X: -power * math.Sin(rad), + Y: power * math.Cos(rad), + } +} diff --git a/geometry_movement_test.go b/geometry_movement_test.go new file mode 100644 index 0000000..d8d0ff3 --- /dev/null +++ b/geometry_movement_test.go @@ -0,0 +1,97 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMovingDistance(t *testing.T) { + tests := []struct { + name string + speed float64 + acc float64 + time float64 + want float64 + }{ + { + name: `speed`, + speed: 10, + time: 5, + want: 50, + }, + { + name: `acc`, + acc: 10, + time: 5, + want: 125, + }, + { + name: `speed and acc`, + speed: 10, + acc: 10, + time: 5, + want: 175, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, MovingDistance(tc.speed, tc.acc, tc.time)) + }) + } +} + +func TestMovingVector(t *testing.T) { + tests := []struct { + name string + angle float64 + power float64 + want Point + }{ + { + name: `top`, + angle: angleForward, + power: 4, + want: Point{0, 4}, + }, + { + name: `right`, + angle: angleRight, + power: 4, + want: Point{4, 0}, + }, + { + name: `left`, + angle: angleLeft, + power: 4, + want: Point{-4, 0}, + }, + { + name: `back`, + angle: angleBack, + power: 4, + want: Point{0, -4}, + }, + { + name: `top left`, + angle: 45, + power: 4, + want: Point{-2.82, 2.828}, + }, + { + name: `top right`, + angle: -45, + power: 4, + want: Point{2.82, 2.828}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mv := MovingVector(tc.angle, tc.power) + assert.InDelta(t, tc.want.X, mv.X, 0.1) + assert.InDelta(t, tc.want.Y, mv.Y, 0.1) + }) + } +} diff --git a/geometry_point.go b/geometry_point.go index 445a3d9..12435bd 100644 --- a/geometry_point.go +++ b/geometry_point.go @@ -36,26 +36,41 @@ func (p Point) IsSame(t Point) bool { } // Index returns the 0-index of the Point in a field of specified width. -func (p Point) Index(width int) int { - if p.X < 0 || p.X >= float64(width) || p.Y < 0 { - panic(fmt.Sprintf("point %s is out of bound %d", p, width)) +func (p Point) Index(width float64) int { + if p.X < 0 || p.X >= width || p.Y < 0 { + panic(fmt.Sprintf("point %s is out of bound %.f", p, width)) } - return int(p.Y)*width + int(p.X) + return int(p.Y*width + p.X) } -// IsInXBound tests if the Point is in the positive bound by X axis. +// IsInXRange tests if the Point is in the range by X axis. +func (p Point) IsInXRange(from, to float64) bool { + return p.X >= from && p.X <= to +} + +// IsInXBound tests if the Point is in the field defined by width. func (p Point) IsInXBound(width float64) bool { - return p.X >= 0 && p.X <= width + return p.X >= 0 && p.X < width +} + +// IsInYRange tests if the Point is in the range by Y axis. +func (p Point) IsInYRange(from, to float64) bool { + return p.Y >= from && p.Y <= to } -// IsInYBound tests if the Point is in the positive bound by Y axis. +// IsInYBound tests if the Point is in the field defined by height. func (p Point) IsInYBound(height float64) bool { - return p.Y >= 0 && p.Y <= height + return p.Y >= 0 && p.Y < height +} + +// IsInRange tests if the Point is in the range by X and Y axis. +func (p Point) IsInRange(from, to float64) bool { + return p.X >= from && p.X <= to && p.Y >= from && p.Y <= to } // IsInBound tests if the Point is in the positive bound by X and Y axis. func (p Point) IsInBound(width, height float64) bool { - return p.IsInXBound(width) && p.IsInYBound(height) + return p.X >= 0 && p.X < width && p.Y >= 0 && p.Y < height } // SymmetricX returns the symmetric Point by X axis. @@ -85,7 +100,9 @@ func (p Point) Sub(t Point) Point { // Distance returns the distance between two Points using the Pythagorean theorem. func (p Point) Distance(t Point) float64 { - return math.Sqrt(math.Pow(p.X-t.X, 2) + math.Pow(p.Y-t.Y, 2)) + x := p.X - t.X + y := p.Y - t.Y + return math.Sqrt(x*x + y*y) } // DistanceManhattan returns the Manhattan distance between two Points. @@ -100,6 +117,31 @@ func (p Point) DistanceChebyshev(t Point) float64 { return math.Max(math.Abs(p.X-t.X), math.Abs(p.Y-t.Y)) } +// SquareLength returns the square length of the Point vector. +func (p Point) SquareLength() float64 { + return p.X*p.X + p.Y*p.Y +} + +// DotProduct returns the dot product of two Points vectors. +func (p Point) DotProduct(t Point) float64 { + return p.X*t.X + p.Y*t.Y +} + +// CrossProduct returns the cross product of two Points vectors. +func (p Point) CrossProduct(t Point) float64 { + return p.X*t.Y - p.Y*t.X +} + +// Normalize returns the normalized Point vector of specified length. +func (p Point) Normalize(length float64) Point { + return Point{p.X / length, p.Y / length} +} + +// distanceToBound returns the distance between x and the expected bound. +func distanceToBound(x, bound float64) float64 { + return math.Min(x, bound-x) +} + // DistanceToXBound returns the distance between the Point and the field bound of specified width. func (p Point) DistanceToXBound(bound float64) float64 { return distanceToBound(p.X, bound) diff --git a/geometry_point_test.go b/geometry_point_test.go index 7dd4392..5cff746 100644 --- a/geometry_point_test.go +++ b/geometry_point_test.go @@ -17,44 +17,44 @@ func TestPoint_IsSame(t *testing.T) { func TestPoint_Index(t *testing.T) { tests := []struct { name string - p Point - width int + point Point + width float64 want int panic bool }{ { name: `first`, - p: Point{0, 0}, + point: Point{0, 0}, width: 10, want: 0, }, { name: `last in row`, - p: Point{9, 0}, + point: Point{9, 0}, width: 10, want: 9, }, { name: `next row`, - p: Point{0, 1}, + point: Point{0, 1}, width: 10, want: 10, }, { name: `next column`, - p: Point{1, 0}, + point: Point{1, 0}, width: 10, want: 1, }, { name: `last`, - p: Point{9, 9}, + point: Point{9, 9}, width: 10, want: 99, }, { name: `out of bound`, - p: Point{10, 0}, + point: Point{10, 0}, width: 10, panic: true, }, @@ -63,14 +63,22 @@ func TestPoint_Index(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.panic { - assert.Panics(t, func() { tc.p.Index(tc.width) }) + assert.Panics(t, func() { tc.point.Index(tc.width) }) return } - assert.Equal(t, tc.want, tc.p.Index(tc.width)) + assert.Equal(t, tc.want, tc.point.Index(tc.width)) }) } } +func TestPoint_IsInXRange(t *testing.T) { + var p Point + p = Point{200, 100} + assert.True(t, p.IsInXRange(100, 300)) + p = Point{200, 100} + assert.False(t, p.IsInXRange(300, 500)) +} + func TestPoint_IsInXBound(t *testing.T) { var p Point p = Point{200, 100} @@ -79,6 +87,14 @@ func TestPoint_IsInXBound(t *testing.T) { assert.False(t, p.IsInXBound(100)) } +func TestPoint_IsInYRange(t *testing.T) { + var p Point + p = Point{100, 200} + assert.True(t, p.IsInYRange(100, 300)) + p = Point{100, 200} + assert.False(t, p.IsInYRange(300, 500)) +} + func TestPoint_IsInYBound(t *testing.T) { var p Point p = Point{100, 200} @@ -87,6 +103,16 @@ func TestPoint_IsInYBound(t *testing.T) { assert.False(t, p.IsInYBound(100)) } +func TestPoint_IsInRange(t *testing.T) { + var p Point + p = Point{200, 100} + assert.True(t, p.IsInRange(100, 300)) + p = Point{200, 100} + assert.False(t, p.IsInRange(300, 500)) + p = Point{100, 200} + assert.False(t, p.IsInRange(300, 500)) +} + func TestPoint_IsInBound(t *testing.T) { var p Point p = Point{200, 100} @@ -289,3 +315,9 @@ func TestNewPoint(t *testing.T) { }) } } + +// Benchmarks + +var ( + GlobalPoint Point +) diff --git a/geometry_rect.go b/geometry_rect.go index f923c8e..922b060 100644 --- a/geometry_rect.go +++ b/geometry_rect.go @@ -38,6 +38,7 @@ func (r Rect) Center() Point { ) } +// Symmetric returns the symmetric Rect according to the given width and height. func (r Rect) Symmetric(width, height float64) Rect { return Rect{ Xf: width - r.Xf, @@ -48,8 +49,8 @@ func (r Rect) Symmetric(width, height float64) Rect { } // IsContainsPoint tests if the Rect contains the Point. -func (r Rect) IsContainsPoint(c Point) bool { - return c.X >= r.Xf && c.X <= r.Xt && c.Y >= r.Yf && c.Y <= r.Yt +func (r Rect) IsContainsPoint(t Point) bool { + return t.X >= r.Xf && t.X <= r.Xt && t.Y >= r.Yf && t.Y <= r.Yt } // IsContainsRectangle tests if the Rect contains the other Rect. @@ -81,6 +82,7 @@ func (r Rect) RectsIntersection(t Rect) (Rect, bool) { return ir, true } +// Vertices returns the Points vertices of the Rect. func (r Rect) Vertices() Points { return Points{ topLeft0: {r.Xf, r.Yt}, diff --git a/geometry_rect_test.go b/geometry_rect_test.go index 316891a..005db77 100644 --- a/geometry_rect_test.go +++ b/geometry_rect_test.go @@ -50,33 +50,32 @@ func TestRect_IsContainsPoint(t *testing.T) { func TestRect_IsContainsRectangle(t *testing.T) { tests := []struct { name string - r Rect - t Rect + a, b Rect want bool }{ { name: `true`, - r: Rect{100, 200, 300, 400}, - t: Rect{120, 180, 320, 380}, + a: Rect{100, 200, 300, 400}, + b: Rect{120, 180, 320, 380}, want: true, }, { name: `false`, - r: Rect{100, 200, 300, 400}, - t: Rect{150, 250, 350, 450}, + a: Rect{100, 200, 300, 400}, + b: Rect{150, 250, 350, 450}, want: false, }, { name: `same`, - r: Rect{100, 200, 300, 400}, - t: Rect{100, 200, 300, 400}, + a: Rect{100, 200, 300, 400}, + b: Rect{100, 200, 300, 400}, want: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.r.IsContainsRectangle(tc.t)) + assert.Equal(t, tc.want, tc.a.IsContainsRectangle(tc.b)) }) } } @@ -84,39 +83,38 @@ func TestRect_IsContainsRectangle(t *testing.T) { func TestRect_IsIntersectsRect(t *testing.T) { tests := []struct { name string - r Rect - t Rect + a, b Rect want bool }{ { name: `true`, - r: Rect{100, 200, 300, 400}, - t: Rect{150, 250, 350, 450}, + a: Rect{100, 200, 300, 400}, + b: Rect{150, 250, 350, 450}, want: true, }, { name: `false`, - r: Rect{100, 200, 300, 400}, - t: Rect{300, 400, 500, 600}, + a: Rect{100, 200, 300, 400}, + b: Rect{300, 400, 500, 600}, want: false, }, { name: `same`, - r: Rect{100, 200, 300, 400}, - t: Rect{100, 200, 300, 400}, + a: Rect{100, 200, 300, 400}, + b: Rect{100, 200, 300, 400}, want: true, }, { name: `inside`, - r: Rect{100, 200, 300, 400}, - t: Rect{120, 180, 320, 380}, + a: Rect{100, 200, 300, 400}, + b: Rect{120, 180, 320, 380}, want: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.r.IsIntersectsRect(tc.t)) + assert.Equal(t, tc.want, tc.a.IsIntersectsRect(tc.b)) }) } } @@ -124,62 +122,61 @@ func TestRect_IsIntersectsRect(t *testing.T) { func TestRect_RectsIntersection(t *testing.T) { tests := []struct { name string - r Rect - t Rect + a, b Rect want Rect ok bool }{ { name: `true`, - r: Rect{100, 200, 300, 400}, - t: Rect{150, 250, 350, 450}, + a: Rect{100, 200, 300, 400}, + b: Rect{150, 250, 350, 450}, want: Rect{150, 200, 350, 400}, ok: true, }, { name: `false`, - r: Rect{100, 200, 300, 400}, - t: Rect{300, 400, 500, 600}, + a: Rect{100, 200, 300, 400}, + b: Rect{300, 400, 500, 600}, ok: false, }, { name: `same`, - r: Rect{100, 200, 300, 400}, - t: Rect{100, 200, 300, 400}, + a: Rect{100, 200, 300, 400}, + b: Rect{100, 200, 300, 400}, want: Rect{100, 200, 300, 400}, ok: true, }, { name: `second>first`, - r: Rect{100, 200, 300, 400}, - t: Rect{120, 180, 320, 380}, + a: Rect{100, 200, 300, 400}, + b: Rect{120, 180, 320, 380}, want: Rect{120, 180, 320, 380}, ok: true, }, { name: `first>second`, - r: Rect{100, 200, 300, 400}, - t: Rect{80, 220, 280, 420}, + a: Rect{100, 200, 300, 400}, + b: Rect{80, 220, 280, 420}, want: Rect{100, 200, 300, 400}, ok: true, }, { name: `line`, - r: Rect{100, 200, 300, 400}, - t: Rect{200, 300, 300, 400}, + a: Rect{100, 200, 300, 400}, + b: Rect{200, 300, 300, 400}, ok: false, }, { name: `point`, - r: Rect{100, 200, 300, 400}, - t: Rect{200, 300, 200, 300}, + a: Rect{100, 200, 300, 400}, + b: Rect{200, 300, 200, 300}, ok: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got, ok := tc.r.RectsIntersection(tc.t) + got, ok := tc.a.RectsIntersection(tc.b) assert.Equal(t, tc.ok, ok) assert.Equal(t, tc.want, got) }) diff --git a/geometry_utils.go b/geometry_utils.go deleted file mode 100644 index cdaf80d..0000000 --- a/geometry_utils.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "math" -) - -// distanceToBound returns the distance between x and the expected bound. -func distanceToBound(x, bound float64) float64 { - return math.Min(x, bound-x) -} - -// linesIntersection returns the intersection point of two Lines. -// Arguments s1 and s2 are true if the Line threaten as segment. -// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection -func linesIntersection(a, b Line, s1, s2 bool) (Point, bool) { - av := a.Vector() - bv := b.Vector() - - vcp := av.X*bv.Y - av.Y*bv.X - if vcp == 0 { - return Point{}, false - } - - sv := b.From.Sub(a.From) - cpa := sv.X*av.Y - sv.Y*av.X - cpb := sv.X*bv.Y - sv.Y*bv.X - t := cpb / vcp - u := cpa / vcp - - if s1 && (t < 0 || t > 1) { - return Point{}, false - } - if s2 && (u < 0 || u > 1) { - return Point{}, false - } - - return Point{ - X: a.From.X + t*av.X, - Y: a.From.Y + t*av.Y, - }, true -} - -func isPointOnLine(ln Line, point Point, s bool) bool { - lv := ln.Vector() - pv := point.Sub(ln.From) - - pcp := lv.X*pv.Y - lv.Y*pv.X - if pcp != 0 { - return false - } - - if s { - dp := pv.X*lv.X + pv.Y*lv.Y - l := lv.X*lv.X + lv.Y*lv.Y - - return dp >= 0 && dp <= l - } - - return true -} diff --git a/geometry_utils_test.go b/geometry_utils_test.go deleted file mode 100644 index e055480..0000000 --- a/geometry_utils_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "testing" -) - -// Benchmarks - -func BenchmarkLine_LinesIntersection(b *testing.B) { - p := Point{} - r := false - al := Line{Point{0, 0}, Point{300, 400}} - bl := Line{Point{0, 400}, Point{300, 0}} - for i := 0; i < b.N; i++ { - p, r = linesIntersection(al, bl, true, true) - } - GlobalPoint = p - GlobalB = r -} - -func BenchmarkLine_IsPointOnLine(b *testing.B) { - r := false - line := Line{Point{0, 0}, Point{300, 300}} - point := Point{150, 150} - for i := 0; i < b.N; i++ { - r = isPointOnLine(line, point, true) - } - GlobalB = r -} diff --git a/input.go b/input.go new file mode 100644 index 0000000..d578bee --- /dev/null +++ b/input.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" +) + +type Unit struct { + x, y, z float64 +} + +type Turn struct { + Power float64 + L, R string +} + +type Game struct { + Units []Unit +} + +func InputGame(data []string) Game { + var err error + var game Game + + var size int + size = StrToInt(data[0]) + data = data[1:] + var unit Unit + units := make([]Unit, 0, size) + for i := 0; i < size; i++ { + _, err = fmt.Sscan(data[i], &unit.x, &unit.y, &unit.z) + if err != nil { + panic(err) + } + units = append(units, unit) + } + + // some additional logic + game.Units = units + + return game +} + +func InputStep(data []string) Turn { + var err error + + var turn Turn + _, err = fmt.Sscan( + data[0], + &turn.Power, + &turn.L, + &turn.R, + ) + if err != nil { + panic(err) + } + + // some additional logic + + return turn +} diff --git a/input_test.go b/input_test.go new file mode 100644 index 0000000..3ac65df --- /dev/null +++ b/input_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInputGame(t *testing.T) { + game := InputGame(readGameTests) + want := Game{ + Units: []Unit{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9}, + }, + } + assert.Equal(t, want, game) +} + +func TestInputStep(t *testing.T) { + turn := InputStep(readStepTests) + want := Turn{ + Power: 1, + L: "R", + R: "L", + } + assert.Equal(t, want, turn) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b4c1204 --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "bufio" + "math/rand" + "os" + "runtime" + "time" +) + +var rnd *rand.Rand + +func init() { + runtime.GOMAXPROCS(1) + rnd = rand.New(rand.NewSource(time.Now().UnixNano())) + debug = true +} + +func main() { + // example + scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 1000000), 1000000) + + dataGame := ReadGame(scanner) + asText(DataExport(dataGame)) + game := InputGame(dataGame) + + dataStep := ReadStep(scanner) + asText(DataExport(dataStep)) + step := InputStep(dataStep) + + // some game logic for the first step + u(game, step) + + for { + dataStep = ReadStep(scanner) + asText(DataExport(dataStep)) + step = InputStep(dataStep) + + // some game logic for the next step + u(game, step) + } +} diff --git a/math.go b/math.go new file mode 100644 index 0000000..6e6c255 --- /dev/null +++ b/math.go @@ -0,0 +1,36 @@ +package main + +import ( + "math" + "sort" +) + +// Median returns the median value of the given slice of float64. +func Median(values []float64) float64 { + sort.Float64s(values) + + middle := len(values) / 2 + if len(values)%2 == 0 { + return (values[middle-1] + values[middle]) / 2 + } + + return values[middle] +} + +// ExpectedExpDiff returns the expected exponential difference of the given values. +func ExpectedExpDiff(expected, current, decay float64) float64 { + diff := math.Abs(expected - current) + if diff == 0 { + return 1.0 + } + if diff > math.Abs(expected) { + return 0.0 + } + + return math.Exp(-diff / (expected / decay)) +} + +// IsFloatsEqual returns true if the given floats are equal within the given delta. +func IsFloatsEqual(a, b, delta float64) bool { + return math.Abs(a-b) <= delta +} diff --git a/math_test.go b/math_test.go new file mode 100644 index 0000000..538700e --- /dev/null +++ b/math_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMedian(t *testing.T) { + values := []float64{ + 0, 2, 4, 6, 8, 10, 1000, + } + + assert.EqualValues(t, 6, Median(values)) +} + +func TestExpectedExpDiff(t *testing.T) { + tests := []struct { + name string + current float64 + exp float64 + decay float64 + want float64 + }{ + { + name: "same", + current: 10, + exp: 10, + want: 1, + }, + { + name: "very big diff", + current: 21, + exp: 10, + want: 0, + }, + { + name: "middle", + current: 15, + exp: 10, + decay: 2, + want: 0.36, + }, + { + name: "big diff", + current: 18, + exp: 10, + decay: 2, + want: 0.2, + }, + { + name: "zero ok", + current: 0, + exp: 0, + want: 1, + }, + { + name: "big number", + current: 1500, + exp: 1000, + decay: 2, + want: 0.36, + }, + { + name: "big far", + current: 2100, + exp: 1000, + want: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.InDelta(t, tc.want, ExpectedExpDiff(tc.exp, tc.current, tc.decay), 0.1) + }) + } +} + +func TestIsFloatsEqual(t *testing.T) { + f := 0.1 + assert.True(t, IsFloatsEqual(f, 0.11, 0.1)) + assert.False(t, IsFloatsEqual(f, 0.21, 0.1)) +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..91cb89a --- /dev/null +++ b/reader.go @@ -0,0 +1,32 @@ +package main + +import ( + "bufio" +) + +// Reading the game state from the standard input stream. + +// ReadGame reads the game state from the standard input stream. +func ReadGame(s *bufio.Scanner) []string { + data := make([]string, 0, 32) + + s.Scan() + size := s.Text() + data = append(data, size) + for i := 0; i < StrToInt(size); i++ { + s.Scan() + data = append(data, s.Text()) + } + + return data +} + +// ReadStep reads the game turn state from the standard input stream. +func ReadStep(s *bufio.Scanner) []string { + data := make([]string, 0, 1) + + s.Scan() + data = append(data, s.Text()) + + return data +} diff --git a/reader_test.go b/reader_test.go new file mode 100644 index 0000000..b982254 --- /dev/null +++ b/reader_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var readGameTests = []string{ + "3", + "1 2 3", + "4 5 6", + "7 8 9", +} + +func TestReadGame(t *testing.T) { + s := strings.Join(readGameTests, "\n") + r := strings.NewReader(s) + b := bufio.NewScanner(r) + + data := ReadGame(b) + assert.Equal(t, readGameTests, data) +} + +var readStepTests = []string{ + "1 R L", +} + +func TestReadStep(t *testing.T) { + s := strings.Join(readStepTests, "\n") + r := strings.NewReader(s) + b := bufio.NewScanner(r) + + data := ReadStep(b) + assert.Equal(t, readStepTests, data) +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..2d9a050 --- /dev/null +++ b/types.go @@ -0,0 +1,37 @@ +package main + +// A collection of helper functions for type conversions. + +import ( + "strconv" +) + +// IntToStr converts an integer to a string. +func IntToStr(x int) string { + return strconv.Itoa(x) +} + +// StrToInt converts a string to an integer. +func StrToInt(s string) int { + result, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return result +} + +// BoolToInt converts a boolean value to an integer. +func BoolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +// IntToBool converts an integer to a boolean value. +func IntToBool(x int) bool { + if x != 0 { + return true + } + return false +} diff --git a/types_test.go b/types_test.go new file mode 100644 index 0000000..6bef6bc --- /dev/null +++ b/types_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIntToStr(t *testing.T) { + var x int + x = 1 + assert.Equal(t, "1", IntToStr(x)) + x = -1 + assert.Equal(t, "-1", IntToStr(x)) + x = 0 + assert.Equal(t, "0", IntToStr(x)) +} + +func TestStrToInt(t *testing.T) { + var s string + s = "1" + assert.Equal(t, 1, StrToInt(s)) + s = "-1" + assert.Equal(t, -1, StrToInt(s)) + s = "0" + assert.Equal(t, 0, StrToInt(s)) +} + +func TestBoolToInt(t *testing.T) { + var b bool + b = true + assert.Equal(t, 1, BoolToInt(b)) + b = false + assert.Equal(t, 0, BoolToInt(b)) +} + +func TestIntToBool(t *testing.T) { + var x int + x = 1 + assert.Equal(t, true, IntToBool(x)) + x = 0 + assert.Equal(t, false, IntToBool(x)) +}