moshcode 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
@@ -4,6 +4,7 @@
4
4
  // decides the angle it leaves at, so the paddle is a steering wheel rather than
5
5
  // a wall. Without that you cannot dig a channel up the side of the wall, and
6
6
  // digging a channel is the entire reason anybody still plays this.
7
+ import { advanceBall, drawnBall, drawnCell, snapBall } from "./games-draw.mjs";
7
8
  import { acid, amber, bone, danger, rgb } from "./ui.mjs";
8
9
 
9
10
  export const WIDTH = 40;
@@ -16,7 +17,11 @@ export const BRICK_TOP = 1;
16
17
 
17
18
  export const PADDLE_W = 7;
18
19
  export const PADDLE_ROW = HEIGHT - 1;
19
- const PADDLE_STEP = 2; // a keypress, not a tick unchanged by the tick rate
20
+ // A keypress, not a tick, so this is unchanged by the tick rate — but it does
21
+ // have to keep up with the ball, and a ball taken off the end of the paddle now
22
+ // crosses a column in under two ticks. Three columns a press stays ahead of it
23
+ // at a terminal's key-repeat rate; two only just did.
24
+ const PADDLE_STEP = 3;
20
25
 
21
26
  /**
22
27
  * How often the wall is stepped. See the note in games-pong.mjs: the ball can
@@ -31,8 +36,20 @@ const PADDLE_STEP = 2; // a keypress, not a tick — unchanged by the tick rate
31
36
  */
32
37
  export const TICK_MS = 16;
33
38
 
39
+ /**
40
+ * How hard the wall is played, against the pace it was first tuned at.
41
+ *
42
+ * Launched, the old ball took three seconds to cross the board and two and a
43
+ * half to fall the height of it, which is a slow enough ball that you can put
44
+ * the paddle under it and go and make a cup of tea. It also meant the drawn
45
+ * ball moved twenty times a second, and twenty steps a second does not read as
46
+ * travel however even they are. Everything below scales together, so a ball off
47
+ * the end of the paddle leaves at the angle it always did.
48
+ */
49
+ const PACE = 1.9;
50
+
34
51
  /** The speeds below are still written per 50ms, the rate this was tuned at. */
35
- const SCALE = TICK_MS / 50;
52
+ const SCALE = (TICK_MS / 50) * PACE;
36
53
 
37
54
  const LIVES = 3;
38
55
  const BASE_VX = 0.62 * SCALE;
