-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSnakeBoard.java
More file actions
107 lines (95 loc) · 4.16 KB
/
Copy pathSnakeBoard.java
File metadata and controls
107 lines (95 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package Phase7_Concurrency.Multithreading.SnakeGame;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
/**
* SnakeBoard — the rendering component.
* <p>
*
* Lives on the SWING EVENT-DISPATCH THREAD (EDT). All painting happens
* on the EDT, courtesy of the AWT framework. Key events arrive on the
* EDT and we hand them to GameState via setDirection — that call is
* thread-safe because GameState itself synchronizes.
* <p>
*
* Threading contract
* ------------------
* - paintComponent(g) reads a state snapshot ONCE under the lock,
* then paints — no chance of seeing a half-modified snake.
* - keyPressed forwards to GameState.setDirection / GameState.reset.
* <p>
*
* Look & feel
* -----------
* 16x16 cell tiles, score in the top-left, "GAME OVER" overlay when
* appropriate.
*/
public final class SnakeBoard extends JPanel {
public static final int CELL = 22;
private final GameState state;
private Runnable onReset = () -> {}; // injected by main runner
public SnakeBoard(GameState state) {
this.state = state;
setPreferredSize(new Dimension(GameState.COLS * CELL, GameState.ROWS * CELL));
setBackground(new Color(20, 24, 28));
setFocusable(true);
addKeyListener(new KeyAdapter() {
@Override public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP, KeyEvent.VK_W -> state.setDirection(Direction.UP);
case KeyEvent.VK_DOWN, KeyEvent.VK_S -> state.setDirection(Direction.DOWN);
case KeyEvent.VK_LEFT, KeyEvent.VK_A -> state.setDirection(Direction.LEFT);
case KeyEvent.VK_RIGHT, KeyEvent.VK_D -> state.setDirection(Direction.RIGHT);
case KeyEvent.VK_R -> { state.reset(); onReset.run(); }
}
}
});
}
public void setOnReset(Runnable r) { this.onReset = r; }
@Override protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// Single atomic read.
GameState.Snapshot snap = state.snapshot();
// Subtle grid
g2.setColor(new Color(28, 34, 40));
for (int x = 0; x < GameState.COLS; x++)
for (int y = 0; y < GameState.ROWS; y++)
g2.drawRect(x * CELL, y * CELL, CELL, CELL);
// Food
if (snap.food() != null) {
g2.setColor(new Color(220, 80, 80));
g2.fillRoundRect(snap.food().x() * CELL + 3, snap.food().y() * CELL + 3,
CELL - 6, CELL - 6, 8, 8);
}
// Snake
Cell[] body = snap.body();
for (int i = 0; i < body.length; i++) {
Cell c = body[i];
// Head a touch brighter than body.
g2.setColor(i == 0 ? new Color(120, 220, 140) : new Color(70, 170, 100));
g2.fillRoundRect(c.x() * CELL + 1, c.y() * CELL + 1, CELL - 2, CELL - 2, 6, 6);
}
// HUD
g2.setColor(new Color(220, 220, 220));
g2.setFont(g2.getFont().deriveFont(Font.BOLD, 14f));
g2.drawString("score " + snap.score(), 8, 18);
g2.drawString("[WASD / arrows] R = restart", 8, getHeight() - 8);
if (snap.gameOver()) {
g2.setColor(new Color(0, 0, 0, 170));
g2.fillRect(0, 0, getWidth(), getHeight());
g2.setColor(Color.WHITE);
g2.setFont(g2.getFont().deriveFont(Font.BOLD, 28f));
String msg = "GAME OVER";
int w = g2.getFontMetrics().stringWidth(msg);
g2.drawString(msg, (getWidth() - w) / 2, getHeight() / 2 - 10);
g2.setFont(g2.getFont().deriveFont(Font.PLAIN, 14f));
String s2 = "score " + snap.score() + " — press R";
int w2 = g2.getFontMetrics().stringWidth(s2);
g2.drawString(s2, (getWidth() - w2) / 2, getHeight() / 2 + 16);
}
}
}