{"id":"asteroids-keys","slug":"keys-and-loop","title":"Steering with the keys","text":"A game reacts to the keyboard, and it keeps moving while nobody touches it. This step adds both: key events, and a **game loop** that runs many times a second.\n\n### Key events\n\nIn part 13 a button had `setOnAction`. For the keyboard, the **Scene** has `setOnKeyPressed` and `setOnKeyReleased`, each taking a handler, usually a lambda. The handler gets a **KeyEvent** (from `javafx.scene.input`), and `event.getCode()` says which key it was, as a **KeyCode**: an enum with a constant for every key, such as `KeyCode.LEFT`, `KeyCode.UP`, `KeyCode.SPACE` and `KeyCode.A`.\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> System.out.println(\"pressed \" + event.getCode()));\n        scene.setOnKeyReleased(event -> System.out.println(\"released \" + event.getCode()));\n\n        stage.setScene(scene);\n        stage.setTitle(\"Keys\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[\"key SPACE\",\"press key LEFT\",\"release key LEFT\",\"key A\"],\"window\":{\"windows\":[{\"title\":\"Keys\",\"scene\":{\"width\":360,\"height\":240,\"onKeyPressed\":true,\"onKeyReleased\":true,\"root\":{\"n\":1,\"type\":\"Pane\",\"prefWidth\":360,\"prefHeight\":240,\"children\":[{\"n\":2,\"type\":\"Rectangle\",\"x\":0,\"y\":0,\"width\":360,\"height\":240,\"fill\":\"#191970\"},{\"n\":3,\"type\":\"Polygon\",\"translateX\":180,\"translateY\":120,\"points\":[-15,-6,9,-6,15,0,9,6,-15,6],\"fill\":\"#ffd700\"}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Keys\\\"\\n  Pane 360 x 240\\n    Rectangle 360 x 240 at (0, 0), fill #191970\\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), fill #ffd700\"}\n```\n\n```output\npressed SPACE\nreleased SPACE\npressed LEFT\nreleased LEFT\npressed A\nreleased A\n```\n\nThe key handlers go on the **scene**, not on a node: the scene gets the keys of its whole window. The event lines are the page's way of pressing keys: `press key LEFT` presses the left arrow key and holds it, `release key LEFT` lets go of it, and `key SPACE` presses and releases Space at once. The keys are named as KeyCode names them (`LEFT`, `RIGHT`, `UP`, `DOWN`, `SPACE`, `A`), and the task cards say them in words: \"Hold Left\", \"let go of Left\", \"press and release Space\".\n\n### Holding a key\n\nA game needs to know which keys are **held right now**, not only when one goes down. So it keeps them in a map, `Map<KeyCode, Boolean>`: the pressed handler puts `true` for its key, the released handler `false`, and the rest of the game asks the map with `held.getOrDefault(KeyCode.LEFT, false)`, which is `false` for a key that was never pressed.\n\nThis matters for one more reason. On a computer, holding a key down makes the system repeat its press, about 30 times a second after a short pause, so a handler that turned the submarine on each press would turn it in jerks. Here a held key isn't repeated at all: it gets one press. A game that keeps the held keys in a map and does its work in the game loop behaves the same either way, which is why the MOOC's game does it too.\n\n### The game loop: AnimationTimer\n\nA game redraws its world many times a second. Each of those pictures is a **frame**, and JavaFX shows about 60 frames a second. An **AnimationTimer** (from `javafx.animation`) runs code once for every frame: its method `handle` is called once a frame, for as long as the timer runs. `AnimationTimer` is an abstract class, so a game makes a class of its own that extends it, usually right where it's needed:\n\n```java\nnew AnimationTimer() {\n    @Override\n    public void handle(long now) {\n        // runs once every frame\n    }\n}.start();\n```\n\n`new AnimationTimer() { ... }` is an **anonymous class**: a class without a name that extends AnimationTimer, written and used in one place. Its body overrides `handle`, and `.start()` starts the new timer at once. Like a lambda (module 49), its code can use the local variables of `start` that never change, such as `sub` below, and the fields of `Main`.\n\n`handle` gets a number, `now`: the time of the frame in nanoseconds. On a computer it comes from the computer's clock; here it's a made-up clock, so a game in these lessons never uses it. To know how many frames have passed, count them in a field.\n\nOn this page, frames run only when you ask for them: the event line `frames 20` runs 20 frames, calling `handle` 20 times, and the page records each one. The submarine turns 4 degrees each frame while Left or Right is held:\n\n```java\nimport java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.setOnKeyReleased(event -> held.put(event.getCode(), false));\n\n        new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (held.getOrDefault(KeyCode.LEFT, false)) {\n                    sub.setRotate(sub.getRotate() - 4);\n                }\n                if (held.getOrDefault(KeyCode.RIGHT, false)) {\n                    sub.setRotate(sub.getRotate() + 4);\n                }\n            }\n        }.start();\n\n        stage.setScene(scene);\n        stage.setTitle(\"Steering\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[\"press key RIGHT\",\"frames 20\",\"release key RIGHT\",\"press key LEFT\",\"frames 5\",\"release key LEFT\",\"frames 10\"],\"window\":{\"windows\":[{\"title\":\"Steering\",\"scene\":{\"width\":360,\"height\":240,\"onKeyPressed\":true,\"onKeyReleased\":true,\"root\":{\"n\":1,\"type\":\"Pane\",\"f\":1,\"prefWidth\":360,\"prefHeight\":240,\"children\":[{\"n\":2,\"type\":\"Rectangle\",\"f\":2,\"x\":0,\"y\":0,\"width\":360,\"height\":240,\"fill\":\"#191970\"},{\"n\":3,\"type\":\"Polygon\",\"f\":3,\"translateX\":180,\"translateY\":120,\"rotate\":60,\"points\":[-15,-6,9,-6,15,0,9,6,-15,6],\"fill\":\"#ffd700\"}]}}}],\"problems\":[],\"frames\":{\"count\":35,\"list\":[[{\"roots\":[1]},{\"k\":1,\"type\":\"Pane\",\"children\":[2,3]},{\"k\":2,\"type\":\"Rectangle\",\"width\":360,\"height\":240,\"fill\":\"#191970\"},{\"k\":3,\"type\":\"Polygon\",\"tx\":180,\"ty\":120,\"r\":4,\"points\":[-15,-6,9,-6,15,0,9,6,-15,6],\"fill\":\"#ffd700\"}],[{\"k\":3,\"r\":8}],[{\"k\":3,\"r\":12}],[{\"k\":3,\"r\":16}],[{\"k\":3,\"r\":20}],[{\"k\":3,\"r\":24}],[{\"k\":3,\"r\":28}],[{\"k\":3,\"r\":32}],[{\"k\":3,\"r\":36}],[{\"k\":3,\"r\":40}],[{\"k\":3,\"r\":44}],[{\"k\":3,\"r\":48}],[{\"k\":3,\"r\":52}],[{\"k\":3,\"r\":56}],[{\"k\":3,\"r\":60}],[{\"k\":3,\"r\":64}],[{\"k\":3,\"r\":68}],[{\"k\":3,\"r\":72}],[{\"k\":3,\"r\":76}],[{\"k\":3,\"r\":80}],[{\"k\":3,\"r\":76}],[{\"k\":3,\"r\":72}],[{\"k\":3,\"r\":68}],[{\"k\":3,\"r\":64}],[{\"k\":3,\"r\":60}],[],[],[],[],[],[],[],[],[],[]]}},\"outline\":\"Window \\\"Steering\\\"\\n  Pane 360 x 240\\n    Rectangle 360 x 240 at (0, 0), fill #191970\\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), rotate 60, fill #ffd700\\nframes run: 35\"}\n```\n\nRight is held for 20 frames (20 times 4 degrees, 80 clockwise), then Left for 5 (20 back), and then 10 frames run with no key held, which change nothing: the submarine ends at 60 degrees. The page shows those 35 frames as an animation. In the window as text, the last line counts them, `frames run: 35`; the challenges compare the window after the last event and that count, not the frames on the way.\n\nBetween the `frames` lines, nothing moves: a key line only runs the key handlers. On a computer with the real JavaFX, the frames run by themselves and you press the keys while they do.\n","task":"Fill in the blanks so that releasing a key puts `false` in the map, `handle` asks the map whether Left is held, and the timer starts. The test holds Left for 15 frames, so the submarine ends turned -60 degrees.\n","kind":"fill","seed":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.[[setOnKeyReleased]](event -> held.put(event.getCode(), false));\n\n        new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (held.[[getOrDefault]](KeyCode.LEFT, false)) {\n                    sub.setRotate(sub.getRotate() - 4);\n                }\n                if (held.getOrDefault(KeyCode.RIGHT, false)) {\n                    sub.setRotate(sub.getRotate() + 4);\n                }\n            }\n        }.[[start]]();\n\n        stage.setScene(scene);\n        stage.setTitle(\"Steering\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.setOnKeyReleased(event -> held.put(event.getCode(), false));\n\n        new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (held.getOrDefault(KeyCode.LEFT, false)) {\n                    sub.setRotate(sub.getRotate() - 4);\n                }\n                if (held.getOrDefault(KeyCode.RIGHT, false)) {\n                    sub.setRotate(sub.getRotate() + 4);\n                }\n            }\n        }.start();\n\n        stage.setScene(scene);\n        stage.setTitle(\"Steering\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["The scene's handler for a key that comes up is `setOnKeyReleased`. The map's method that gives a value for a missing key is `getOrDefault`.","A timer does nothing until it's started: `new AnimationTimer() { ... }.start();`."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["press key LEFT","frames 15","release key LEFT"],"window":{"windows":[{"title":"Steering","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":120,"rotate":-60,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":15,"list":[]}},"outline":"Window \"Steering\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), rotate -60, fill #ffd700\nframes run: 15"},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":["press key RIGHT","frames 7","release key RIGHT","press key LEFT","frames 3","release key LEFT","frames 4"],"window":{"windows":[{"title":"Steering","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":120,"rotate":16,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":14,"list":[]}},"outline":"Window \"Steering\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), rotate 16, fill #ffd700\nframes run: 14"}],"require":[],"forbid":[],"more":[{"task":"The submarine dives and climbs. In the starter code, the submarine is at (180, 120). Keep the held keys in a `Map<KeyCode, Boolean>` and add a game loop: in every frame, while Up is held the submarine moves up 2 pixels (its translate y goes down by 2), but only if its y is more than 20; while Down is held it moves down 2 pixels, but only if its y is less than 220. Check Up first, then Down. It doesn't turn, and nothing is printed.\n\nThe test holds Up for 30 frames, so the submarine ends at (180, 60).\n","kind":"code","seed":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n\n        stage.setScene(scene);\n        stage.setTitle(\"Depth\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.setOnKeyReleased(event -> held.put(event.getCode(), false));\n\n        new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (held.getOrDefault(KeyCode.UP, false) && sub.getTranslateY() > 20) {\n                    sub.setTranslateY(sub.getTranslateY() - 2);\n                }\n                if (held.getOrDefault(KeyCode.DOWN, false) && sub.getTranslateY() < 220) {\n                    sub.setTranslateY(sub.getTranslateY() + 2);\n                }\n            }\n        }.start();\n\n        stage.setScene(scene);\n        stage.setTitle(\"Depth\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["Keep the held keys in a map field, filled by a pressed and a released handler on the scene, and move the submarine in the timer's `handle`, which reads the map.","The field is `private Map<KeyCode, Boolean> held = new HashMap<>();`; the handlers put `true` and `false` for `event.getCode()`. In `handle`: `if (held.getOrDefault(KeyCode.UP, false) && sub.getTranslateY() > 20) { sub.setTranslateY(sub.getTranslateY() - 2); }`, and the same for Down with `< 220` and `+ 2`. Don't forget `.start()` after the timer."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["press key UP","frames 30","release key UP"],"window":{"windows":[{"title":"Depth","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":60,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":30,"list":[]}},"outline":"Window \"Depth\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 60), fill #ffd700\nframes run: 30"},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":["press key DOWN","frames 70","release key DOWN","frames 5"],"window":{"windows":[{"title":"Depth","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":220,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":75,"list":[]}},"outline":"Window \"Depth\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 220), fill #ffd700\nframes run: 75"},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":["press key UP","frames 10","press key DOWN","frames 10","release key UP","frames 5","release key DOWN"],"window":{"windows":[{"title":"Depth","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":110,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":25,"list":[]}},"outline":"Window \"Depth\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 110), fill #ffd700\nframes run: 25"},{"name":"Test 4","stdin":"","expect":"","hidden":true,"events":["press key UP","frames 80","release key UP"],"window":{"windows":[{"title":"Depth","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":20,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":80,"list":[]}},"outline":"Window \"Depth\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 20), fill #ffd700\nframes run: 80"}],"require":[],"forbid":[]},{"task":"The submarine's sonar charges while Space is held. Count the frames during which Space is held: in every frame, if Space is held, add 1 to a counter (a field). When Space is released, print `Sonar charged for N frames` with the counter, and set the counter back to 0. Other keys don't print anything. Keep the held keys in a `Map<KeyCode, Boolean>`.\n\nThe test holds Space for 12 frames, lets go, runs 5 more frames, and then presses and releases Space at once (no frame runs while it's held).\n```\nSonar charged for 12 frames\nSonar charged for 0 frames\n```\n","kind":"code","seed":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.setOnKeyReleased(event -> held.put(event.getCode(), false));\n\n        stage.setScene(scene);\n        stage.setTitle(\"Sonar\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import java.util.HashMap;\nimport java.util.Map;\nimport javafx.animation.AnimationTimer;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.input.KeyCode;\nimport javafx.scene.layout.Pane;\nimport javafx.scene.paint.Color;\nimport javafx.scene.shape.Polygon;\nimport javafx.scene.shape.Rectangle;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private Map<KeyCode, Boolean> held = new HashMap<>();\n    private int charge = 0;\n\n    @Override\n    public void start(Stage stage) {\n        Pane pane = new Pane();\n        pane.setPrefSize(360, 240);\n        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);\n        sub.setFill(Color.GOLD);\n        sub.setTranslateX(180);\n        sub.setTranslateY(120);\n        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);\n\n        Scene scene = new Scene(pane);\n        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));\n        scene.setOnKeyReleased(event -> {\n            held.put(event.getCode(), false);\n            if (event.getCode() == KeyCode.SPACE) {\n                System.out.println(\"Sonar charged for \" + charge + \" frames\");\n                charge = 0;\n            }\n        });\n\n        new AnimationTimer() {\n            @Override\n            public void handle(long now) {\n                if (held.getOrDefault(KeyCode.SPACE, false)) {\n                    charge++;\n                }\n            }\n        }.start();\n\n        stage.setScene(scene);\n        stage.setTitle(\"Sonar\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["The counter must be a field, `private int charge = 0;`: a lambda can't change a local variable of `start`. The timer adds 1 to it in every frame where `held.getOrDefault(KeyCode.SPACE, false)` is true.","In the released handler, after putting `false`, check `if (event.getCode() == KeyCode.SPACE)`: print the line, then set the counter to 0."],"tests":[{"name":"Test 1","stdin":"","expect":"Sonar charged for 12 frames\nSonar charged for 0 frames","hidden":false,"events":["press key SPACE","frames 12","release key SPACE","frames 5","key SPACE"],"window":{"windows":[{"title":"Sonar","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":120,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":17,"list":[]}},"outline":"Window \"Sonar\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), fill #ffd700\nframes run: 17"},{"name":"Test 2","stdin":"","expect":"Sonar charged for 27 frames\nSonar charged for 1 frames","hidden":true,"events":["press key LEFT","frames 3","press key SPACE","frames 20","release key LEFT","frames 7","release key SPACE","press key SPACE","frames 1","release key SPACE"],"window":{"windows":[{"title":"Sonar","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":120,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":31,"list":[]}},"outline":"Window \"Sonar\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), fill #ffd700\nframes run: 31"},{"name":"Test 3","stdin":"","expect":"Sonar charged for 40 frames","hidden":true,"events":["key UP","press key SPACE","frames 40","release key SPACE"],"window":{"windows":[{"title":"Sonar","scene":{"width":360,"height":240,"onKeyPressed":true,"onKeyReleased":true,"root":{"n":1,"type":"Pane","f":1,"prefWidth":360,"prefHeight":240,"children":[{"n":2,"type":"Rectangle","f":2,"x":0,"y":0,"width":360,"height":240,"fill":"#191970"},{"n":3,"type":"Polygon","f":3,"translateX":180,"translateY":120,"points":[-15,-6,9,-6,15,0,9,6,-15,6],"fill":"#ffd700"}]}}}],"problems":[],"frames":{"count":40,"list":[]}},"outline":"Window \"Sonar\"\n  Pane 360 x 240\n    Rectangle 360 x 240 at (0, 0), fill #191970\n    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), fill #ffd700\nframes run: 40"}],"require":[],"forbid":[]}]}