@@ -66,6 +83,7 @@ export function brickAt(wall, x, y) {
66
83
  function rest(state) {
67
84
  state.ball = { x: state.paddle + PADDLE_W / 2, y: PADDLE_ROW - 1, vx: 0, vy: 0 };
68
85
  state.stuck = true;
86
+ state.drawn = drawnBall(state.ball.x, state.ball.y);
69
87
  return state;
70
88
  }
71
89
 
@@ -81,8 +99,12 @@ export function launch(state) {
81
99
  export function step(state) {
82
100
  if (state.stuck) {
83
101
  // A ball that has not been launched rides the paddle, so moving before you
84
- // serve aims the serve.
102
+ // serve aims the serve. It is carried rather than travelling, so it is put
103
+ // where the paddle is rather than paced there — a stationary ball earns no
104
+ // steps, and would otherwise sit still while the paddle slid out from under
105
+ // it.
85
106
  state.ball.x = state.paddle + PADDLE_W / 2;
107
+ snapBall(state.drawn, state.ball.x, state.ball.y);
86
108
  return state;
87
109
  }
88
110
 
@@ -121,11 +143,16 @@ export function step(state) {
121
143
  }
122
144
  }
123
145
 
146
+ advanceBall(state.drawn, ball);
147
+
124
148
  if (ball.y > PADDLE_ROW) {
125
149
  state.lives--;
126
150
  if (state.lives <= 0) {
127
151
  state.lives = 0;
128
152
  state.over = `out of balls · ${state.score} points`;
153
+ // The last ball is left where it went, below the board and so off it,
154
+ // rather than resting on the row it fell past.
155
+ snapBall(state.drawn, ball.x, ball.y);
129
156
  return state;
130
157
  }
131
158
  rest(state);
@@ -197,7 +224,10 @@ export const BREAKOUT = {
197
224
  }
198
225
 
199
226
  for (let i = 0; i < PADDLE_W; i++) put(state.paddle + i, PADDLE_ROW, bone("▀"));
200
- put(Math.round(state.ball.x), Math.round(state.ball.y), state.stuck ? amber("●") : bone("●"));
227
+ // Drawn on half-rows and on its own even clock, so the ball steps the same
228
+ // distance down the wall as across it, and at a rate. See games-draw.mjs.
229
+ const ball = drawnCell(state.drawn);
230
+ put(ball.col, ball.row, (state.stuck ? amber : bone)(ball.glyph));
201
231
 
202
232
  return grid.map((row) => row.map((cell) => cell ?? " ").join(""));
203
233
  },
@@ -0,0 +1,115 @@
1
+ // Shared drawing for the things in the arcade that move on a diagonal.
2
+ //
3
+ // A terminal cell is about twice as tall as it is wide, so a board drawn one
4
+ // object per cell has half the resolution going down that it has going across.
5
+ // For a ball that is not a rounding detail, it is the whole reason the motion
6
+ // looks wrong. Two things fall out of it, and both of them read as bad physics
7
+ // rather than as a coarse grid:
8
+ //
9
+ // A step sideways moves the ball one pixel and a step down moves it two, so the
10
+ // same ball appears to travel faster down the board than across it, and a
11
+ // trajectory the maths has at forty-five degrees is drawn at sixty.
12
+ //
13
+ // Worse, the two steps keep their own schedules. Crossing into the next column
14
+ // and crossing into the next row almost never land on the same tick, so a ball
15
+ // going diagonally does not move diagonally — it hops sideways, then a tick or
16
+ // two later hops down. Measured on pong, the ball holds a cell for four ticks,
17
+ // four ticks, then two and two as a row change splits one of those spans. That
18
+ // 4-4-2-2 stutter, three or four times a second, is what is left of the jiggle
19
+ // after the clock was fixed.
20
+ //
21
+ // Half blocks fix both. `▀` and `▄` each fill half a cell, which is close to
22
+ // square, so the ball is drawn on a grid with the same pitch both ways: it
23
+ // steps the same distance whichever way it goes, a row is two steps rather than
24
+ // one, and the corner it turns is half as wide. It is also a better ball than
25
+ // `●` was — a square pixel moving on a square grid, rather than a round dot
26
+ // snapping between cells twice its own height apart.
27
+ //
28
+ // Half blocks fix the size of the steps. They do not fix when the steps happen,
29
+ // which turned out to be the other half of it — see `drawnBall` below.
30
+
31
+ /**
32
+ * The ball's position in half-rows — the unit it is actually drawn in.
33
+ *
34
+ * `y` is a row centre, so row r covers y from r - 0.5 up to r + 0.5: below the
35
+ * centre the ball is in the top half of that cell, at or above it the bottom.
36
+ * Half-rows and columns are the same size on screen, so this and the column
37
+ * together are a square lattice, and a step is a step whichever way it goes.
38
+ */
39
+ export const halfRow = (y) => {
40
+ const row = Math.round(y);
41
+ return y < row ? row * 2 : row * 2 + 1;
42
+ };
43
+
44
+ /**
45
+ * A drawn ball, released on its own even clock.
46
+ *
47
+ * Equal pitch fixed the size of the ball's steps but not their timing, and the
48
+ * timing is the rest of the jiggle. Rounding the true position to the lattice
49
+ * moves the ball whenever it happens to cross a column edge or a half-row edge,
50
+ * and those two are on unrelated schedules: a ball with the two periods close
51
+ * but not equal — which is what a near-diagonal is, and what breakout launches
52
+ * at — beats between them. Measured, breakout held a cell for 16ms, then 80ms,
53
+ * then 48ms, five times a second. The steps were the right size and still the
54
+ * ball looked like it was struggling, because nothing was moving at a rate.
55
+ *
56
+ * So the true position is not what is drawn. It decides only which way the
57
+ * drawn ball owes a step; when that step is paid is decided by a clock that
58
+ * ticks at the ball's own speed. `owed` accrues at |vx| + 2|vy| lattice units
59
+ * per tick — the distance the true ball covers, measured the way the drawn one
60
+ * has to travel it — and a whole unit buys one step. The drawn ball therefore
61
+ * moves every 1/speed ticks whatever angle it is on, trailing the true one by
62
+ * under a unit, which is under half a character.
63
+ *
64
+ * It cannot fall behind: the same accrual that paces the ball also lets it pay
65
+ * two steps in a tick when the ball is genuinely moving that fast.
66
+ */
67
+ export function drawnBall(x, y) {
68
+ return snapBall({ col: 0, half: 0, owed: 0 }, x, y);
69
+ }
70
+
71
+ /**
72
+ * Put the drawn ball exactly where the real one is, with no debt either way.
73
+ *
74
+ * For the moves that are not travel and so have nothing to smooth: a serve, a
75
+ * fresh ball on the paddle, the ball riding a paddle that is being aimed.
76
+ */
77
+ export function snapBall(drawn, x, y) {
78
+ drawn.col = Math.round(x);
79
+ drawn.half = halfRow(y);
80
+ drawn.owed = 0;
81
+ return drawn;
82
+ }
83
+
84
+ /** Far enough apart that the ball was put there rather than travelled there. */
85
+ const TELEPORT = 4;
86
+
87
+ /** Pay out whatever steps the ball has earned this tick. */
88
+ export function advanceBall(drawn, ball) {
89
+ const col = Math.round(ball.x);
90
+ const half = halfRow(ball.y);
91
+ if (Math.abs(col - drawn.col) + Math.abs(half - drawn.half) >= TELEPORT) return snapBall(drawn, ball.x, ball.y);
92
+
93
+ drawn.owed += Math.abs(ball.vx) + Math.abs(ball.vy) * 2;
94
+ while (drawn.owed >= 1) {
95
+ const dcol = col - drawn.col;
96
+ const dhalf = half - drawn.half;
97
+ if (dcol === 0 && dhalf === 0) break;
98
+ // Whichever axis is further behind goes first, which is what keeps a
99
+ // diagonal a staircase instead of a sideways run and then a drop.
100
+ if (Math.abs(dcol) >= Math.abs(dhalf)) drawn.col += Math.sign(dcol);
101
+ else drawn.half += Math.sign(dhalf);
102
+ drawn.owed -= 1;
103
+ }
104
+ // A ball that has caught up banks at most one step, so that standing still
105
+ // for a moment cannot be turned into a lurch later.
106
+ if (drawn.owed > 1) drawn.owed = 1;
107
+ return drawn;
108
+ }
109
+
110
+ /** The cell and half block to write for a drawn ball. */
111
+ export const drawnCell = (drawn) => ({
112
+ col: drawn.col,
113
+ row: drawn.half >> 1,
114
+ glyph: drawn.half % 2 === 0 ? "▀" : "▄",
115
+ });
@@ -7,6 +7,7 @@
7
7
  // A flat return it will always get; one taken off the end of your paddle it will
8
8
  // not. That is the whole game, and it is why the angle off the paddle depends on
9
9
  // where the ball hit it.
10
+ import { advanceBall, drawnBall, drawnCell, snapBall } from "./games-draw.mjs";
10
11
  import { acid, bone, danger, dim } from "./ui.mjs";
11
12
 
12
13
  export const WIDTH = 44;
@@ -36,13 +37,25 @@ export const TARGET = 7; // first to this many
36
37
  */
37
38
  export const TICK_MS = 16;
38
39
 
40
+ /**
41
+ * How hard the table is played, against the pace it was first tuned at.
42
+ *
43
+ * The old pace put the ball across the table in just under three seconds. That
44
+ * is not a ball being hit, it is a ball being carried, and it was also why the
45
+ * drawn ball only moved twenty times a second — too few steps for any of them
46
+ * to be smooth. Everything below is scaled by this, the machine along with the
47
+ * ball, so the balance is exactly the one that was tuned; only the clock it is
48
+ * played against changes.
49
+ */
50
+ const PACE = 1.9;
51
+
39
52
  /**
40
53
  * The speeds below are still written per 55ms — the rate this game was tuned
41
54
  * at — and scaled to the tick. Keeping the tuned numbers legible matters more
42
55
  * than saving a multiply: they are what makes the machine beatable off the end
43
56
  * of the paddle and not from the middle, and that balance is the game.
44
57
  */
45
- const SCALE = TICK_MS / 55;
58
+ const SCALE = (TICK_MS / 55) * PACE;
46
59
 
47
60
  const SERVE_SPEED = 0.85 * SCALE;
48
61
  const MAX_SPEED = 1.7 * SCALE;
@@ -64,6 +77,8 @@ export function serve(state, toward) {
64
77
  // Never dead flat: a ball with no angle is a rally nobody can lose.
65
78
  vy: (state.rng() < 0.5 ? -1 : 1) * (0.15 + state.rng() * 0.2) * SCALE,
66
79
  };
80
+ // A serve is a ball put on the table, not a ball that travelled there.
81
+ state.drawn = drawnBall(state.ball.x, state.ball.y);
67
82
  return state;
68
83
  }
69
84
 
@@ -105,6 +120,12 @@ export function step(state) {
105
120
 
106
121
  if (ball.x < 0) { state.theirs++; point(state, 1); }
107
122
  else if (ball.x > WIDTH - 1) { state.yours++; point(state, -1); }
123
+ // Anything else is the ball travelling, which is the only thing the drawn
124
+ // ball is asked to follow — a serve puts it back itself.
125
+ else advanceBall(state.drawn, ball);
126
+ // A match ends with the ball where it went out, off the table and so off the
127
+ // board, rather than parked on the edge it left by.
128
+ if (state.over) snapBall(state.drawn, ball.x, ball.y);
108
129
 
109
130
  // The machine: idle in the middle until the ball is on its half, then chase
110
131
  // the ball's row. Perfect tracking here would make the game unloseable for it,
@@ -164,7 +185,10 @@ export const PONG = {
164
185
 
165
186
  for (const row of paddleRows(state.you)) put(YOU_COL, row, acid("█"));
166
187
  for (const row of paddleRows(state.them)) put(THEM_COL, row, danger("█"));
167
- put(Math.round(state.ball.x), Math.round(state.ball.y), bone("●"));
188
+ // Drawn on half-rows and on its own even clock, so the ball steps the same
189
+ // distance up the table as across it, and at a rate. See games-draw.mjs.
190
+ const ball = drawnCell(state.drawn);
191
+ put(ball.col, ball.row, bone(ball.glyph));
168
192
 
169
193
  return grid.map((row, y) => row.map((cell, x) => (
170
194
  // The net, which is only there so the middle of the table has a middle.