invaders.mod 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. MODULE invaders;
  2. (* Simple "Space Invaders" using the tigr Modula-2 binding.
  3. No assets: everything is drawn with tigrFillRect. Built-in font.
  4. Controls: LEFT/RIGHT move, SPACE hold to autofire, R restart
  5. (after game over), ESC quit.
  6. Compilation (from modula2/):
  7. gcc -c tigr.c
  8. gm2 -fiso -c helper.mod
  9. gm2 -fiso -I. tigr.o helper.o examples/invaders/invaders.mod \
  10. -o examples/invaders/invaders -lGL -lX11
  11. *)
  12. FROM tigr IMPORT TigrPtr, tigrWindow, tigrFree, tigrClosed, tigrClear,
  13. tigrUpdate, TPixelType, tigrFillRect, tigrPrint, tfont,
  14. tigrKeyDown, tigrKeyHeld, tigrTime, tigrTextWidth,
  15. TK_ESCAPE, TK_SPACE, TK_LEFT, TK_RIGHT;
  16. FROM helper IMPORT tigrRGB;
  17. CONST
  18. winW = 320;
  19. winH = 240;
  20. invRows = 4;
  21. invCols = 8;
  22. cell = 16; (* invader sprite box *)
  23. maxBombs = 5;
  24. playerW = 24;
  25. playerH = 6;
  26. bulletW = 3;
  27. bulletH = 9;
  28. bombW = 3;
  29. bombH = 8;
  30. bulletSpeed = 6;
  31. bombSpeed = 4;
  32. moveStep = 4;
  33. dropStep = 12;
  34. border = 4;
  35. autofire = 0.3;
  36. TYPE
  37. State = (s_play, s_win, s_lose);
  38. VAR
  39. win : TigrPtr;
  40. alive : ARRAY [0..invRows - 1] OF ARRAY [0..invCols - 1] OF BOOLEAN;
  41. gx, gy : CARDINAL; (* invader grid origin *)
  42. dir : INTEGER; (* -1 left, +1 right *)
  43. frame : CARDINAL; (* 0/1 invader sprite frame *)
  44. moveAcc, moveInt : SHORTREAL;
  45. fireAcc : SHORTREAL;
  46. playerX, playerY : CARDINAL;
  47. bulletActive : BOOLEAN;
  48. bulletX, bulletY : CARDINAL;
  49. bombFlags : ARRAY [0..maxBombs - 1] OF BOOLEAN;
  50. bombX, bombY : ARRAY [0..maxBombs - 1] OF CARDINAL;
  51. score, lives : CARDINAL;
  52. seed : CARDINAL; (* tiny LCG *)
  53. state : State;
  54. dt : SHORTREAL; (* tigrTime: delta since last call *)
  55. black, white, red, green, yellow, bombColor : TPixelType;
  56. palette : ARRAY [0..invRows - 1] OF TPixelType;
  57. row, col, n, slot : CARDINAL;
  58. scoreText : ARRAY [0..20] OF CHAR;
  59. msgbuf : ARRAY [0..60] OF CHAR;
  60. PROCEDURE CardToText(n : CARDINAL; VAR s : ARRAY OF CHAR);
  61. (* Converts n to a NUL-terminated decimal string in s. *)
  62. VAR
  63. k : CARDINAL;
  64. tmp : ARRAY [0..10] OF CHAR;
  65. j : CARDINAL;
  66. BEGIN
  67. IF n = 0 THEN
  68. s[0] := '0';
  69. s[1] := 0C;
  70. RETURN
  71. END;
  72. k := 0;
  73. WHILE n > 0 DO
  74. tmp[k] := CHR(ORD('0') + (n MOD 10));
  75. k := k + 1;
  76. n := n DIV 10
  77. END;
  78. j := 0;
  79. WHILE k > 0 DO
  80. k := k - 1;
  81. s[j] := tmp[k];
  82. j := j + 1
  83. END;
  84. s[j] := 0C
  85. END CardToText;
  86. PROCEDURE StartGame;
  87. VAR
  88. r, c : CARDINAL;
  89. BEGIN
  90. gx := 32;
  91. gy := 20;
  92. dir := 1;
  93. frame := 0;
  94. moveAcc := 0.0;
  95. moveInt := 0.5;
  96. fireAcc := 0.0;
  97. score := 0;
  98. lives := 3;
  99. playerX := (winW - playerW) / 2;
  100. bulletActive := FALSE;
  101. FOR slot := 0 TO maxBombs - 1 DO
  102. bombFlags[slot] := FALSE
  103. END;
  104. seed := 1;
  105. state := s_play;
  106. FOR r := 0 TO invRows - 1 DO
  107. FOR c := 0 TO invCols - 1 DO
  108. alive[r][c] := TRUE
  109. END
  110. END
  111. END StartGame;
  112. PROCEDURE CountAlive() : CARDINAL;
  113. VAR
  114. r, c, cnt : CARDINAL;
  115. BEGIN
  116. cnt := 0;
  117. FOR r := 0 TO invRows - 1 DO
  118. FOR c := 0 TO invCols - 1 DO
  119. IF alive[r][c] THEN
  120. cnt := cnt + 1
  121. END
  122. END
  123. END;
  124. RETURN cnt
  125. END CountAlive;
  126. PROCEDURE MoveInvaders;
  127. VAR
  128. hitBound : BOOLEAN;
  129. cnt : CARDINAL;
  130. BEGIN
  131. IF dir < 0 THEN
  132. hitBound := gx < (border + moveStep)
  133. ELSE
  134. hitBound := gx + moveStep + invCols * cell > winW - border
  135. END;
  136. IF hitBound THEN
  137. gy := gy + dropStep;
  138. dir := -dir;
  139. IF gy + invRows * cell + 8 >= winH THEN
  140. state := s_lose
  141. END
  142. ELSE
  143. IF dir < 0 THEN
  144. gx := gx - moveStep
  145. ELSE
  146. gx := gx + moveStep
  147. END
  148. END;
  149. frame := 1 - frame;
  150. (* Speeds up as invaders are destroyed. *)
  151. cnt := CountAlive();
  152. IF cnt > 0 THEN
  153. moveInt := 0.5 - (SHORTREAL(cnt - 1)
  154. * 0.38) / SHORTREAL(invRows * invCols - 1)
  155. END
  156. END MoveInvaders;
  157. PROCEDURE TrySpawnBomb;
  158. (* Drops a bomb from a surviving invader, some aimed at the player. *)
  159. VAR
  160. tgtCol, r2 : CARDINAL;
  161. tgt : INTEGER;
  162. aim : BOOLEAN;
  163. BEGIN
  164. slot := 0;
  165. WHILE (slot < maxBombs) AND bombFlags[slot] DO
  166. slot := slot + 1
  167. END;
  168. IF slot = maxBombs THEN
  169. RETURN
  170. END;
  171. seed := (seed * 1103515245 + 12345) MOD 2147483648;
  172. aim := seed MOD 4 = 0;
  173. IF aim THEN
  174. tgt := INTEGER(playerX + playerW / 2) - INTEGER(gx);
  175. IF tgt < 0 THEN
  176. tgtCol := 0
  177. ELSIF tgt >= INTEGER(invCols * cell) THEN
  178. tgtCol := invCols - 1
  179. ELSE
  180. tgtCol := tgt / cell
  181. END
  182. ELSE
  183. tgtCol := seed MOD invCols
  184. END;
  185. r2 := 0;
  186. WHILE (r2 < invRows) AND (NOT alive[r2][tgtCol]) DO
  187. r2 := r2 + 1
  188. END;
  189. IF r2 < invRows THEN
  190. bombFlags[slot] := TRUE;
  191. bombX[slot] := gx + tgtCol * cell + (cell - bombW) / 2;
  192. bombY[slot] := gy + r2 * cell + cell
  193. END
  194. END TrySpawnBomb;
  195. PROCEDURE Shoot;
  196. BEGIN
  197. IF NOT bulletActive THEN
  198. bulletActive := TRUE;
  199. bulletX := playerX + playerW / 2 - bulletW / 2;
  200. bulletY := playerY - bulletH
  201. END
  202. END Shoot;
  203. PROCEDURE MoveBullet;
  204. VAR
  205. r, c, ix, iy : CARDINAL;
  206. overlaps : BOOLEAN;
  207. slot2 : CARDINAL;
  208. BEGIN
  209. IF bulletActive THEN
  210. IF bulletY > border THEN
  211. bulletY := bulletY - bulletSpeed
  212. ELSE
  213. bulletActive := FALSE
  214. END;
  215. (* Versus invaders. *)
  216. overlaps := FALSE;
  217. r := 0;
  218. WHILE (r < invRows) AND (NOT overlaps) DO
  219. c := 0;
  220. WHILE (c < invCols) AND (NOT overlaps) DO
  221. IF alive[r][c] THEN
  222. ix := gx + c * cell;
  223. iy := gy + r * cell;
  224. IF (bulletX + bulletW > ix) AND (bulletX < ix + cell)
  225. AND (bulletY < iy + cell) AND (bulletY + bulletH > iy) THEN
  226. alive[r][c] := FALSE;
  227. score := score + 10;
  228. overlaps := TRUE
  229. END
  230. END;
  231. c := c + 1
  232. END;
  233. r := r + 1
  234. END;
  235. IF overlaps THEN
  236. bulletActive := FALSE
  237. END;
  238. (* Versus bombs (shoot them down for +5). *)
  239. IF bulletActive THEN
  240. slot2 := 0;
  241. WHILE slot2 < maxBombs DO
  242. IF bombFlags[slot2]
  243. AND (bulletX + bulletW > bombX[slot2])
  244. AND (bulletX < bombX[slot2] + bombW)
  245. AND (bulletY + bulletH > bombY[slot2])
  246. AND (bulletY < bombY[slot2] + bombH) THEN
  247. bombFlags[slot2] := FALSE;
  248. score := score + 5;
  249. bulletActive := FALSE;
  250. slot2 := maxBombs
  251. ELSE
  252. slot2 := slot2 + 1
  253. END
  254. END
  255. END;
  256. IF CountAlive() = 0 THEN
  257. state := s_win
  258. END
  259. END
  260. END MoveBullet;
  261. PROCEDURE MoveBombs;
  262. BEGIN
  263. FOR slot := 0 TO maxBombs - 1 DO
  264. IF bombFlags[slot] THEN
  265. bombY[slot] := bombY[slot] + bombSpeed;
  266. IF bombY[slot] >= winH THEN
  267. bombFlags[slot] := FALSE
  268. ELSIF (bombX[slot] + bombW > playerX)
  269. AND (bombX[slot] < playerX + playerW)
  270. AND (bombY[slot] + bombH > playerY)
  271. AND (bombY[slot] < playerY + playerH) THEN
  272. bombFlags[slot] := FALSE;
  273. IF lives > 1 THEN
  274. lives := lives - 1;
  275. playerX := (winW - playerW) / 2
  276. ELSE
  277. lives := 0;
  278. state := s_lose
  279. END
  280. END
  281. END
  282. END
  283. END MoveBombs;
  284. PROCEDURE DrawInvader(x, y : CARDINAL; color : TPixelType);
  285. (* Two classic invader frames, toggled on every move step. *)
  286. BEGIN
  287. IF frame = 0 THEN
  288. tigrFillRect(win, x + 3, y, 2, 3, color); (* antenna up *)
  289. tigrFillRect(win, x + 11, y, 2, 3, color);
  290. tigrFillRect(win, x + 1, y + 3, cell - 2, 8, color); (* body *)
  291. tigrFillRect(win, x + 4, y + 5, 2, 3, black); (* eyes *)
  292. tigrFillRect(win, x + 10, y + 5, 2, 3, black);
  293. tigrFillRect(win, x, y + 11, 4, 3, color); (* feet out *)
  294. tigrFillRect(win, x + 12, y + 11, 4, 3, color)
  295. ELSE
  296. tigrFillRect(win, x + 3, y + 1, 2, 2, color); (* antenna down *)
  297. tigrFillRect(win, x + 11, y + 1, 2, 2, color);
  298. tigrFillRect(win, x + 1, y + 3, cell - 2, 8, color);
  299. tigrFillRect(win, x + 4, y + 5, 2, 3, black);
  300. tigrFillRect(win, x + 10, y + 5, 2, 3, black);
  301. tigrFillRect(win, x + 2, y + 11, 3, 3, color); (* feet tucked *)
  302. tigrFillRect(win, x + 11, y + 11, 3, 3, color)
  303. END
  304. END DrawInvader;
  305. PROCEDURE Draw;
  306. VAR
  307. r, c, ix, iy : CARDINAL;
  308. tw : INTEGER;
  309. msgX : CARDINAL;
  310. BEGIN
  311. tigrClear(win, black);
  312. FOR r := 0 TO invRows - 1 DO
  313. FOR c := 0 TO invCols - 1 DO
  314. IF alive[r][c] THEN
  315. ix := gx + c * cell;
  316. iy := gy + r * cell;
  317. DrawInvader(ix, iy, palette[r])
  318. END
  319. END
  320. END;
  321. tigrFillRect(win, playerX, playerY, playerW, playerH, green);
  322. tigrFillRect(win, playerX + (playerW - 4) / 2, playerY - 5, 4, 5, green);
  323. IF bulletActive THEN
  324. tigrFillRect(win, bulletX, bulletY, bulletW, bulletH, white)
  325. END;
  326. FOR slot := 0 TO maxBombs - 1 DO
  327. IF bombFlags[slot] THEN
  328. tigrFillRect(win, bombX[slot], bombY[slot], bombW, bombH,
  329. bombColor)
  330. END
  331. END;
  332. scoreText := "SCORE ";
  333. CardToText(score * 10, msgbuf);
  334. n := 6;
  335. WHILE msgbuf[n - 6] # 0C DO
  336. scoreText[n] := msgbuf[n - 6];
  337. n := n + 1
  338. END;
  339. scoreText[n] := 0C;
  340. tigrPrint(win, tfont, 6, 4, white, scoreText);
  341. tigrPrint(win, tfont, 6, 14, yellow,
  342. "L/R move SPACE fire ESC quit");
  343. IF state = s_win THEN
  344. tw := tigrTextWidth(tfont, "YOU WIN");
  345. IF tw < INTEGER(winW) THEN
  346. msgX := (winW - CARDINAL(tw)) / 2
  347. ELSE
  348. msgX := 0
  349. END;
  350. tigrPrint(win, tfont, msgX, 110, green, "YOU WIN");
  351. tw := tigrTextWidth(tfont, "SPACE to restart");
  352. IF tw < INTEGER(winW) THEN
  353. msgX := (winW - CARDINAL(tw)) / 2
  354. ELSE
  355. msgX := 0
  356. END;
  357. tigrPrint(win, tfont, msgX, 130, white, "SPACE to restart")
  358. ELSIF state = s_lose THEN
  359. tw := tigrTextWidth(tfont, "GAME OVER");
  360. IF tw < INTEGER(winW) THEN
  361. msgX := (winW - CARDINAL(tw)) / 2
  362. ELSE
  363. msgX := 0
  364. END;
  365. tigrPrint(win, tfont, msgX, 110, red, "GAME OVER");
  366. tw := tigrTextWidth(tfont, "SPACE to restart");
  367. IF tw < INTEGER(winW) THEN
  368. msgX := (winW - CARDINAL(tw)) / 2
  369. ELSE
  370. msgX := 0
  371. END;
  372. tigrPrint(win, tfont, msgX, 130, white, "SPACE to restart")
  373. END
  374. END Draw;
  375. BEGIN
  376. win := tigrWindow(winW, winH, "Space Invaders", 0);
  377. black := tigrRGB(0, 0, 0);
  378. white := tigrRGB(255, 255, 255);
  379. red := tigrRGB(255, 60, 60);
  380. green := tigrRGB(60, 230, 60);
  381. yellow := tigrRGB(255, 220, 60);
  382. bombColor := tigrRGB(255, 160, 40);
  383. palette[0] := yellow;
  384. palette[1] := green;
  385. palette[2] := red;
  386. palette[3] := white;
  387. playerY := winH - playerH - 4;
  388. StartGame;
  389. WHILE (NOT (tigrClosed(win) > 0)) AND (NOT (tigrKeyDown(win, TK_ESCAPE) > 0)) DO
  390. dt := tigrTime(); (* seconds since the previous call *)
  391. IF state = s_play THEN
  392. (* Player movement *)
  393. IF (tigrKeyHeld(win, TK_LEFT) > 0) AND (playerX > border) THEN
  394. playerX := playerX - 3
  395. END;
  396. IF (tigrKeyHeld(win, TK_RIGHT) > 0) AND
  397. (playerX + playerW < winW - border) THEN
  398. playerX := playerX + 3
  399. END;
  400. fireAcc := fireAcc + dt;
  401. IF (tigrKeyDown(win, TK_SPACE) > 0)
  402. OR ((tigrKeyHeld(win, TK_SPACE) > 0) AND (fireAcc >= autofire)) THEN
  403. Shoot;
  404. fireAcc := 0.0
  405. END;
  406. (* Invader ticks: move, then rain up to two bombs. *)
  407. moveAcc := moveAcc + dt;
  408. WHILE moveAcc >= moveInt DO
  409. moveAcc := moveAcc - moveInt;
  410. MoveInvaders;
  411. TrySpawnBomb;
  412. TrySpawnBomb
  413. END;
  414. MoveBullet;
  415. MoveBombs
  416. ELSIF tigrKeyDown(win, TK_SPACE) > 0 THEN
  417. StartGame
  418. END;
  419. Draw;
  420. tigrUpdate(win)
  421. END;
  422. tigrFree(win)
  423. END invaders.