view src/Serpentron.st @ 19:733ad1ed5548

Handle ; key in WebKit and Blink based browsers.
author Mikhail Kryshen <mikhail@kryshen.net>
date Fri, 13 Mar 2020 14:54:23 +0300
parents 392c2a5ebab4
children c8162ac60edc
line source
1 Smalltalk createPackage: 'Serpentron'!
2 (Smalltalk packageAt: 'Serpentron' ifAbsent: [ self error: 'Package not created: Serpentron' ]) imports: {'silk/Silk'}!
3 Object subclass: #Serpentron
4 slots: {#field. #skin. #players. #playerColors. #controllerPrototypes. #score. #pointsToWin. #timeoutId. #startScreenVisible}
5 package: 'Serpentron'!
6 !Serpentron commentStamp!
7 The game UI.!
9 !Serpentron methodsFor: 'initialization'!
11 initialize
12 super initialize.
13 playerColors := #('#e41a1c' '#377eb8' '#4daf4a' '#ff7f00' '#984ea3' '#b3ad78').
14 startScreenVisible := true.
15 skin := TronSkin new.
16 field := TronField new
17 skin: skin;
18 onEndGame: [ self handleEndGame ].
19 self initializePlayers; initializeControllers.
20 (Silk fromElement: document)
21 on: #keydown bind: [ :event | self keyDown: event ].
22 ! !
24 !Serpentron methodsFor: 'private'!
26 handleEndGame
27 | color |
28 field isGameFinished
29 "Interrupted game, no winner."
30 ifFalse: [ ^ self startScreenVisible: true ].
31 color := field winningColorIfNone: [
32 ^ self status: 'No winner in this round.'; nextRound ].
33 self updateScore.
34 (score anySatisfy: [ :each | each >= pointsToWin ])
35 ifFalse: [
36 self
37 status: (self winnerDOM: color) << ' won the round.';
38 nextRound.
39 ^ self ].
40 self status: self scoreDOM.
41 timeoutId := window
42 setTimeout: [ self showGameWinner: color ]
43 after: 1000.
44 !
46 hideMessage
47 (Silk at: '#message') element className: 'hidden'.
48 !
50 initializeControllers
51 controllerPrototypes := {
52 TronKeyboardController new
53 keyMap: #{
54 38 -> (0 @ -1).
55 39 -> (1 @ 0).
56 40 -> (0 @ 1).
57 37 -> (-1 @ 0)}
58 name: 'arrows'.
59 TronKeyboardController new
60 keyMap: #{
61 87 -> (0 @ -1).
62 68 -> (1 @ 0).
63 83 -> (0 @ 1).
64 65 -> (-1 @ 0)}
65 name: 'WASD'.
66 TronKeyboardController new
67 keyMap: #{
68 89 -> (0 @ -1).
69 74 -> (1 @ 0).
70 72 -> (0 @ 1).
71 71 -> (-1 @ 0)}
72 name: 'YGHJ'.
73 TronKeyboardController new
74 keyMap: #{
75 80 -> (0 @ -1).
76 222 -> (1 @ 0).
77 59 -> (0 @ 1). "; in Gecko (Firefox)"
78 186 -> (0 @ 1). "; in WebKit and Blink"
79 76 -> (-1 @ 0)}
80 name: 'PL;'''.
81 TronComputerController1 new.
82 "TronRandomController new"
83 }.
84 (players at: 1) controller: (controllerPrototypes at: 5) copy.
85 (players at: 2) controller: (controllerPrototypes at: 1) copy.
86 (players at: 3) controller: (controllerPrototypes at: 5) copy.
87 (players at: 4) controller: (controllerPrototypes at: 5) copy.
88 !
90 initializePlayers
91 players := {
92 TronPlayer new
93 name: 'Player 1';
94 color: (playerColors at: 1).
95 TronPlayer new
96 name: 'Player 2';
97 color: (playerColors at: 2).
98 TronPlayer new
99 name: 'Player 3';
100 color: (playerColors at: 3);
101 enabled: false.
102 TronPlayer new
103 name: 'Player 4';
104 color: (playerColors at: 4);
105 enabled: false
106 }.
107 !
109 keyDown: event
110 startScreenVisible ifTrue: [ ^ self ].
111 "Handle Esc key."
112 (event keyCode = 27)
113 ifTrue: [
114 event preventDefault.
115 field stopTimer.
116 window clearTimeout: timeoutId.
117 self hideMessage; startScreenVisible: true.
118 ^ self ].
119 field keyDown: event.
120 !
122 nextRound
123 timeoutId := window
124 setTimeout: [
125 self status: self scoreDOM.
126 field start: field players ]
127 after: 2000.
128 !
130 randomizePlayerColors
131 | enabledPlayers |
132 enabledPlayers := players select: #isEnabled.
133 enabledPlayers do: [ :each |
134 | color |
135 [ color := playerColors atRandom.
136 enabledPlayers allSatisfy: [ :p |
137 p = each | (p color ~= color) ]
138 ] whileFalse.
139 each color: color.
140 ].
141 !
143 randomizeTeams
144 | colorA colorB teamA |
145 players do: [ :each | each enabled: true ].
146 colorA := playerColors atRandom.
147 [ colorB := playerColors atRandom.
148 colorB = colorA ] whileTrue.
149 teamA := players copy.
150 players size // 2
151 timesRepeat: [ (teamA remove: teamA atRandom) color: colorB ].
152 teamA do: [ :each | each color: colorA ].
153 !
155 renderColorSelectorFor: aPlayer on: aSilk
156 | container buttons onChange |
157 container := aSilk SPAN: {#class -> 'color-selector'}.
158 buttons := Dictionary from:
159 (playerColors collect: [ :each |
160 | button |
161 button := container A.
162 button on: #click bind: [
163 aPlayer enabled: true; color: each.
164 self validatePlayerColor: aPlayer ].
165 button element style background: each.
166 each -> button ]).
167 buttons at: #disabled put: (container A
168 SPAN: '✗';
169 on: #click bind: [
170 aPlayer enabled: false.
171 self validatePlayerColor: aPlayer ]).
172 onChange := [
173 buttons keysAndValuesDo: [ :k :v |
174 (aPlayer isEnabled not & k = #disabled) |
175 (aPlayer isEnabled & k = aPlayer color)
176 ifTrue: [ v element className: 'selected-color-button' ]
177 ifFalse: [ v element className: 'color-button' ] ] ].
178 aPlayer
179 onEnabledChange: onChange;
180 onColorChange: onChange.
181 onChange value.
182 !
184 renderControllerSelectorFor: aPlayer on: aSilk
185 | select options onChange |
186 select := aSilk SELECT.
187 options := Dictionary new.
188 controllerPrototypes do: [ :each |
189 options at: each name put: (select OPTION: each name) element ].
190 select
191 on: #change
192 bind: [
193 aPlayer controller:
194 (controllerPrototypes
195 detect: [ :each | each name = select element value ])
196 copy ].
197 onChange := [
198 | value |
199 value := aPlayer controller name.
200 (options at: value) selected: 'selected'.
201 aPlayer controller class = TronKeyboardController
202 ifTrue: [
203 players do: [ :each |
204 (each ~= aPlayer and: [ each controller name = value ])
205 ifTrue: [ each controller: TronComputerController1 new ]]]].
206 aPlayer onControllerChange: onChange.
207 onChange value.
208 !
210 renderPlayer: aPlayer on: aSilk
211 | container |
212 container := aSilk DIV: { #class -> 'player' }.
213 self renderColorSelectorFor: aPlayer on: container.
214 (container INPUT
215 on: #change bind: [ :event |
216 aPlayer name: event target value ])
217 element value: aPlayer name.
218 self renderControllerSelectorFor: aPlayer on: container.
219 !
221 renderStartScreenOn: aSilk
222 | startScreen |
223 startScreen := aSilk DIV: {#id -> 'start-screen'}.
225 players do: [ :each | self renderPlayer: each on: startScreen ].
227 (startScreen BUTTON: 'Random colors')
228 on: #click bind: [ self randomizePlayerColors ].
229 (startScreen BUTTON: 'Random teams')
230 on: #click bind: [ self randomizeTeams ].
232 startScreen BR.
234 (startScreen BUTTON: 'Start')
235 on: #click bind: [ :event | self startGame; status: self scoreDOM ].
236 !
238 scoreDOM
239 | message |
240 message := Silk SPAN: 'Score: '.
241 score associations
242 do: [ :each |
243 (message SPAN: {#class -> 'player-color'. (each value * 10) rounded / 10})
244 element style color: each key ]
245 separatedBy: [ message << ':' ].
246 message << ('/', pointsToWin).
247 ^ message.
248 !
250 showGameWinner: color
251 self showMessage: (self winnerDOM: color) << ' won the game!!'.
252 timeoutId := window
253 setTimeout: [
254 self
255 startScreenVisible: true;
256 hideMessage;
257 status: self scoreDOM << '. ' << (self winnerDOM: color) << ' won the game!!' ]
258 after: 1000.
259 !
261 showMessage: anObject
262 ((Silk at: '#message') resetContents << anObject)
263 element className: 'visible'.
264 !
266 startGame
267 | enabledPlayers |
268 self startScreenVisible: false.
269 field start: (players select: #isEnabled).
270 pointsToWin := (30 / (field players size + 1)) ceiling.
271 score := field colors collect: [ :each | 0 ].
272 !
274 startScreenVisible: aBoolean
275 startScreenVisible := aBoolean.
276 (Silk at: '#start-screen') element
277 className: (aBoolean ifTrue: [ 'visible' ] ifFalse: [ 'hidden' ]).
278 !
280 status: anObject
281 '#status' asSilk resetContents << anObject
282 !
284 updateScore
285 | color teamSize alive points defeated |
286 color := field winningColorIfNone: [ ^ self ].
287 teamSize := field colors at: color.
288 defeated := field players size - teamSize.
289 alive := field liveColors at: color.
290 points := defeated * alive / teamSize * (teamSize + 1) / 2.
291 points := points / (field players size - 1).
292 score at: color put: (score at: color) + points.
293 !
295 updateSize
296 | w h fw fh ratio |
297 w := window innerWidth.
298 h := window innerHeight - ('#title' asSilk element offsetHeight).
299 ratio := field fieldSize x / field fieldSize y.
300 (w / ratio <= h)
301 ifTrue: [ fw := w. fh := w / ratio ]
302 ifFalse: [ fh := h. fw := h * ratio ].
303 '#serpentron' asSilk element style
304 width: fw rounded asString, 'px';
305 marginLeft: ((w - fw) / 2) rounded asString, 'px';
306 marginTop: ((h - fh) / 2) rounded asString, 'px'.
307 '#field' asSilk element style
308 width: fw rounded asString, 'px';
309 height: fh rounded asString, 'px'.
310 !
312 validatePlayerColor: aPlayer
313 | enabledPlayers otherPlayers freeColor |
314 enabledPlayers := players select: #isEnabled.
315 ((enabledPlayers collect: #color) asSet size > 1)
316 ifTrue: [ ^ self ].
317 freeColor := playerColors detect: [ :each | each ~= enabledPlayers anyOne color ].
318 otherPlayers := players select: [ :each | each ~~ aPlayer ].
319 (otherPlayers
320 detect: [ :each | each isEnabled not ]
321 ifNone: [ otherPlayers first ])
322 enabled: true;
323 color: freeColor.
324 !
326 winnerDOM: color
327 | names team message |
328 team := field players select: [ :each | each color = color ].
329 names := ''.
330 (team collect: #name)
331 do: [ :each | names := names, each]
332 separatedBy: [ names := names, ' and ' ].
333 message := Silk SPAN.
334 (message SPAN: {#class -> 'player-color'. names})
335 element style color: color.
336 ^ message
337 ! !
339 !Serpentron methodsFor: 'rendering'!
341 augmentPage
342 Serpentron isCompatibleBrowser ifFalse: [
343 '#serpentron' asSilk resetContents
344 << 'Your browser is not supported.'
345 << Silk BR
346 << 'Please use a modern browser to run the game.'.
347 ^ self ].
348 '#serpentron' asSilk resetContents << 'Loading...'.
349 skin
350 load: 'resources/skin.png'
351 andDo: [ '#serpentron' asSilk resetContents << self ]
352 !
354 renderOnSilk: aSilk
355 | scale title container width height |
356 (title := aSilk DIV: {#id -> 'title'})
357 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
358 #target -> '_top'.
359 'Serpentron'});
360 SPAN: {#id -> 'status'. 'version 1.0.1'}.
362 (title IMG: {#id -> 'fullscreen-button'.
363 #title -> 'Toggle fullscreen'.
364 #alt -> 'Toggle fullscreen'.
365 #src -> 'resources/fullscreen.png'})
366 on: #click bind: [ Serpentron toggleFullscreen ].
368 container := aSilk DIV: {#id -> 'field'}.
370 self updateSize.
371 window onresize: [ self updateSize ].
373 container << field.
374 self renderStartScreenOn: container.
376 (container DIV: {#id -> 'message'. #class -> 'hidden'})
377 element style
378 margin: '0 auto'.
379 ! !
381 Serpentron class slots: {#Instance}!
383 !Serpentron class methodsFor: 'compatibility'!
385 isCompatibleBrowser
386 "No reason to polyfill requestAnimationFrame
387 or use vendor prefixes as browsers that do not have it
388 will likely have other incompatibilities."
389 <inlineJS: 'return window.requestAnimationFrame && true || false'>
390 !
392 toggleFullscreen
393 "Sample code from https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API"
394 <inlineJS: '
395 if (!!document.fullscreenElement &&
396 !!document.mozFullScreenElement && !!document.webkitFullscreenElement && !!document.msFullscreenElement ) {
397 if (document.documentElement.requestFullscreen) {
398 document.documentElement.requestFullscreen();
399 } else if (document.documentElement.msRequestFullscreen) {
400 document.documentElement.msRequestFullscreen();
401 } else if (document.documentElement.mozRequestFullScreen) {
402 document.documentElement.mozRequestFullScreen();
403 } else if (document.documentElement.webkitRequestFullscreen) {
404 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
405 }
406 } else {
407 if (document.exitFullscreen) {
408 document.exitFullscreen();
409 } else if (document.msExitFullscreen) {
410 document.msExitFullscreen();
411 } else if (document.mozCancelFullScreen) {
412 document.mozCancelFullScreen();
413 } else if (document.webkitExitFullscreen) {
414 document.webkitExitFullscreen();
415 }
416 }'>
417 ! !
419 !Serpentron class methodsFor: 'starting'!
421 start
422 (Instance := self new) augmentPage
423 ! !
425 Object subclass: #TronCollider
426 slots: {#canvas. #context. #skin. #animationRequest. #lastFrameTime. #particles. #particlePool. #particlesPerCollision}
427 package: 'Serpentron'!
428 !TronCollider commentStamp!
429 Particle system for collision effect.!
431 !TronCollider methodsFor: 'accessing'!
433 canvas: aCanvas
434 canvas := aCanvas.
435 context := canvas getContext: '2d'.
436 !
438 skin: aTronSkin
439 skin := aTronSkin
440 ! !
442 !TronCollider methodsFor: 'initialization'!
444 initialize
445 super initialize.
446 particlesPerCollision := 200.
447 particlePool := Queue new.
448 particlesPerCollision * 2 + 10 timesRepeat: [
449 particlePool nextPut: TronColliderParticle new ].
450 particles := OrderedCollection new
451 ! !
453 !TronCollider methodsFor: 'private'!
455 renderFrame: timestamp
456 | liveParticles delta |
457 delta := lastFrameTime
458 ifNil: [ 0 ]
459 ifNotNil: [ timestamp - lastFrameTime ].
460 lastFrameTime := timestamp.
461 "Transcript show: 'renderFrame: ';
462 show: timestamp;
463 show: '; ';
464 show: delta;
465 cr."
466 liveParticles := OrderedCollection new.
467 context clearRect: 0 y: 0 w: canvas width h: canvas height.
468 particles do: [ :each |
469 each
470 update: delta;
471 drawOn: context tileSize: skin tileSize.
472 each
473 ifAlive: [ liveParticles add: each ]
474 ifNotAlive: [ particlePool nextPut: each ] ].
475 particles := liveParticles.
476 animationRequest := particles
477 ifEmpty: [ nil ]
478 ifNotEmpty: [
479 window
480 requestAnimationFrame: [ :ts |
481 self renderFrame: ts ]
482 on: canvas ]
483 !
485 startAnimation
486 animationRequest ifNotNil: [ ^ self ].
487 lastFrameTime := nil.
488 animationRequest := window
489 requestAnimationFrame: [ :ts | self renderFrame: ts ]
490 on: canvas.
491 ! !
493 !TronCollider methodsFor: 'starting'!
495 animateCollisionFor: aPlayer
496 | particle |
497 particlesPerCollision timesRepeat: [
498 particles add:
499 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
500 resetPosition:
501 aPlayer location - 0.5 - (aPlayer direction / 2)
502 color: (Math random < 0.5
503 ifTrue: [ aPlayer color ]
504 ifFalse: [ '#ff3000' ])) ].
505 self startAnimation.
506 ! !
508 Object subclass: #TronColliderParticle
509 slots: {#color. #size. #velocity. #position. #alpha. #decay}
510 package: 'Serpentron'!
512 !TronColliderParticle methodsFor: 'accessing'!
514 ifAlive: aliveBlock ifNotAlive: notAliveBlock
515 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
516 !
518 resetPosition: positionPoint color: aString
519 position := positionPoint.
520 color := aString.
521 "Particles return to the pool as soon as they die.
522 Randomize on each reset or the pool will end up sotrted by particles' life time."
523 alpha := 1 - (Math random * 0.5).
524 decay := 0.997 - (Math random * 0.01)
525 ! !
527 !TronColliderParticle methodsFor: 'drawing'!
529 drawOn: aContext tileSize: tileSize
530 aContext
531 globalAlpha: alpha;
532 fillStyle: color;
533 fillRect: (position x - (size / 2)) * tileSize
534 y: (position y - (size / 2)) * tileSize
535 w: size * tileSize
536 h: size * tileSize.
537 ! !
539 !TronColliderParticle methodsFor: 'initialization'!
541 initialize
542 super initialize.
543 size := (Math random * 0.3 + 0.7).
544 velocity := (Math random - 0.5) @ (Math random - 0.5).
545 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
546 ! !
548 !TronColliderParticle methodsFor: 'updating'!
550 update: delta
551 position := position + (velocity * delta).
552 alpha := alpha * (decay raisedTo: delta).
553 ! !
555 Object subclass: #TronController
556 slots: {#field. #player}
557 package: 'Serpentron'!
558 !TronController commentStamp!
559 Abstract superclass for controlling input devices and algorithms.!
561 !TronController methodsFor: 'accessing'!
563 field: aTronField
564 field := aTronField
565 !
567 name
568 self subclassResponsibility.
569 !
571 nextDirection: aPoint
572 "Do not turn back."
573 player nextDirection = (aPoint * (-1 @ -1))
574 ifTrue: [ ^ self ].
575 player nextDirection: aPoint.
576 !
578 player: aTronPlayer
579 player := aTronPlayer
580 ! !
582 !TronController methodsFor: 'computing'!
584 compute
585 !
587 isAtDecisionPoint
588 | loc dir turn |
589 player isFirstMove ifTrue: [ ^ true ].
590 dir := player nextDirection.
591 loc := player location.
592 (field isFreeAt: loc + dir)
593 ifFalse: [ ^ true ].
594 turn := dir rotate90ccw.
595 ((field isFreeAt: loc + turn)
596 and: [ (field isFreeAt: loc + dir + turn) not ])
597 ifTrue: [ ^ true ].
598 turn := dir rotate90cw.
599 ((field isFreeAt: loc + turn)
600 and: [ (field isFreeAt: loc + dir + turn) not ])
601 ifTrue: [ ^ true ].
602 ^ false
603 ! !
605 !TronController methodsFor: 'event handling'!
607 keyDown: keyCode
608 ^ false
609 ! !
611 !TronController methodsFor: 'initialization'!
613 reset
614 ! !
616 TronController class slots: {#directions}!
618 TronController subclass: #TronComputerController1
619 slots: {#weight. #aggressiveness}
620 package: 'Serpentron'!
622 !TronComputerController1 methodsFor: 'accessing'!
624 name
625 ^ 'Computer'
626 ! !
628 !TronComputerController1 methodsFor: 'computing'!
630 compute
631 | best bestDirection |
632 "player isFirstMove
633 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
634 (self isAtDecisionPoint
635 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
636 ifFalse: [ ^ self ].
637 aggressiveness := 200 atRandom.
638 best := 0.
639 TronPlayer directions do: [ :each |
640 weight := 0.
641 self scan: each.
642 weight > best
643 ifTrue: [
644 best := weight.
645 bestDirection := each ]].
646 bestDirection ifNil: [ ^ self ].
647 self nextDirection: bestDirection.
648 ! !
650 !TronComputerController1 methodsFor: 'private'!
652 extend: directionPoint from: nwPoint to: sePoint
653 | point scanDir |
654 scanDir := directionPoint y abs @ directionPoint x abs.
655 point := Point
656 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
657 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
658 [ point <= sePoint ]
659 whileTrue: [
660 weight := weight + 1.
661 (field isFreeAt: point)
662 ifFalse: [
663 (self isEnemyHeadAt: point)
664 ifTrue: [ weight := weight + aggressiveness ]
665 ifFalse: [ ^ false ] ].
666 point := point + scanDir ].
667 ^ true
668 !
670 isEnemyHeadAt: aPoint
671 (field playerWithHeadAt: aPoint)
672 ifNotNil: [ :p | ^ p color ~= player color ].
673 ^ false
674 !
676 scan: directionPoint
677 | nw se nextNW nextSE directions scanDir |
678 nw := se := player location + directionPoint.
679 (field isFreeAt: nw) ifFalse: [ ^ self ].
680 directions := TronPlayer directions copy
681 remove: directionPoint * -1;
682 yourself.
683 [ scanDir := directions atRandom.
684 scanDir <= 0 asPoint
685 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
686 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
687 (self extend: scanDir from: nextNW to: nextSE)
688 ifTrue: [ nw := nextNW. se := nextSE ]
689 ifFalse: [ directions remove: scanDir ].
690 directions isEmpty not
691 ] whileTrue.
692 ! !
694 TronController subclass: #TronKeyboardController
695 slots: {#keyMap. #name}
696 package: 'Serpentron'!
698 !TronKeyboardController methodsFor: 'accessing'!
700 keyMap: aDictionary name: aString
701 keyMap := aDictionary.
702 name := 'Keyboard: ', aString.
703 !
705 name
706 ^ name
707 ! !
709 !TronKeyboardController methodsFor: 'event handling'!
711 keyDown: keyCode
712 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
713 ^ true
714 ! !
716 TronKeyboardController class slots: {#keyMaps}!
718 TronController subclass: #TronRandomController
719 slots: {}
720 package: 'Serpentron'!
722 !TronRandomController methodsFor: 'accessing'!
724 name
725 ^ 'Computer (random)'
726 ! !
728 !TronRandomController methodsFor: 'computing'!
730 compute
731 | dirs |
732 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
733 ifFalse: [ ^ self ].
734 dirs := TronPlayer directions
735 select: [ :each | field isFreeAt: player location + each ].
736 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
737 ! !
739 Object subclass: #TronField
740 slots: {#skin. #size. #matrix. #players. #livePlayers. #colors. #liveColors. #canvas. #context. #collider. #timerId. #onEndGame}
741 package: 'Serpentron'!
742 !TronField commentStamp!
743 The game field. Provides game logic and rendering.!
745 !TronField methodsFor: 'accessing'!
747 at: aPoint
748 ^ (matrix at: aPoint y) at: aPoint x
749 !
751 at: aPoint ifAbsent: aBlock
752 ^ matrix
753 at: aPoint y
754 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
755 ifAbsent: aBlock
756 !
758 at: aPoint put: aSegment
759 (matrix at: aPoint y) at: aPoint x put: aSegment
760 !
762 colors
763 ^ colors
764 !
766 fieldSize
767 ^ size
768 !
770 isFreeAt: aPoint
771 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
772 !
774 isGameFinished
775 ^ liveColors size < 2
776 !
778 liveColors
779 ^ liveColors
780 !
782 playerWithHeadAt: aPoint
783 | s |
784 s := self at: aPoint ifAbsent: [ ^ nil ].
785 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
786 !
788 players
789 ^ players
790 !
792 skin
793 ^ skin
794 !
796 skin: aTronSkin
797 skin := aTronSkin
798 !
800 winningColorIfNone: aBlock
801 ^ (liveColors size = 1)
802 ifTrue: [ liveColors keys anyOne ]
803 ifFalse: aBlock.
804 ! !
806 !TronField methodsFor: 'event handling'!
808 keyDown: event
809 "Check all players rather than livePlayers
810 so that preventDefault is called for all used keys."
811 (players anySatisfy: [ :each | each keyDown: event keyCode ])
812 ifTrue: [ event preventDefault ].
813 timerId ifNil: [ self startIfAllReady ].
814 ! !
816 !TronField methodsFor: 'initialization'!
818 initialize
819 super initialize.
820 livePlayers := players := #().
821 size := 50 @ 35
822 ! !
824 !TronField methodsFor: 'observing'!
826 onEndGame: aBlock
827 onEndGame := aBlock
828 ! !
830 !TronField methodsFor: 'private'!
832 endGame
833 self stopTimer.
834 livePlayers := #().
835 onEndGame ifNotNil: #value.
836 !
838 killPlayer: aPlayer
839 collider animateCollisionFor: aPlayer.
840 livePlayers remove: aPlayer.
841 self updateLiveColors.
842 !
844 locatePlayers
845 players size = 2 ifTrue: [
846 (players at: 1) location: 16 @ 18.
847 (players at: 2) location: 35 @ 18.
848 ^ self ].
849 players size = 3 ifTrue: [
850 (players at: 1) location: 18 @ 13.
851 (players at: 2) location: 18 @ 23.
852 (players at: 3) location: 33 @ 18.
853 ^ self ].
854 players size = 4 ifTrue: [
855 (players at: 1) location: 16 @ 11.
856 (players at: 2) location: 35 @ 11.
857 (players at: 3) location: 16 @ 25.
858 (players at: 4) location: 35 @ 25.
859 ^ self ].
860 self error: 'Invalid number of players.'
861 !
863 renderCanvasOn: aSilk
864 ^ aSilk CANVAS: {
865 #width -> (size x * skin tileSize).
866 #height -> (size y * skin tileSize)}.
867 !
869 startIfAllReady
870 (livePlayers isEmpty not
871 and: [ livePlayers allSatisfy: #isReady ])
872 ifTrue: [ self startTimer ]
873 !
875 startTimer
876 timerId ifNotNil: [ self error: 'Timer already running.' ].
877 timerId := window setInterval: [ self update ] every: 75.
878 !
880 update
881 | lpCopy |
882 lpCopy := livePlayers copy.
883 lpCopy do: [ :each |
884 | l |
885 l := each location.
886 self at: l put: each move.
887 skin drawField: self on: context at: l ].
888 lpCopy do: [ :each |
889 | l other |
890 l := each location.
891 (self isFreeAt: l)
892 ifTrue: [ self at: l put: each headSegment. ]
893 ifFalse: [
894 self killPlayer: each.
895 "Check for head-to-head collision."
896 other := self playerWithHeadAt: l.
897 (other isNil | (other = each))
898 ifFalse: [
899 self killPlayer: other.
900 self at: l put: nil. ]]].
901 livePlayers do: [ :each |
902 skin drawField: self on: context at: each location ].
903 self isGameFinished ifTrue: [ ^ self endGame ].
904 livePlayers do: #compute.
905 !
907 updateLiveColors
908 liveColors := Dictionary new.
909 livePlayers do: [ :each |
910 liveColors
911 at: each color
912 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
913 ifAbsent: [ liveColors at: each color put: 1 ]].
914 ! !
916 !TronField methodsFor: 'rendering'!
918 renderOnSilk: aSilk
919 | backgroundCanvas |
921 backgroundCanvas := (self renderCanvasOn: aSilk) element.
923 canvas := (self renderCanvasOn: aSilk) element.
924 context := canvas getContext: '2d'.
926 collider := TronCollider new
927 canvas: (self renderCanvasOn: aSilk) element;
928 skin: skin.
930 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
931 ! !
933 !TronField methodsFor: 'starting'!
935 start: anArrayOfPlayers
936 players := anArrayOfPlayers.
937 livePlayers := players copy.
938 self updateLiveColors.
939 colors := liveColors.
940 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
941 context clearRect: 0 y: 0 w: canvas width h: canvas height.
942 self locatePlayers.
943 players do: [ :each | each field: self; reset; compute ].
944 players do: [ :each |
945 self at: each location put: each headSegment.
946 skin drawField: self on: context at: each location ].
947 self startIfAllReady.
948 ! !
950 !TronField methodsFor: 'stopping'!
952 stopTimer
953 timerId ifNotNil: [
954 window clearInterval: timerId.
955 timerId := nil ]
956 ! !
958 Object subclass: #TronPlayer
959 slots: {#enabled. #color. #name. #controller. #segments. #location. #moved. #direction. #nextDirection. #onColorChange. #onEnabledChange. #onControllerChange}
960 package: 'Serpentron'!
962 !TronPlayer methodsFor: 'accessing'!
964 color
965 ^ color
966 !
968 color: anObject
969 color := anObject.
970 onColorChange ifNotNil: #value.
971 !
973 controller
974 ^ controller
975 !
977 controller: aTronController
978 aTronController player: self.
979 controller := aTronController.
980 onControllerChange ifNotNil: #value.
981 !
983 direction
984 ^ direction
985 !
987 enabled: aBoolean
988 enabled := aBoolean.
989 onEnabledChange ifNotNil: #value.
990 !
992 field: aField
993 controller field: aField
994 !
996 headSegment
997 ^ segments at: direction
998 !
1000 isEnabled
1001 ^ enabled
1004 isFirstMove
1005 ^ moved not
1008 isReady
1009 ^ nextDirection notNil
1012 location
1013 ^ location
1016 location: anObject
1017 location := anObject
1020 name
1021 ^ name
1024 name: anObject
1025 name := anObject.
1028 nextDirection
1029 ^ nextDirection
1032 nextDirection: aPoint
1033 moved ifFalse: [ direction := aPoint ].
1034 nextDirection := aPoint
1035 ! !
1037 !TronPlayer methodsFor: 'event handling'!
1039 keyDown: keyCode
1040 ^ controller keyDown: keyCode
1041 ! !
1043 !TronPlayer methodsFor: 'initialization'!
1045 initialize
1046 super initialize.
1047 enabled := true.
1048 self reset; initializeSegments.
1051 reset
1052 direction := 0 @ -1.
1053 nextDirection := nil.
1054 moved := false.
1055 controller ifNotNil: [ controller reset ].
1056 ! !
1058 !TronPlayer methodsFor: 'observing'!
1060 onColorChange: aBlock
1061 onColorChange := aBlock.
1064 onControllerChange: aBlock
1065 onControllerChange := aBlock.
1068 onEnabledChange: aBlock
1069 onEnabledChange := aBlock.
1070 ! !
1072 !TronPlayer methodsFor: 'private'!
1074 initializeSegments
1075 segments := Dictionary new.
1076 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1077 segments
1078 at: to
1079 put: (TronHead new
1080 player: self;
1081 sprite: 'head', toName;
1082 direction: to);
1083 at: {to. to}
1084 put: (TronSegment new
1085 player: self;
1086 sprite: 'body', toName).
1087 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1088 (from x = to x) | (from y = to y) ifFalse: [
1089 segments
1090 at: {from. to}
1091 put: (TronSegment new
1092 player: self;
1093 sprite: 'body', fromName, 'To', toName) ]]].
1094 ! !
1096 !TronPlayer methodsFor: 'updating'!
1098 compute
1099 controller compute
1102 move
1103 | segment |
1104 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1105 direction := nextDirection.
1106 location := location + direction.
1107 moved := true.
1108 ^ segment
1109 ! !
1111 TronPlayer class slots: {#directions. #directionNames}!
1113 !TronPlayer class methodsFor: 'initialization'!
1115 directionNames
1116 ^ directionNames
1119 directions
1120 ^ directions
1123 initialize
1124 super initialize.
1125 directionNames := Dictionary new
1126 at: 0 @ -1 put: 'North';
1127 at: 1 @ 0 put: 'East';
1128 at: 0 @ 1 put: 'South';
1129 at: -1 @ 0 put: 'West';
1130 yourself.
1131 directions := directionNames keys.
1132 ! !
1134 Object subclass: #TronSegment
1135 slots: {#player. #sprite}
1136 package: 'Serpentron'!
1138 !TronSegment methodsFor: 'accessing'!
1140 color
1141 ^ player color
1144 isHead
1145 ^ false
1148 player
1149 ^ player
1152 player: anObject
1153 player := anObject
1156 sprite
1157 ^ sprite
1160 sprite: anObject
1161 sprite := anObject
1162 ! !
1164 TronSegment subclass: #TronHead
1165 slots: {#direction}
1166 package: 'Serpentron'!
1168 !TronHead methodsFor: 'accessing'!
1170 direction
1171 ^ direction
1174 direction: anObject
1175 direction := anObject
1178 isHead
1179 ^ true
1180 ! !
1182 Object subclass: #TronSkin
1183 slots: {#skinMap. #skinImage. #tileSize. #maskOffset}
1184 package: 'Serpentron'!
1186 !TronSkin methodsFor: 'accessing'!
1188 tileSize
1189 ^ tileSize
1190 ! !
1192 !TronSkin methodsFor: 'drawing'!
1194 drawBackgroundOn: aContext at: aPoint
1195 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1196 drawTile: #backgroundTile on: aContext at: aPoint
1199 drawBackgroundOn: aContext from: nwPoint to: sePoint
1200 nwPoint y to: sePoint y do: [ :row |
1201 nwPoint x to: sePoint x do: [ :col |
1202 self drawBackgroundOn: aContext at: col @ row ]]
1205 drawField: aField on: aContext at: aPoint
1206 | x y segment |
1208 x := (aPoint - 1) x * tileSize.
1209 y := (aPoint - 1) y * tileSize.
1211 aContext
1212 clearRect: x and: y and: tileSize and: tileSize.
1214 segment := (aField at: aPoint) ifNil: [ ^ self ].
1216 self
1217 drawTile: segment sprite
1218 offset: maskOffset
1219 on: aContext
1220 at: aPoint.
1221 aContext
1222 globalCompositeOperation: 'source-atop';
1223 fillStyle: segment color;
1224 fillRect: x and: y and: tileSize and: tileSize.
1225 self drawTile: segment sprite on: aContext at: aPoint.
1227 aContext globalCompositeOperation: 'source-over'.
1230 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1231 aContext drawImage: skinImage
1232 sx: sourcePoint x
1233 sy: sourcePoint y
1234 sw: tileSize
1235 sh: tileSize
1236 dx: destinationPoint x
1237 dy: destinationPoint y
1238 dw: tileSize
1239 dh: tileSize
1242 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1243 self
1244 drawSkinImageOn: aContext
1245 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1246 destination: (targetPoint - 1) * tileSize
1249 drawTile: aSymbol on: aContext at: aPoint
1250 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1251 ! !
1253 !TronSkin methodsFor: 'initialization'!
1255 initialize
1256 super initialize.
1257 skinMap := Dictionary new
1258 at: #background put: 0@0;
1259 at: #backgroundBottomRight put: 49@34;
1260 at: #backgroundTile put: 0@35;
1261 at: #headNorth put: 1@35;
1262 at: #headEast put: 2@35;
1263 at: #headSouth put: 3@35;
1264 at: #headWest put: 4@35;
1265 at: #bodyNorth put: 5@35;
1266 at: #bodyEast put: 6@35;
1267 at: #bodySouth put: 7@35;
1268 at: #bodyWest put: 8@35;
1269 at: #bodySouthToEast put: 9@35;
1270 at: #bodyWestToNorth put: 9@35;
1271 at: #bodyNorthToEast put: 10@35;
1272 at: #bodyWestToSouth put: 10@35;
1273 at: #bodyNorthToWest put: 11@35;
1274 at: #bodyEastToSouth put: 11@35;
1275 at: #bodySouthToWest put: 12@35;
1276 at: #bodyEastToNorth put: 12@35;
1277 yourself.
1278 maskOffset := 12 @ 0.
1279 skinImage := document createElement: 'img'
1280 ! !
1282 !TronSkin methodsFor: 'loading'!
1284 load: url andDo: aBlock
1285 skinImage onload: [
1286 tileSize := skinImage width / 50.
1287 aBlock value ].
1288 skinImage src: url.
1289 ! !
1291 !Point methodsFor: '*Serpentron'!
1293 rotate90ccw
1294 ^ self y @ self x negated
1297 rotate90cw
1298 ^ self y negated @ self x
1299 ! !