diff --git a/config.json b/config.json index 607f8df..30db813 100644 --- a/config.json +++ b/config.json @@ -371,6 +371,15 @@ "math" ] }, + { + "slug": "darts", + "name": "Darts", + "uuid": "90237a3f-595a-4238-bcef-46d6f84b0945", + "practices": [], + "prerequisites": [], + "difficulty": 2, + "topics": [] + }, { "slug": "gigasecond", "name": "Gigasecond", diff --git a/exercises/practice/darts/.docs/instructions.md b/exercises/practice/darts/.docs/instructions.md new file mode 100644 index 0000000..5e57a86 --- /dev/null +++ b/exercises/practice/darts/.docs/instructions.md @@ -0,0 +1,31 @@ +# Instructions + +Write a function that returns the earned points in a single toss of a Darts game. + +[Darts][darts] is a game where players throw darts at a [target][darts-target]. + +In our particular instance of the game, the target rewards 4 different amounts of points, depending on where the dart lands: + +![Our dart scoreboard with values from a complete miss to a bullseye](https://assets.exercism.org/images/exercises/darts/darts-scoreboard.svg) + +- If the dart lands outside the target, player earns no points (0 points). +- If the dart lands in the outer circle of the target, player earns 1 point. +- If the dart lands in the middle circle of the target, player earns 5 points. +- If the dart lands in the inner circle of the target, player earns 10 points. + +The outer circle has a radius of 10 units (this is equivalent to the total radius for the entire target), the middle circle a radius of 5 units, and the inner circle a radius of 1. +Of course, they are all centered at the same point — that is, the circles are [concentric][] defined by the coordinates (0, 0). + +Write a function that given a point in the target (defined by its [Cartesian coordinates][cartesian-coordinates] `x` and `y`, where `x` and `y` are [real][real-numbers]), returns the correct amount earned by a dart landing at that point. + +## Credit + +The scoreboard image was created by [habere-et-dispertire][habere-et-dispertire] using [Inkscape][inkscape]. + +[darts]: https://en.wikipedia.org/wiki/Darts +[darts-target]: https://en.wikipedia.org/wiki/Darts#/media/File:Darts_in_a_dartboard.jpg +[concentric]: https://mathworld.wolfram.com/ConcentricCircles.html +[cartesian-coordinates]: https://www.mathsisfun.com/data/cartesian-coordinates.html +[real-numbers]: https://www.mathsisfun.com/numbers/real-numbers.html +[habere-et-dispertire]: https://exercism.org/profiles/habere-et-dispertire +[inkscape]: https://en.wikipedia.org/wiki/Inkscape diff --git a/exercises/practice/darts/.meta/config.json b/exercises/practice/darts/.meta/config.json new file mode 100644 index 0000000..2bc8b5c --- /dev/null +++ b/exercises/practice/darts/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "darts.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Write a function that returns the earned points in a single toss of a Darts game.", + "source": "Inspired by an exercise created by a professor Della Paolera in Argentina" +} diff --git a/exercises/practice/darts/.meta/example.sml b/exercises/practice/darts/.meta/example.sml new file mode 100644 index 0000000..ae30ba6 --- /dev/null +++ b/exercises/practice/darts/.meta/example.sml @@ -0,0 +1,9 @@ +fun score (x: real, y: real): int = + let + val r = Math.sqrt(x * x + y * y) + in + if r <= 1.0 then 10 + else if r <= 5.0 then 5 + else if r <= 10.0 then 1 + else 0 + end diff --git a/exercises/practice/darts/.meta/tests.toml b/exercises/practice/darts/.meta/tests.toml new file mode 100644 index 0000000..fbe2976 --- /dev/null +++ b/exercises/practice/darts/.meta/tests.toml @@ -0,0 +1,49 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[9033f731-0a3a-4d9c-b1c0-34a1c8362afb] +description = "Missed target" + +[4c9f6ff4-c489-45fd-be8a-1fcb08b4d0ba] +description = "On the outer circle" + +[14378687-ee58-4c9b-a323-b089d5274be8] +description = "On the middle circle" + +[849e2e63-85bd-4fed-bc3b-781ae962e2c9] +description = "On the inner circle" + +[1c5ffd9f-ea66-462f-9f06-a1303de5a226] +description = "Exactly on center" + +[b65abce3-a679-4550-8115-4b74bda06088] +description = "Near the center" + +[66c29c1d-44f5-40cf-9927-e09a1305b399] +description = "Just within the inner circle" + +[d1012f63-c97c-4394-b944-7beb3d0b141a] +description = "Just outside the inner circle" + +[ab2b5666-b0b4-49c3-9b27-205e790ed945] +description = "Just within the middle circle" + +[70f1424e-d690-4860-8caf-9740a52c0161] +description = "Just outside the middle circle" + +[a7dbf8db-419c-4712-8a7f-67602b69b293] +description = "Just within the outer circle" + +[e0f39315-9f9a-4546-96e4-a9475b885aa7] +description = "Just outside the outer circle" + +[045d7d18-d863-4229-818e-b50828c75d19] +description = "Asymmetric position between the inner and middle circles" diff --git a/exercises/practice/darts/darts.sml b/exercises/practice/darts/darts.sml new file mode 100644 index 0000000..f50ca8b --- /dev/null +++ b/exercises/practice/darts/darts.sml @@ -0,0 +1,2 @@ +fun score (x: int, y: int): int = + raise Fail "'score' is not implemented" diff --git a/exercises/practice/darts/test.sml b/exercises/practice/darts/test.sml new file mode 100644 index 0000000..faa413f --- /dev/null +++ b/exercises/practice/darts/test.sml @@ -0,0 +1,51 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "darts.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "darts" [ + test "Missed target" + (fn _ => score (~9.0, 9.0) |> Expect.equalTo 0), + + test "On the outer circle" + (fn _ => score (0.0, 10.0) |> Expect.equalTo 1), + + test "On the middle circle" + (fn _ => score (~5.0, 0.0) |> Expect.equalTo 5), + + test "On the inner circle" + (fn _ => score (0.0, ~1.0) |> Expect.equalTo 10), + + test "Exactly on center" + (fn _ => score (0.0, 0.0) |> Expect.equalTo 10), + + test "Near the center" + (fn _ => score (~0.1, ~0.1) |> Expect.equalTo 10), + + test "Just within the inner circle" + (fn _ => score (0.7, 0.7) |> Expect.equalTo 10), + + test "Just outside the inner circle" + (fn _ => score (0.8, ~0.8) |> Expect.equalTo 5), + + test "Just within the middle circle" + (fn _ => score (~3.5, 3.5) |> Expect.equalTo 5), + + test "Just outside the middle circle" + (fn _ => score (~3.6, ~3.6) |> Expect.equalTo 1), + + test "Just within the outer circle" + (fn _ => score (~7.0, 7.0) |> Expect.equalTo 1), + + test "Just outside the outer circle" + (fn _ => score (7.1, ~7.1) |> Expect.equalTo 0), + + test "Asymmetric position between the inner and middle circles" + (fn _ => score (0.5, ~4.0) |> Expect.equalTo 5) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/darts/testlib.sml b/exercises/practice/darts/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/darts/testlib.sml @@ -0,0 +1,160 @@ +structure Expect = +struct + datatype expectation = Pass | Fail of string * string + + local + fun failEq b a = + Fail ("Expected: " ^ b, "Got: " ^ a) + + fun failExn b a = + Fail ("Expected: " ^ b, "Raised: " ^ a) + + fun exnName (e: exn): string = General.exnName e + in + fun truthy a = + if a + then Pass + else failEq "true" "false" + + fun falsy a = + if a + then failEq "false" "true" + else Pass + + fun equalTo b a = + if a = b + then Pass + else failEq (PolyML.makestring b) (PolyML.makestring a) + + fun nearTo delta b a = + if Real.abs (a - b) <= delta * Real.abs a orelse + Real.abs (a - b) <= delta * Real.abs b + then Pass + else failEq (Real.toString b ^ " +/- " ^ Real.toString delta) (Real.toString a) + + fun anyError f = + ( + f (); + failExn "an exception" "Nothing" + ) handle _ => Pass + + fun error e f = + ( + f (); + failExn (exnName e) "Nothing" + ) handle e' => if exnMessage e' = exnMessage e + then Pass + else failExn (exnMessage e) (exnMessage e') + end +end + +structure TermColor = +struct + datatype color = Red | Green | Yellow | Normal + + fun f Red = "\027[31m" + | f Green = "\027[32m" + | f Yellow = "\027[33m" + | f Normal = "\027[0m" + + fun colorize color s = (f color) ^ s ^ (f Normal) + + val redit = colorize Red + + val greenit = colorize Green + + val yellowit = colorize Yellow +end + +structure Test = +struct + datatype testnode = TestGroup of string * testnode list + | Test of string * (unit -> Expect.expectation) + + local + datatype evaluation = Success of string + | Failure of string * string * string + | Error of string * string + + fun indent n s = (implode (List.tabulate (n, fn _ => #" "))) ^ s + + fun fmt indentlvl ev = + let + val check = TermColor.greenit "\226\156\148 " (* ✔ *) + val cross = TermColor.redit "\226\156\150 " (* ✖ *) + val indentlvl = indentlvl * 2 + in + case ev of + Success descr => indent indentlvl (check ^ descr) + | Failure (descr, exp, got) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) exp, + indent (indentlvl + 2) got] + | Error (descr, reason) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) (TermColor.redit reason)] + end + + fun eval (TestGroup _) = raise Fail "Only a 'Test' can be evaluated" + | eval (Test (descr, thunk)) = + ( + case thunk () of + Expect.Pass => ((1, 0, 0), Success descr) + | Expect.Fail (s, s') => ((0, 1, 0), Failure (descr, s, s')) + ) + handle e => ((0, 0, 1), Error (descr, "Unexpected error: " ^ exnMessage e)) + + fun flatten depth testnode = + let + fun sum (x, y, z) (a, b, c) = (x + a, y + b, z + c) + + fun aux (t, (counter, acc)) = + let + val (counter', texts) = flatten (depth + 1) t + in + (sum counter' counter, texts :: acc) + end + in + case testnode of + TestGroup (descr, ts) => + let + val (counter, texts) = foldr aux ((0, 0, 0), []) ts + in + (counter, (indent (depth * 2) descr) :: List.concat texts) + end + | Test _ => + let + val (counter, evaluation) = eval testnode + in + (counter, [fmt depth evaluation]) + end + end + + fun println s = print (s ^ "\n") + in + fun run suite = + let + val ((succeeded, failed, errored), texts) = flatten 0 suite + + val summary = String.concatWith ", " [ + TermColor.greenit ((Int.toString succeeded) ^ " passed"), + TermColor.redit ((Int.toString failed) ^ " failed"), + TermColor.redit ((Int.toString errored) ^ " errored"), + (Int.toString (succeeded + failed + errored)) ^ " total" + ] + + val status = if failed = 0 andalso errored = 0 + then OS.Process.success + else OS.Process.failure + + in + List.app println texts; + println ""; + println ("Tests: " ^ summary); + OS.Process.exit status + end + end +end + +fun describe description tests = Test.TestGroup (description, tests) +fun test description thunk = Test.Test (description, thunk)