2
0

upacman.pp 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497
  1. unit upacman;
  2. interface
  3. uses
  4. sysutils, classes, types, web, js;
  5. const
  6. TimerInterval = 20;
  7. GridXSize = 30;
  8. GridYSize = 33;
  9. DrawGrid = False;
  10. ControlCount = 5;
  11. ControlNames : Array[1..ControlCount] of string = ('left','right','down','up','pause');
  12. type
  13. TAudio = (aStart,aDie,aEatGhost,aEatPill);
  14. TStr4 = String; // set of N,E,S,W
  15. TSprite=record
  16. SpImg : TJSHTMLImageElement; // Image of a ghost
  17. XY : TPoint; // Grid x and y
  18. Sx,Sy : double; // smooth x and y between 0 en 1
  19. Dir : char; // N,E,S,W
  20. Spd : double;
  21. StartPos : TPoint;
  22. end;
  23. TCell=record
  24. WallType :(wtNone,wtEW,wtNS,wtNE,wtNW,wtSW,wtSE,wtNoGo);
  25. PillType :(ptNone,ptPill,ptSuperPill);
  26. I :integer; // used for searching the maze
  27. Dirty : Boolean;
  28. end;
  29. TField = array[0..GridYSize-1] of String;
  30. { TPacman }
  31. TProcedure = Procedure Of Object;
  32. { TPacmanAudio }
  33. TPacmanAudio = Class
  34. private
  35. FOnLoaded: TNotifyEvent;
  36. FLoaded : Boolean;
  37. procedure AudioLoaded;
  38. function CheckEnd(Event: TEventListenerEvent): boolean;
  39. function CheckplayOK(Event: TEventListenerEvent): boolean;
  40. published
  41. files : Array [TAudio] of TJSHTMLAudioElement;
  42. filesOK : Array [TAudio] of Boolean;
  43. Playing : Array [TAudio] of Boolean;
  44. Procedure LoadAudio;
  45. Procedure play(aAudio : Taudio);
  46. Procedure DisableSound;
  47. Procedure Pause;
  48. Procedure Resume;
  49. Property Loaded : Boolean Read FLoaded Write FLoaded;
  50. Property OnLoaded : TNotifyEvent Read FOnLoaded Write FonLoaded;
  51. end;
  52. TPacman = class(TComponent)
  53. Private
  54. // Html image elements
  55. // 0 = pacman, virtual
  56. // 1..4 : Ghost
  57. // 5 = scared
  58. ImgGhost : Array[0..5] of TJSHTMLImageElement;
  59. ImgBonus: TJSHTMLImageElement;
  60. SpriteTimer: NativeInt;
  61. pnBonusBarOuter: TJSHTMLElement;
  62. pnBonusBarInner: TJSHTMLElement;
  63. pnScareBarOuter: TJSHTMLElement;
  64. pnScareBarInner: TJSHTMLElement;
  65. lbBonusCnt: TJSHTMLElement;
  66. lbLives: TJSHTMLElement;
  67. lbScore: TJSHTMLElement;
  68. lbStatus: TJSHTMLElement;
  69. lbHiscore: TJSHTMLElement;
  70. lbGhostCnt: TJSHTMLElement;
  71. FCanvasEl:TJSHTMLCanvasElement;
  72. FCanvas:TJSCanvasRenderingContext2D;
  73. FCBXSound:TJSHTMLInputElement;
  74. FBtnReset : TJSHTMLButtonElement;
  75. FAudio : TPacmanAudio;
  76. function CheckSound(Event: TEventListenerEvent): boolean;
  77. procedure DoAudioLoaded(Sender: TObject);
  78. function DoResetClick(aEvent: TJSMouseEvent): boolean;
  79. procedure InitAudio;
  80. procedure MarkCellsDirty;
  81. private
  82. FAudioDisabled: Boolean;
  83. FCanvasID: String;
  84. FResetID: String;
  85. Pause:boolean;
  86. LivesLeft:integer;
  87. BonusCnt :integer;
  88. GhostCnt :integer;
  89. BonusTimer:integer;
  90. ScareTimer:integer;
  91. PacMouthOpen:integer;
  92. PacMouthOpenDir:integer;
  93. PillsLeft:integer;
  94. PacmanDir:char;
  95. score,HiScore:integer;
  96. // 0: Packman.
  97. // 1..4 : ghost
  98. // 5: Bonus
  99. Sprite:array[0..5] of TSprite;
  100. Cells:array[0..GridXSize-1,0..GridYSize] of TCell;
  101. FDying : Boolean;
  102. // Maze solving code
  103. function SolveMaze (P1,P2: TPoint): boolean;
  104. function SolveMazeStep1(P1,P2: TPoint): boolean;
  105. function SolveMazeStep2(P1,P2: TPoint): boolean;
  106. function SolveMazeStep3(P1,P2: TPoint): boolean;
  107. // Display code
  108. procedure line(x1, y1, x2, y2: integer);
  109. procedure DrawCells(DirtyOnly : Boolean = False);
  110. procedure DrawPacman();
  111. procedure CheckGameOver;
  112. procedure StartTimer;
  113. procedure ShowText(aText: string; OnDone : TProcedure);
  114. procedure UpdateScore();
  115. procedure UpdateStatus(aText : String);
  116. // Initializing code
  117. procedure InitSprite(var aSprite: TSprite; aImg: TJSHTMLImageElement; aSpd: Double);
  118. procedure InitSprites();
  119. procedure InitVars(aField: TField);
  120. procedure InitCells(aField: TField);
  121. procedure SetGhostScared(aScared: boolean);
  122. // Business code: TestAndGet
  123. function GetGhostDir(aXY:TPoint; aOldDir: char): char;
  124. function GetBestDir(aXY:TPoint): char;
  125. function GetPossibleDir(aXY:TPoint): TStr4;
  126. function GetPacmanDir(aXY:TPoint; aOldDir: char): char;
  127. procedure GetRandomCellAndDir(var aXY:TPoint; var aDir: char);
  128. // Business code: Actions
  129. procedure StopTimer;
  130. Function DoRestartClick(aEvent: TJSMouseEvent): boolean;
  131. procedure EatPill(aXY: TPoint);
  132. procedure EatSuperPill(aXY: TPoint);
  133. procedure EatBonus();
  134. procedure EatGhost(var aGhost: TSprite);
  135. procedure ClearCell(aXY: TPoint);
  136. procedure MoveSprite(aSpriteInx:integer);
  137. function DoBonusTimer(): boolean;
  138. procedure DoScareTimer();
  139. Procedure DrawScene;
  140. // Business code: Decisions
  141. procedure CollisionDetect(var aXY:TPoint);
  142. procedure RestartGame();
  143. procedure RestartLevel();
  144. procedure PacmanDies();
  145. procedure NextLevel();
  146. procedure GameOver();
  147. // Debug & Test
  148. // procedure DbgShow();
  149. // Business code: Actions
  150. Procedure PlaySound(aAudio : TAudio);
  151. procedure DoSpriteTimer;
  152. // User response code
  153. function HandleKeyPress(k : TJSKeyBoardEvent) : Boolean;
  154. function DoMouseClick(aEvent: TJSMouseEvent): boolean;
  155. Public
  156. // Initializing code
  157. Constructor Create(aOwner : TComponent); override;
  158. Procedure SetupPacman;
  159. Procedure Start;
  160. Property CanvasID : String Read FCanvasID Write FCanvasID;
  161. Property ResetID : String Read FResetID Write FResetID;
  162. Property AudioDisabled : Boolean Read FAudioDisabled Write FAudioDisabled;
  163. end;
  164. implementation
  165. //==============================================================================
  166. // Generic constants
  167. //==============================================================================
  168. // These constants define the look and feel of the game.
  169. // They set speeds and timeouts, and the define a playing field
  170. // To make the definition of a different playing field easier it is defined as
  171. // an array of strings, in which each character defines specific cell-properties
  172. // The initialization code reads this and uses it to build an array of type TCell[].
  173. //
  174. // The const Level1field defines a playing field.
  175. // These are the characters used to define the habitat of the ghosts and pacman
  176. // 'x' : a NoGo area. It shows up empty on the screen, but ghosts, pacman
  177. // and bonusses cannot go there.
  178. // '-','|' : a horizontal or verical wall
  179. // '/','\' : a cornerwall, which one depends on surrounding cells
  180. // '1'..'4' : starting position of ghost 1 to 4
  181. // 'P' : starting position of Pacman
  182. // ' ' : empty space, Pacman, ghosts and bonusses can go there
  183. // '.' : simple pill, Pacman, ghosts and bonusses can go there
  184. // 'o' : super pill, Pacman, ghosts and bonusses can go there.
  185. // This also sets the "ScareTheGhosts" timer
  186. //==============================================================================
  187. const
  188. CellSize = 16; // do not change...
  189. GhostSpeedScared = 0.10; // Speed of ghosts when scared
  190. GhostSpeedNormal = 0.20; // Speed of ghosts when not scared.
  191. PacmanSpeed = 0.25; // Speed of Pacman
  192. BonusSpeed = 0.04; // speed of cherries
  193. BonusTimeOut1 = 500; // time for cherries not visible
  194. BonusTimeOut2 = 300; // time for cherries visible
  195. ScareTimeOut = 300; // time that the ghosts stay scared
  196. HuntFactor = 0.5; // 0.0:ghosts move random, 1.0=ghosts really hunt
  197. AudioNames : Array[TAudio] of string = ('start','die','eatghost','eatpill');
  198. const
  199. Level1Field : TField =
  200. ('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  201. 'x/------------\/------------\x',
  202. 'x|............||............|x',
  203. 'x|./--\./---\.||./---\./--\.|x',
  204. 'x|o|xx|.|xxx|.||.|xxx|.|xx|o|x',
  205. 'x|.\--/.\---/.\/.\---/.\--/.|x',
  206. 'x|..........................|x',
  207. 'x|./--\./\./------\./\./--\.|x',
  208. 'x|.\--/.||.\--\/--/.||.\--/.|x',
  209. 'x|......||....||....||......|x',
  210. 'x\----\.|\--\ || /--/|./----/x',
  211. 'xxxxxx|.|/--/ \/ \--\|.|xxxxxx',
  212. 'xxxxxx|.|| ||.|xxxxxx',
  213. 'xxxxxx|.|| /-- --\ ||.|xxxxxx',
  214. '------/.\/ | 1 3 | \/.\------',
  215. ' . | 2 4 | . ',
  216. '------\./\ | | /\./------',
  217. 'xxxxxx|.|| \------/ ||.|xxxxxx',
  218. 'xxxxxx|.|| ||.|xxxxxx',
  219. 'xxxxxx|.|| /------\ ||.|xxxxxx',
  220. 'x/----/.\/ \--\/--/ \/.\----\x',
  221. 'x|............||............|x',
  222. 'x|./--\./---\.||./---\./--\.|x',
  223. 'x|.\-\|.\---/.\/.\---/.|/-/.|x',
  224. 'x|o..||.......P........||..o|x',
  225. 'x\-\.||./\./------\./\.||./-/x',
  226. 'x/-/.\/.||.\--\/--/.||.\/.\-\x',
  227. 'x|......||....||....||......|x',
  228. 'x|./----/\--\.||./--/\----\.|x',
  229. 'x|.\--------/.\/.\--------/.|x',
  230. 'x|..........................|x',
  231. 'x\--------------------------/x',
  232. 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  233. const
  234. WallSet = ['-','|','\','/'];
  235. clBlack = 'black';
  236. clWhite = 'white';
  237. clRed = 'red';
  238. clYellow = '#FFFF00';
  239. clBlue = 'blue';
  240. clLime = 'lime';
  241. { TPacman }
  242. constructor TPacman.Create(aOwner: TComponent);
  243. begin
  244. inherited;
  245. FaudioDisabled:=True;
  246. FAudio:=TPacmanAudio.Create;
  247. Faudio.OnLoaded:=@DoAudioLoaded;
  248. SetupPacman;
  249. end;
  250. //==============================================================================
  251. // Display code
  252. //==============================================================================
  253. // This code is responsible for showing pacman, ghosts, bonuses, scores on the
  254. // screen It uses global variables and the Cells[] array to know where and what
  255. // ShowText() this code shows a flashing text (how surprising) in the
  256. // middle of the playing field for about 3 seconds
  257. // Line() draws a line on img.canvas (should be a standard function!!!)
  258. // DrawCells() clears and draws the complete playingfield according to the
  259. // cell properties in the Cell[] array. Does not draw Pacman,
  260. // ghosts or flying bonusses.
  261. // DrawPacman() Draws an image of Pacman in sprite[0] depending on direction
  262. // UpdateScore() Updates the labels for lives, score, hiscore etc.
  263. Type
  264. { TFlashText }
  265. TFlashText = Class(TObject)
  266. FPacMan : TPacMan;
  267. FText : String;
  268. FFlashInterval : NativeInt;
  269. FCount : Integer;
  270. FonDone : TProcedure;
  271. Procedure DoFlash;
  272. Constructor Create(aPacMan : TPacMan; aText : String; aOnDone : TProcedure);
  273. end;
  274. { TFlashText }
  275. procedure TFlashText.DoFlash;
  276. var
  277. n,x,y:integer;
  278. FS : TJSTextMetrics;
  279. begin
  280. // FPacMan.FCanvas.fillStyle:=clBlack;
  281. if FCount mod 2=0 then
  282. FPacMan.FCanvas.FillStyle:=clRed //textbackground is black
  283. else
  284. FPacMan.FCanvas.FillStyle:=clYellow; //textbackground is black
  285. FPacMan.FCanvas.Font:='40px Roboto'; //make text really big
  286. // position text in the middle of the field
  287. FS:=FPacMan.FCanvas.measureText(FText);
  288. x:=FPacMan.FCanvasEl.Width div 2-Round(FS.width) div 2;
  289. y:=FPacMan.FCanvasEl.Height div 2- 20 { Round(FS.actualBoundingBoxAscent) div 2};
  290. FPacMan.FCanvas.FillText(FText,x,y);
  291. Inc(FCount);
  292. if FCount>=10 then
  293. begin
  294. window.ClearInterval(FFlashInterval);
  295. FPacMan.DrawScene;
  296. if Assigned(FonDone) then
  297. FonDone();
  298. Free;
  299. end;
  300. end;
  301. constructor TFlashText.Create(aPacMan : TPacMan; aText: String; aOnDone : TProcedure);
  302. begin
  303. FPacMan:=aPacMan;
  304. FText:=aText;
  305. FOnDone:=aOnDone;
  306. DoFlash;
  307. FFlashInterval:=window.setInterval(@DoFlash,150);
  308. end;
  309. procedure TPacman.ShowText(aText: string; OnDone : TProcedure);
  310. begin
  311. TFlashText.Create(Self,aText,OnDone);
  312. end;
  313. procedure TPacman.line(x1, y1, x2, y2: integer);
  314. begin // should be a standard method of a canvas...
  315. FCanvas.BeginPath;
  316. FCanvas.MoveTo(x1,y1);
  317. FCanvas.LineTo(x2,y2);
  318. FCanvas.stroke();
  319. end;
  320. procedure TPacman.DrawCells(DirtyOnly : Boolean = False);
  321. const
  322. Sze=CellSize;
  323. HSze=CellSize div 2;
  324. Procedure DoArc(x,y,r,a1,a2 : Double; anti : boolean = false);
  325. begin
  326. FCanvas.BeginPath;
  327. FCanvas.Arc(x,y,r,a1,a2,anti);
  328. FCanvas.Stroke;
  329. end;
  330. var
  331. x,y,sx,sy,r:integer;
  332. begin
  333. // Clear where necessary
  334. with FCanvas do
  335. if DirtyOnly then
  336. begin
  337. // Only selected cells
  338. StrokeStyle:=clBlack;
  339. FillStyle:=clBlack;
  340. for x:=0 to GridXSize-1 do
  341. for y:=0 to GridYSize-1 do
  342. if Cells[x,y].Dirty or not DirtyOnly then
  343. begin
  344. sx:=x*Sze;
  345. sy:=y*Sze; //calculate pixel position on screen
  346. FillRect(sx,sy,sze,sze);
  347. end;
  348. end
  349. else
  350. begin
  351. // clear screen to black
  352. FillStyle:='black';
  353. FillRect(0,0, FCanvasEl.Width,FCanvasEl.Height);
  354. // Draw supportGrid (helpfull during development, not needed)
  355. if DrawGrid then
  356. begin
  357. lineWidth:=2; // Pen.width:=1;
  358. StrokeStyle:='#202020';
  359. for x:=0 to GridXSize do
  360. line(x*Sze,0,x*Sze,Sze*(GridYSize));
  361. for y:=0 to GridYSize do
  362. line(0,y*Sze,Sze*(GridXSize),y*Sze);
  363. end;
  364. end;
  365. // Draw pills
  366. With FCanvas do
  367. begin
  368. // Draw Pills
  369. StrokeStyle:=clWhite;
  370. FillStyle:=clWhite;
  371. for x:=0 to GridXSize-1 do
  372. for y:=0 to GridYSize-1 do
  373. if Cells[x,y].Dirty or not DirtyOnly then
  374. begin
  375. sx:=x*Sze+HSze;
  376. sy:=y*Sze+HSze;
  377. r:=0;
  378. case Cells[x,y].PillType of
  379. ptPill : r:=2;
  380. ptSuperPill : r:=6;
  381. end;
  382. if r>0 then
  383. begin
  384. BeginPath;
  385. Arc(sx,sy,r,0,2*Pi);
  386. Fill;
  387. end;
  388. end;
  389. end;
  390. // Draw Walls per cell
  391. With FCanvas do
  392. begin
  393. StrokeStyle:=clBlue;
  394. FillStyle:=clBlue;
  395. LineWidth:=sze div 4;
  396. for x:=0 to GridXSize-1 do
  397. for y:=0 to GridYSize-1 do
  398. if Cells[x,y].Dirty or not DirtyOnly then
  399. begin
  400. sx:=x*Sze;
  401. sy:=y*Sze; //calculate pixel position on screen
  402. case Cells[x,y].WallType of
  403. wtEW: line(sx,sy+hsze,sx+sze,sy+hsze); // left to right
  404. wtNS: line(sx+hsze,sy,sx+hsze,sy+sze); // top to bottom
  405. wtSW: DoArc(sx , sy+Sze, Sze / 2,0 ,(3*Pi/2),true); // bottom to left
  406. wtNE: DoArc(sx+Sze, sy , Sze / 2,Pi/2,Pi); // top to right
  407. wtSE: DoArc(sx+Sze, sy+Sze, Sze / 2,Pi ,Pi*3/2); // bottom to right
  408. wtNW: DoArc(sx , sy , Sze / 2,0 ,Pi/2); // top to left
  409. end;
  410. Cells[x,y].Dirty:=False;
  411. end;
  412. end;
  413. end;
  414. procedure TPacman.DrawPacman();
  415. Const
  416. Radius = 12;
  417. Offset = CellSize;
  418. EyeY = CellSize * 2/3;
  419. LeftEyeX = CellSize * 2/3;
  420. RightEyeX = CellSize * 4/3;
  421. MouthRadius = CellSize * 1/3;
  422. EyeRadius = 1.5;
  423. Var
  424. X,Y : Double;
  425. Procedure Pie(aAngle : double);
  426. Var
  427. aStart,aEnd : Double;
  428. begin
  429. if PacMouthOpen=0 then
  430. begin
  431. aStart:=0;
  432. aEnd:=2*pi
  433. end
  434. else
  435. begin
  436. aStart:=aAngle + (PacMouthOpen/90)*(Pi/2);
  437. if aStart>2*Pi then
  438. aStart:=aStart-2*pi;
  439. aEnd :=aAngle - (PacMouthOpen/90)*(Pi/2);
  440. {
  441. // Draw this to clear first
  442. FCtx.fillStyle:=clBlack;
  443. FCtx.StrokeStyle:=clBlack;
  444. FCtx.Arc(X+15,Y+15,Radius,0,2*pi,True);
  445. FCtx.Fill;
  446. }
  447. end;
  448. With FCanvas do
  449. begin
  450. BeginPath;
  451. MoveTo(X+OffSet,Y+Offset);
  452. Arc(X+Offset,Y+Offset,Radius,aStart,aEnd);
  453. LineTo(X+Offset,Y+Offset);
  454. Fill;
  455. end;
  456. end;
  457. begin
  458. X:=Sprite[0].XY.x*CellSize-CellSize/2;
  459. Y:=Sprite[0].XY.y*CellSize-CellSize/2;
  460. if PacMouthOpen>40 then
  461. PacMouthOpenDir:=-10 // if maxopen then start closing
  462. else if PacMouthOpen<2 then
  463. PacMouthOpenDir:= 10; // if minopen then start opening
  464. inc(PacMouthOpen,PacMouthOpenDir); // adjust mouth opening
  465. with FCanvas do
  466. begin
  467. FillStyle:=clYellow; // set face color to yellow
  468. StrokeStyle:=clYellow; // pen too
  469. case Sprite[0].Dir of // draw face depending on direction (opposite to what you'd expect)
  470. 'E': Pie(Pi); // to the right
  471. 'W': Pie(0); // to the left
  472. 'N': Pie(3*Pi/2); // to the top
  473. 'S': Pie(Pi/2); // to the bottom
  474. else
  475. beginPath;
  476. Arc(X+OffSet,y+OffSet,Radius,0,2*pi); // whole face area
  477. Fill();
  478. FillStyle:=clBlack; //
  479. StrokeStyle:=clBlack; //
  480. beginPath;
  481. Arc(X+LeftEyeX,Y+EyeY,EyeRadius,0,2*pi); // left eye
  482. Stroke;
  483. beginPath;
  484. Arc(X+RightEyeX,Y+EyeY,Eyeradius,0,2*pi); // right eye
  485. Stroke;
  486. LineWidth:=3; //
  487. beginPath;
  488. arc(X+offSet,Y+OffSet,MouthRadius,0,Pi);//mouth
  489. Stroke;
  490. end;
  491. end;
  492. end;
  493. procedure TPacman.UpdateScore();
  494. begin
  495. if Score>HiScore then
  496. HiScore:=Score;
  497. lbScore.InnerText := inttostr(Score);
  498. lbHiScore.InnerText := inttostr(HiScore);
  499. lbLives.InnerText := inttostr(LivesLeft);
  500. lbBonusCnt.InnerText:= inttostr(BonusCnt);
  501. lbGhostCnt.InnerText:= inttostr(GhostCnt);
  502. end;
  503. procedure TPacman.UpdateStatus(aText: String);
  504. begin
  505. lbStatus.InnerText:=aText;
  506. end;
  507. //==============================================================================
  508. // Initialization code
  509. //==============================================================================
  510. // There are several moments in the game something needs to be put in the
  511. // beginstate.
  512. // InitSprite() Called by InitSprites on Create(), creates images and presets
  513. // sprite variables
  514. // InitSprites() This code first creates and initializes all objects and
  515. // variables sets their beginstate values. Called only once !!
  516. // InitVars() This gets some sprite properties from a TField constant
  517. // and resets counters prior to a new game
  518. // InitCells() This copies the cell-properties from a TField constant
  519. // SetGhostScared() sets images and speeds of the 4 ghosts depending on param.
  520. procedure TPacman.InitSprite(var aSprite: TSprite; aImg: TJSHTMLImageElement; aSpd: Double);
  521. begin
  522. aSprite.spImg := aImg; // get an image instance, owned
  523. aSprite.SpImg.Width:=28; // make the black pixels transparent
  524. aSprite.SpImg.Height:=28; // make the black pixels transparent
  525. aSprite.dir := '-'; // no direction
  526. aSprite.Spd := aSpd; // default speed
  527. aSprite.XY := point(1,1); // Just a non error generating value
  528. aSprite.Sx := 0; // partial X in the middle of a cell
  529. aSprite.Sy := 0; // partial Y in the middle of a cell
  530. aSprite.StartPos:=point(2,2); // Just a non error generating value
  531. end;
  532. procedure TPacman.InitSprites();
  533. var
  534. I : integer;
  535. begin
  536. Sprite[0].SpImg:=Nil;
  537. For I:=1 to 4 do
  538. InitSprite(Sprite[I],ImgGhost[i],GhostSpeedNormal);
  539. Sprite[0].Spd:=PacmanSpeed; // the image is overwritten later
  540. InitSprite(Sprite[5],ImgBonus ,BonusSpeed);
  541. end;
  542. procedure TPacman.InitVars(aField: TField);
  543. // Uses a TField definition to set the global variable PillCount and the initial
  544. // positions of Pacman and the Ghosts, Also (pre)sets timers and pacman's mouth.
  545. var x,y,n:integer;
  546. begin
  547. PillsLeft:=0;
  548. Score :=0;
  549. LivesLeft:=3;
  550. BonusCnt :=0;
  551. GhostCnt :=0;
  552. Pause :=false;
  553. pacMouthopen:=0;
  554. pacMouthopenDir:=10; //startvalues for open mouth of pacman
  555. for x:=0 to GridXSize-1 do
  556. for y:=0 to GridYSize-1 do
  557. begin
  558. case aField[y][x+1] of
  559. '.','o': inc(PillsLeft); // normal and superpills
  560. 'P' : sprite[0].StartPos:=point(x,y); // starting position of PacMan
  561. '1' : sprite[1].StartPos:=point(x,y); // starting position of Ghost #1
  562. '2' : sprite[2].StartPos:=point(x,y); // starting position of Ghost #2
  563. '3' : sprite[3].StartPos:=point(x,y); // starting position of Ghost #3
  564. '4' : sprite[4].StartPos:=point(x,y); // starting position of Ghost #4
  565. end;
  566. end;
  567. for n:=0 to 4 do
  568. sprite[n].XY:=sprite[n].StartPos;
  569. ScareTimer:=0;
  570. BonusTimer:=0;
  571. end;
  572. procedure TPacman.InitCells(aField: TField);
  573. // Uses a TField definition to set properties of all cells in the Cell[] array
  574. const
  575. wsH=['-','\','/']; // set of wall chars used in SW-NE detection
  576. wsV=['|','\','/']; // set of wall chars used in SE-NW detection
  577. var
  578. x,y:integer;
  579. c : char;
  580. begin
  581. for y:=0 to GridYSize-1 do
  582. for x:=0 to GridXSize-1 do
  583. begin
  584. // Set values for WallType from string-field definition
  585. c:=aField[y][x+1];
  586. case c of
  587. '|': Cells[x,y].WallType:=wtNS; // top to bottom
  588. '-': Cells[x,y].WallType:=wtEW; // left to right
  589. '\': if (aField[y][x] in wsH) and (aField[y+1][x+1] in wsV)
  590. then Cells[x,y].WallType:=wtSW // bottom to left
  591. else Cells[x,y].WallType:=wtNE; // top to right
  592. '/': if (aField[y][x+2] in wsH) and (aField[y+1][x+1] in wsV)
  593. then Cells[x,y].WallType:=wtSE // bottom to right
  594. else Cells[x,y].WallType:=wtNW; // top to left
  595. 'x': Cells[x,y].Walltype:=wtNoGo; // no visible wall, but still occupied
  596. else
  597. Cells[x,y].WallType:=wtNone; // no obstacle to pacman and ghosts
  598. end;
  599. // set values for PillType from string-field definition
  600. case c of
  601. '.': Cells[x,y].PillType := ptPill; // this cell contains a Pill
  602. 'o': Cells[x,y].PillType := ptSuperPill; // this cell a SuperPill
  603. else Cells[x,y].PillType := ptNone; // walls and empty space, no points
  604. end;
  605. end;
  606. end;
  607. procedure TPacman.SetGhostScared(aScared: boolean);
  608. Procedure DoImg(Idx: Integer;aImg : TJSHTMLImageElement; aSpeed : Double);
  609. begin
  610. Sprite[Idx].spImg:=aImg;
  611. Sprite[Idx].Spd:=aSpeed;
  612. end;
  613. var
  614. i : Integer;
  615. begin
  616. if aScared then
  617. begin // assign "scared" images and set speed to scared
  618. for I:=1 to 4 do
  619. DoImg(i,ImgGhost[5],GhostSpeedScared);
  620. end
  621. else
  622. begin // assign normal ghost images and set speed to normal
  623. For i:=1 to 4 do
  624. DoImg(I,ImgGhost[i],GhostSpeedNormal);
  625. end;
  626. end;
  627. //==============================================================================
  628. // User input code
  629. //==============================================================================
  630. // This is a very simple piece of code, the only function is FormKeyDown (which
  631. // is an eventproperty of the form) which sets the direction Pacman should go.
  632. // for now only 4 keys are valid, arrow up,down,left,right.
  633. function TPacman.HandleKeyPress(k: TJSKeyBoardEvent): Boolean;
  634. Var
  635. aCode : String;
  636. begin
  637. Result:=True;
  638. if FDying then exit;
  639. aCode:=k.Key;
  640. if aCode='' then
  641. aCode:=K.Code;
  642. case aCode of
  643. // For some reason, it is opposite of what you'd expect...
  644. 'Right', TJSKeyNames.ArrowRight : PacManDir:='W';
  645. 'Up', TJSKeyNames.ArrowUp : PacManDir:='N';
  646. 'Left', TJSKeyNames.ArrowLeft : PacManDir:='E';
  647. 'Down', TJSKeyNames.ArrowDown : PacManDir:='S';
  648. 'P', 'KeyP' : Pause:=not Pause;
  649. end;
  650. k.preventDefault;
  651. end;
  652. function TPacman.DoResetClick(aEvent: TJSMouseEvent): boolean;
  653. begin
  654. Result:=True;
  655. FDying:=True;
  656. StopTimer;
  657. RestartGame();
  658. end;
  659. function TPacman.CheckSound(Event: TEventListenerEvent): boolean;
  660. begin
  661. Result:=True;
  662. AudioDisabled:=Not FCBXSound.checked;
  663. if AudioDisabled then
  664. FAudio.DisableSound
  665. else If not FAudio.Loaded then
  666. begin
  667. FAudio.OnLoaded:=Nil;
  668. FAudio.LoadAudio;
  669. end;
  670. end;
  671. procedure TPacman.DoAudioLoaded(Sender: TObject);
  672. begin
  673. Start;
  674. end;
  675. function TPacman.DoMouseClick(aEvent: TJSMouseEvent): boolean;
  676. Const
  677. SControl = 'control-';
  678. Var
  679. S : String;
  680. begin
  681. Result:=true;
  682. S:=aEvent.currentTarget.ID;
  683. aEvent.preventDefault;
  684. if Copy(S,1,Length(SControl))=SControl then
  685. begin
  686. Delete(S,1,Length(sControl));
  687. Case S of
  688. 'left' : PacManDir:='E';
  689. 'right' : PacManDir:='W';
  690. 'down' : PacManDir:='S';
  691. 'up' : PacManDir:='N';
  692. 'pause' : Pause:=Not Pause;
  693. end;
  694. end;
  695. end;
  696. //==============================================================================
  697. // Business logic, rules of the game.
  698. //==============================================================================
  699. // The ghosts are aware of the position of pacman. Depending on their fear for
  700. // him they try to get to him (Fear=-1) or to get away from him (Fear=1) or anything in
  701. // between.
  702. //
  703. // Every once in a while a bonuscherry starts moving around for a some time.
  704. // When Pacman eats the cherry the score is incremented and the cherry disappears.
  705. // Whenever Pacman eats a small pill the score is incremented and the pill disappears
  706. // Whenever Pacman eats a large pill the score is incremented, the pill diappears,
  707. // and a timer is started that keeps the ghosts to a Fearlavel of 1 al long as the
  708. // timer runs. after that the ghosts wil gradually return to fear=-1;
  709. // When pacman eats a scared ghost the score is incremented and the ghost is sent
  710. // back to his cave...
  711. // When pacman eats a not so scared ghost he dies...
  712. // In this case all ghosts are sent home, and if there are stil lives left the
  713. // game continues with one life less...
  714. // When Pacman runs out of lives the game is ended and a new game is started.
  715. // If all pills are eaten the game is also ended and a new game is started.
  716. //==============================================================================
  717. // Business code: TestAndGet
  718. //==============================================================================
  719. // GetPossibleDir()
  720. // GetGhostDir()
  721. // GetPacmanDir()
  722. // GetRandomCellAndDir()
  723. function TPacman.GetPossibleDir(aXY: TPoint): TStr4;
  724. begin
  725. result:=''; // Start with an empty string
  726. if Cells[aXY.X,aXY.Y-1].WallType=wtNone then result:=result+'N'; // up is possible
  727. if Cells[aXY.X-1,aXY.Y].WallType=wtNone then result:=result+'E'; // left is possible
  728. if Cells[aXY.X,aXY.Y+1].WallType=wtNone then result:=result+'S'; // down is possible
  729. if Cells[aXY.X+1,aXY.Y].WallType=wtNone then result:=result+'W'; // right is possible
  730. end;
  731. function TPacman.GetBestDir(aXY: TPoint): char;
  732. begin
  733. result:='-';
  734. if SolveMaze(aXY,sprite[0].XY) then begin // fill the SearchIndexes cell[x,y].i
  735. if Cells[aXY.X,aXY.Y-1].I<-10 then result:='N'; // up is best
  736. if Cells[aXY.X-1,aXY.Y].I<-10 then result:='E'; // left is best
  737. if Cells[aXY.X,aXY.Y+1].I<-10 then result:='S'; // down is best
  738. if Cells[aXY.X+1,aXY.Y].I<-10 then result:='W'; // right is best
  739. end;
  740. end;
  741. function TPacman.GetGhostDir(aXY: TPoint; aOldDir: char): char;
  742. var BestDir:char; D:Char;s:TStr4;
  743. begin
  744. result:='-';
  745. s:=GetPossibleDir(aXY);
  746. case aOldDir of // get the direction opposite of the current direction
  747. 'W':D:='E'; 'E':D:='W'; 'S':D:='N'; 'N':D:='S'; else D:='-';
  748. end;
  749. if (length(s)>1) then begin // more than one direction: make a choice
  750. BestDir:=GetBestDir(aXY);
  751. if (scaretimer=0) and (BestDir<>'-') then begin//
  752. if random < Huntfactor then s:=BestDir; // hunt depends on factor
  753. end else begin
  754. delete(s,pos(BestDir,s),1); // fleeing does not
  755. end;
  756. end;
  757. // if other than the reverse direction are possible then remove the reverse direction
  758. if (length(s)>1) and (pos(d,s)<>0) then delete(s,pos(d,s),1);
  759. if (length(s)=1) then result:=s[1]; // only one direction possible: Go
  760. if (length(s)>1) then result:=s[1+random(length(s))]; // choose at random
  761. end;
  762. function TPacman.GetPacmanDir(aXY: TPoint; aOldDir: char): char;
  763. var s:TStr4;
  764. begin
  765. s:=GetPossibleDir(aXY);
  766. if pos(PacmanDir,s)>0 then s:=pacmandir else
  767. if pos(aOldDir,s)>0 then s:=aOldDir else s:='-';
  768. result:=s[1];
  769. end;
  770. procedure TPacman.GetRandomCellAndDir(var aXY: TPoint; var aDir: char);
  771. begin
  772. repeat
  773. aXY:=point(1+random(GridXSize-3),random(GridYSize-3));
  774. until (Cells[aXY.x,aXY.y].WallType=wtnone);
  775. aDir:=GetGhostDir(aXY,'-');
  776. end;
  777. procedure TPacman.StopTimer;
  778. begin
  779. Window.clearInterval(SpriteTimer);
  780. end;
  781. procedure TPacman.MarkCellsDirty;
  782. Var
  783. n,maxn,x,y,i,j : Integer;
  784. begin
  785. maxn:=4;
  786. if BonusTimer>0 then
  787. inc(maxn);
  788. for n:=0 to 4 do
  789. begin
  790. X:=Sprite[n].XY.x;
  791. Y:=Sprite[n].XY.Y;
  792. for i:=-1 to 1 do
  793. for j:=-1 to 1 do
  794. Cells[X+i,Y+j].Dirty:=True;
  795. end;
  796. end;
  797. procedure TPacman.DoSpriteTimer;
  798. var n:integer;
  799. begin
  800. if Pause=false then
  801. begin
  802. MarkCellsDirty;
  803. DrawCells(True);
  804. for n:=0 to 4 do
  805. MoveSprite(n); // for 'Pacman' and each 'Ghost'
  806. if DoBonusTimer() then
  807. MoveSprite(5); // update bonustimer plus cherry
  808. DoScareTimer(); // update the timer that controls scaring of the ghosts
  809. DrawPacman(); // the images have moved, update the pacmanface
  810. end;
  811. end;
  812. //==============================================================================
  813. // Business code: Actions
  814. //==============================================================================
  815. // OnRestartMessage()
  816. // EatPill()
  817. // EatSuperPill()
  818. // EatBonus()
  819. // EatGhost()
  820. // ClearCell()
  821. // MoveSprite()
  822. // DoBonusTimer()
  823. // DoScareTimer()
  824. // OnSpriteTimer()
  825. function TPacman.DoRestartClick(aEvent: TJSMouseEvent): boolean;
  826. begin
  827. RestartGame(); // start game after VCL is ready drawing the screen
  828. end;
  829. procedure TPacman.EatPill(aXY: TPoint);
  830. begin
  831. inc(Score, 1);
  832. ClearCell(aXY);
  833. dec(PillsLeft);
  834. UpdateScore();
  835. playsound(aEatPill);
  836. if PillsLeft=0 then NextLevel();
  837. end;
  838. procedure TPacman.EatSuperPill(aXY: TPoint);
  839. begin
  840. ClearCell(aXY);
  841. ScareTimer:=ScareTimeOut; // Make 'm scared for a while...
  842. inc(Score,10);
  843. playsound(aEatPill);
  844. UpdateScore();
  845. dec(PillsLeft); if PillsLeft=0 then NextLevel();
  846. end;
  847. procedure TPacman.EatBonus();
  848. begin
  849. BonusTimer:=0; // remove cherry
  850. inc(Score,50);
  851. inc(BonusCnt);
  852. UpdateScore(); // write scores to screen
  853. end;
  854. procedure TPacman.EatGhost(var aGhost: TSprite);
  855. begin
  856. playsound(aEatGhost);
  857. aGhost.XY:=aGhost.StartPos; // send ghost home
  858. inc(Score,20);
  859. inc(GhostCnt);
  860. UpdateScore(); // write scores to screen
  861. end;
  862. procedure TPacman.ClearCell(aXY: TPoint);
  863. var sx,sy:integer;
  864. begin
  865. cells[aXY.X,aXY.Y].PillType:=ptNone; // clear cell in Cell[] array
  866. Fcanvas.FillStyle:=clBlack; // also clear this part of the canvas
  867. sx:=aXY.x*CellSize;
  868. sy:=aXY.y*CellSize;
  869. FCanvas.fillrect(sx,sy,cellsize,cellsize);
  870. end;
  871. procedure TPacman.MoveSprite(aSpriteInx: integer);
  872. var oXY:TPoint;
  873. begin
  874. with Sprite[aSpriteInx] do begin
  875. // change position depending on direction
  876. oXY:=XY;
  877. case Dir of
  878. 'N': begin Sy:=Sy-Spd; if Sy<=-1 then begin dec(XY.y); Sy:=Sy+1; end; end;
  879. 'E': begin Sx:=Sx-Spd; if Sx<=-1 then begin dec(XY.x); Sx:=Sx+1; end; end;
  880. 'S': begin Sy:=Sy+Spd; if Sy>= 1 then begin inc(XY.y); Sy:=Sy-1; end; end;
  881. 'W': begin Sx:=Sx+Spd; if Sx>= 1 then begin inc(XY.x); Sx:=Sx-1; end; end;
  882. else
  883. begin
  884. oXY:=point(0,0);
  885. Sx:=0;Sy:=0;
  886. end;
  887. end;
  888. //if cell changed then choose new direction depending on wall limitations
  889. if (XY.x<>oXY.x) or (XY.y<>oXY.y) then
  890. begin
  891. if aSpriteInx=0 then
  892. dir:=GetPacmanDir(XY,dir)
  893. else
  894. dir:=GetGhostDir (XY,dir);
  895. if dir in ['E','W'] then //correct partial displacements
  896. sy:=0
  897. else
  898. sx:=0;
  899. if aSpriteInx=0 then
  900. CollisionDetect(XY); //only for The Man himself...
  901. end;
  902. // if position goes offgrid then reenter on the other side of the screen
  903. if XY.x>GridXSize-3 then XY.x:=2; if XY.x<2 then XY.x:=GridXSize-3;
  904. if XY.y>GridYSize-3 then XY.y:=2; if XY.y<2 then XY.y:=GridYSize-3;
  905. // set sprite image position according to new Cx:Sx,Cy,Sy
  906. // Pacman is drawn separately
  907. if aSpriteInx<>0 then
  908. FCanvas.drawImage(spimg,(XY.x+Sx+0.5)*CellSize-SpImg.Width/2,
  909. (XY.y+Sy+0.5)*CellSize-SpImg.Height/2);
  910. // SpImg.Left := round();
  911. // SPImg.Top := round((XY.y+Sy+0.5)*CellSize-SpImg.picture.Height/2);
  912. end;
  913. end;
  914. function TPacman.DoBonusTimer(): boolean;
  915. Var
  916. S : String;
  917. w : Integer;
  918. begin
  919. if BonusTimer>=0 then begin // bonustimer is positive: cherry is onscreen
  920. dec(BonusTimer);
  921. if BonusTimer<=0 then begin // if decrement makes it negative then
  922. // sprite[5].SpImg.visible:=false; // remove cherry from screen, and
  923. BonusTimer:=-BonusTimeOut1-random(BonusTimeOut1); // set a negative timeout
  924. end;
  925. end else begin // if bonus timer is negative then cherry is not onscreen
  926. inc(BonusTimer);
  927. if BonusTimer>=0 then begin // when increment makes it positive then
  928. // sprite[5].SpImg.visible:=true; // make cherry visible,
  929. // sprite[5].Sx:=0; sprite[5].Sy:=0;// set partial position to zero, and
  930. GetRandomCellAndDir(Sprite[5].XY,Sprite[5].Dir);// choose a random position
  931. BonusTimer:=+BonusTimeOut2+random(BonusTimeOut2); // Set a positive timeout
  932. end;
  933. end;
  934. // update a custom made progressbar on the screen
  935. S:='background-color: ';
  936. W:=bonustimer*Round(pnBonusBarOuter.clientWidth) div (2*BonusTimeOut2);
  937. if BonusTimer>0 then
  938. S:=S+clLime+'; width: '+IntToStr(W)+'px;'
  939. else
  940. S:=S+clRed+'; width: 0px;';
  941. pnbonusbarInner['style']:=S;
  942. result:=BonusTimer>0;
  943. end;
  944. procedure TPacman.DoScareTimer();
  945. Var
  946. S: String;
  947. w : integer;
  948. begin
  949. // just after superpill is eaten the caretimer is set to ScareTimeOut
  950. if scaretimer>=ScareTimeOut then SetGhostScared(true); //frighten them !!
  951. if ScareTimer>0 then begin
  952. dec(ScareTimer);
  953. // if scaretimer becomes zero then scare time is over: return to normal
  954. if scaretimer=0 then SetGhostScared(false); // fun is over...
  955. // update a custom made progressbar on the screen
  956. if ScareTimer>ScareTimeOut div 5 then
  957. S:='background-color: '+clLime
  958. else
  959. S:='background-color: '+clRed; // make bar red for last 20% of the time
  960. W:=ScareTimer*pnScareBarOuter.Clientwidth div ScareTimeOut;
  961. S:=S+'; width: '+IntToStr(w)+'px;';
  962. pnScareBarInner.Attrs['style']:=S;
  963. end;
  964. end;
  965. procedure TPacman.DrawScene;
  966. Var
  967. I : Integer;
  968. begin
  969. DrawCells();
  970. for I:=0 to 4 do
  971. MoveSprite(I); // For 'Pacman' and each 'Ghost'
  972. DrawPacMan;
  973. end;
  974. procedure TPacman.SetupPacman;
  975. Function GetElement(aName : String) : TJSHTMLElement;
  976. begin
  977. Result:=TJSHTMLElement(Document.getElementById(aName));
  978. end;
  979. Var
  980. I : integer;
  981. El : TJSElement;
  982. begin
  983. if FCanvasID='' then
  984. FCanvasID:='my-canvas';
  985. if FResetID='' then
  986. FResetID:='btn-reset';
  987. FCanvasEl:=TJSHTMLCanvasElement(Document.getElementById(FCanvasID));
  988. FCanvas:=TJSCanvasRenderingContext2D(FCanvasEl.getContext('2d'));
  989. FBtnReset:=TJSHTMLButtonElement(Document.getElementById(FResetID));
  990. FCBXSound:=TJSHTMLInputElement(GetElement('cbx-sound'));
  991. FCBXSound.onchange:=@CheckSound;
  992. if Assigned(FBtnReset) then
  993. FBtnReset.OnClick:=@DoResetClick;
  994. FCanvasEl.width := Round(FCanvasEl.OffsetWidth);
  995. FCanvasEl.height := Round(FCanvasEl.OffsetHeight);
  996. for I:=1 to 4 do
  997. ImgGhost[i]:=TJSHTMLImageElement(GetElement('ghost'+IntToStr(i))) ;
  998. ImgGhost[5]:=TJSHTMLImageElement(GetElement('ghost-scared'));
  999. ImgBonus:=TJSHTMLImageElement(GetElement('cherry'));
  1000. for I:=1 to ControlCount do
  1001. begin
  1002. El:=GetElement('control-'+ControlNames[i]);
  1003. if Assigned(El) then
  1004. TJSHTMLElement(El).onClick:=@DoMouseClick;
  1005. end;
  1006. pnBonusBarOuter:=GetElement('bonus-outer');
  1007. pnBonusBarInner:= GetElement('bonus-inner');
  1008. pnScareBarOuter:=GetElement('scare-outer');
  1009. pnScareBarInner:=GetElement('scare-inner');
  1010. lbScore:=GetElement('score');
  1011. lbStatus:=GetElement('status');
  1012. lbHiscore:=GetElement('highscore');
  1013. lbLives:=GetElement('lives');
  1014. lbBonusCnt:=GetElement('bonus');
  1015. lbGhostCnt:=GetElement('ghosts');
  1016. // Sprites need the images, so this can only be done in this part
  1017. InitSprites();
  1018. document.onkeydown:=@HandleKeyPress;
  1019. if not AudioDisabled then
  1020. InitAudio()
  1021. end;
  1022. procedure TPacman.InitAudio;
  1023. begin
  1024. Faudio.LoadAudio;
  1025. end;
  1026. procedure TPacman.StartTimer;
  1027. begin
  1028. FDying:=False;
  1029. UpdateStatus('Playing');
  1030. SpriteTimer:=window.setInterval(@DoSpriteTimer,TimerInterval);
  1031. end;
  1032. procedure TPacman.Start;
  1033. begin
  1034. RestartGame;
  1035. end;
  1036. //==============================================================================
  1037. // Business code: Decisions
  1038. //==============================================================================
  1039. // CollisionDetect()
  1040. // RestartGame()
  1041. // RestartLevel()
  1042. // PacmanDies()
  1043. // NextLevel()
  1044. // GameOver()
  1045. procedure TPacman.CollisionDetect(var aXY: TPoint);
  1046. var n,ix,dX,dY:integer;
  1047. begin
  1048. case cells[aXY.X,aXY.Y].PillType of
  1049. ptPill :EatPill(aXY);
  1050. ptSuperPill :EatSuperPill(aXY);
  1051. end;
  1052. ix:=0; for n:=1 to 5 do begin
  1053. dX:=sprite[n].XY.x-aXY.x;
  1054. dY:=sprite[n].XY.y-aXY.y;
  1055. if (abs(dX)<=1) and (abs(dY)<=1) then ix:=n;
  1056. end;
  1057. if (ix=5) and (BonusTimer>0) then EatBonus();
  1058. if ix in [1..4] then begin
  1059. if ScareTimer>0 then EatGhost(sprite[ix]) else PacmanDies();
  1060. end;
  1061. end;
  1062. procedure TPacman.RestartGame();
  1063. begin
  1064. InitVars(Level1Field);
  1065. InitCells(Level1Field);
  1066. RestartLevel();
  1067. UpdateStatus('Playing');
  1068. end;
  1069. procedure TPacman.RestartLevel();
  1070. var n:integer;
  1071. begin
  1072. for n:=0 to 4 do
  1073. Sprite[n].XY:=Sprite[n].StartPos;
  1074. UpdateScore();
  1075. SetGhostScared(false);
  1076. DrawScene;
  1077. PacmanDir:='-';
  1078. DrawPacman(); // the images have moved, set the pacmanface
  1079. PlaySound(aStart);
  1080. ShowText('GET READY !!!',@StartTimer);
  1081. PacmanDir:='-';
  1082. end;
  1083. procedure TPacman.CheckGameOver;
  1084. begin
  1085. if LivesLeft<=0 then
  1086. GameOver()
  1087. else
  1088. ReStartLevel();
  1089. end;
  1090. procedure TPacman.PacmanDies();
  1091. begin
  1092. //exit;
  1093. if FDying then
  1094. exit;
  1095. FDying:=True;
  1096. StopTimer;
  1097. playsound(aDie);
  1098. dec(LivesLeft);
  1099. UpdateScore();
  1100. PacmanDir:='-';
  1101. UpdateStatus('You died');
  1102. ShowText('YOU DIE !!!',@CheckGameOver);
  1103. end;
  1104. procedure TPacman.NextLevel();
  1105. begin
  1106. StopTimer;
  1107. ShowText('YOU WIN !!!',@RestartGame);
  1108. UpdateStatus('You win');
  1109. end;
  1110. procedure TPacman.GameOver();
  1111. begin
  1112. ShowText('YOU LOST !!!',@RestartGame);
  1113. UpdateStatus('You lost');
  1114. end;
  1115. procedure TPacman.PlaySound(aAudio: TAudio);
  1116. begin
  1117. if (not AudioDisabled) and (FAudio.Loaded) then
  1118. FAudio.play(aAudio);
  1119. end;
  1120. //==============================================================================
  1121. // Maze solving
  1122. //==============================================================================
  1123. // Solving a maze is implemented here as a 3 step process.
  1124. // Step 1:
  1125. // All accessible maze cells get an searchindex of 0, all blocked cells
  1126. // (f.i. Walls) get an index of -1.
  1127. // Step 2:
  1128. // Two arrays are used to keep track of a set of cells that are tested
  1129. // This step begins with adding the first point to the primary array.
  1130. // This now contains exactly one cell. Then a loop starts: for each cell in
  1131. // the primary array the 4 surrounding cells are tested (left,right,up down)
  1132. // If the index of such a cell is 0 then the cell is free and it is added to
  1133. // a secondary array of cell coordinates. The searchindex of the cell is set
  1134. // to a value that is one higher than the searchindex of original cell.
  1135. // If the neighbour cells of all cells in the primary array are tested then
  1136. // the secondary array is copied to the primary array and the secondary array
  1137. // is cleared.
  1138. // There are 2 reasons to end this loop:
  1139. // 1: The cell that was searched for is found
  1140. // 2: There are no more cells with a searchindex of 0, secondary array is empty
  1141. // When this is all done the cells have a search index that increments as the
  1142. // cell is further away from the originating point. Not all cells are tested.
  1143. // When the loop finds the target in say 10 steps the testing stops, so no cell
  1144. // will get an index higher than 10.
  1145. // Imagine an octopus with growing tentacles that stops when the prey is found
  1146. // Step 3:
  1147. // Now that the target is found we have to find "the tentacle that leads back
  1148. // to the octopus", the shortest way back to the originating point.
  1149. // This is done by starting at the endpoint, and looking in the surrounding
  1150. // cells for a valid searchindex that is smaller than the cells own searchindex.
  1151. // Move the cellpointer to the adjacing cell with a smaller index and eventually
  1152. // you get back to the source.
  1153. // Imagine a river valley in which a lot of streams go down to the middle. Just
  1154. // follow gravity and you will end up in the center.
  1155. // On the way back the cells are marked, and that way you will have a set of
  1156. // cells that give you the shortest route form A to B.
  1157. //
  1158. // For debugging the searchindexes are set to 10 and higher for the tested cells
  1159. // on routes without result, and -10 and lower for the tested cells that are part
  1160. // of the shortest route. SearchIndex = 10 or -10 is the startingpoint.
  1161. // Blocked cells are -1, Untested cells are 0.
  1162. // Cells with an index of -10 or less are the solution.
  1163. //
  1164. // For this game we are only interested in the first direction decision of a
  1165. // Ghost, so after step 1 to 3 we only look which cell in the adjacent cells of
  1166. // a Ghost is in the path, and send the Ghost that way (or opposite when it is
  1167. // scared).
  1168. function TPacman.SolveMaze(P1, P2: TPoint): boolean;
  1169. begin // 3 step maze solving algorithm
  1170. result := SolveMazeStep1(P1,P2); // step1
  1171. if result then result := SolveMazeStep2(P1,P2); // step2
  1172. if result then result := SolveMazeStep3(P1,P2); // step3
  1173. end;
  1174. function TPacman.SolveMazeStep1(P1, P2: TPoint): boolean;
  1175. var x,y:integer;
  1176. begin
  1177. for x:=0 to GridXSize-1 do for y:=0 to GridYSize-1 do begin
  1178. if Cells[x,y].WallType=wtNone
  1179. then Cells[x,y].I:=0 // these cells can be part of a route
  1180. else Cells[x,y].I:=-1; // these cells can not...
  1181. end;
  1182. // no search is usefull if P1 or P1 is not a valid cell...
  1183. result:= (cells[P1.x ,P1.y].I=0) and (cells[P2.x,P2.y].I=0)
  1184. end;
  1185. // In the procedure below a fixed size is used for SArr1 and SArr2.
  1186. // Of course it is much better to use a dynamic array that is never too small
  1187. // I tested the maximum number of alternative routes in this maze is 17, and the
  1188. // maximum number of searchloops is 54.
  1189. // To keep code as simple as possible the arraysizes are set to 64 (17 needed).
  1190. function TPacman.SolveMazeStep2(P1, P2: TPoint): boolean;
  1191. var SArr1,SArr2:array[0..63] of tpoint;
  1192. SArr1Cnt,SArr2Cnt:integer;
  1193. SI:integer; n:integer;
  1194. procedure AddLS2(x,y:integer);
  1195. begin
  1196. if (x<0) or (x>=GridXSize) then exit; // index offgrid: do nothing
  1197. if (y<0) or (y>=GridYSize) then exit; // index offgrid: do nothing
  1198. if cells[x,y].i<>0 then exit; // cell is blocked: do nothing
  1199. cells[x,y].i:=SI; // cell is usable: give index
  1200. SArr2[SArr2Cnt]:=point(x,y); inc(SArr2Cnt); // add cell to SArr2 for next run
  1201. if (x=P2.x) and (y=P2.y) then Result:=true; // if endpoint is found then stop
  1202. end;
  1203. begin
  1204. SI:=10; Result:=false; // start at 10 to have some special numbers to spare
  1205. cells[p1.x,p1.y].i:=SI; // for debugging, set the searchindex of first cell
  1206. SArr1Cnt:=1; SArr1[0]:=P1;// prepare primary array with one (the first) cell
  1207. repeat // now start searching for PacMan !!
  1208. inc(SI); // increment search index
  1209. SArr2Cnt:=0; // clear secondary array
  1210. for n:=0 to SArr1Cnt-1 do begin // for all points in primary array do
  1211. AddLS2(SArr1[n].x+1,SArr1[n].y );// Test and maybe add cell to the right
  1212. AddLS2(SArr1[n].x ,SArr1[n].y+1);// Test and maybe add cell below
  1213. AddLS2(SArr1[n].x-1,SArr1[n].y );// Test and maybe add cell to the left
  1214. AddLS2(SArr1[n].x ,SArr1[n].y-1);// Test and maybe add cell above
  1215. end;
  1216. //now copy alle new searchpoints in SArr2 to sArr1, and set the number of points
  1217. for n:=0 to SArr2Cnt-1 do SArr1[n]:=SArr2[n]; SArr1Cnt:=SArr2Cnt;
  1218. until Result or (SArr2Cnt=0); // repeat until pacman is found or all cells tested
  1219. end;
  1220. function TPacman.SolveMazeStep3(P1, P2: TPoint): boolean;
  1221. var Rdy:boolean; dP:TPoint; I:integer;
  1222. procedure Check(x,y:integer);
  1223. var It:integer;
  1224. begin
  1225. if (x<0) or (x>=GridXSize) then exit; // index offgrid: do nothing
  1226. if (y<0) or (y>=GridYSize) then exit; // index offgrid: do nothing
  1227. It:=cells[x,y].I; // make a long name short...
  1228. if (It>0) and (It<I) then begin // if index is smaller than the last but >0
  1229. I:=It; // then make I the smaller index
  1230. dP:=point(x,y); // and make the next cell the tested cell
  1231. end;
  1232. end;
  1233. begin
  1234. repeat
  1235. I:=cells[P2.x,P2.y].i; // inx of current cell (P)
  1236. dP:=P2; // make next p equal to current cell
  1237. Check(P2.x+1,P2.y ); // test right
  1238. Check(P2.x-1,P2.y ); // test left
  1239. Check(P2.x ,P2.y+1); // test bottom
  1240. Check(P2.x ,P2.y-1); // test top
  1241. Rdy:=(dP.x=P2.x)and(dP.y=P2.y); // if dP still equal to P than search is over
  1242. cells[p2.x,p2.y].i := -cells[p2.x,p2.y].i;// mark this cell as returnpath
  1243. P2:=dP; // move current cell to the next one
  1244. until Rdy;
  1245. result:=(P2.x=P1.x)and(P2.y=P1.y);// what can possibly go wrong???
  1246. end;
  1247. procedure TPacmanAudio.AudioLoaded;
  1248. Var
  1249. AllLoaded : Boolean;
  1250. A : TAudio;
  1251. begin
  1252. allLoaded:=True;
  1253. For a in TAudio do
  1254. AllLoaded:=AllLoaded and FilesOK[a];
  1255. FLoaded:=allLoaded;
  1256. if Assigned(FonLoaded) then
  1257. FOnLoaded(Self);
  1258. end;
  1259. function TPacmanAudio.CheckEnd(Event: TEventListenerEvent): boolean;
  1260. var
  1261. a : TAudio;
  1262. begin
  1263. For a in TAudio do
  1264. if (Files[a]=Event.target) then
  1265. Playing[a]:=False;
  1266. end;
  1267. function TPacmanAudio.CheckplayOK (Event: TEventListenerEvent): boolean;
  1268. var
  1269. a : TAudio;
  1270. begin
  1271. For a in TAudio do
  1272. if (Files[a]=Event.target) then
  1273. begin
  1274. Files[a].oncanplaythrough:=nil;
  1275. FilesOK[a]:=True;
  1276. AudioLoaded;
  1277. end;
  1278. end;
  1279. procedure TPacmanAudio.LoadAudio;
  1280. var
  1281. F : TJSHTMLAudioElement;
  1282. A : TAudio;
  1283. begin
  1284. for a in TAudio do
  1285. begin
  1286. F:=TJSHTMLAudioElement(Document.GetElementByID('audio-'+audionames[a]));
  1287. Files[a]:=F;
  1288. FilesOK[a]:=F.readyState>=3;
  1289. if not FilesOK[a] then
  1290. F.oncanplaythrough:=@CheckPlayOK;
  1291. end;
  1292. AudioLoaded;
  1293. end;
  1294. procedure TPacmanAudio.DisableSound;
  1295. var
  1296. a : TAudio;
  1297. begin
  1298. For a in TAudio do
  1299. if Playing[a] then
  1300. begin
  1301. files[a].pause();
  1302. files[a].currentTime := 0;
  1303. end;
  1304. end;
  1305. procedure TPacmanAudio.play(aAudio: Taudio);
  1306. begin
  1307. Writeln('Attempting to play:',AudioNames[aAudio]);
  1308. if FilesOK[aAudio] then
  1309. begin
  1310. Playing[aAudio]:=true;
  1311. Files[aAudio].play;
  1312. Files[aAudio].onended:=@CheckEnd;
  1313. end;
  1314. end;
  1315. procedure TPacmanAudio.Pause;
  1316. var
  1317. a : TAudio;
  1318. begin
  1319. For a in TAudio do
  1320. if Playing[a] and not Files[a].paused then
  1321. files[a].pause();
  1322. end;
  1323. procedure TPacmanAudio.Resume;
  1324. var
  1325. a : TAudio;
  1326. begin
  1327. For a in TAudio do
  1328. if Playing[a] and Files[a].paused then
  1329. files[a].play();
  1330. end;
  1331. end.