view src/Serpentron.st @ 9:8c7e910cb328

Fullscreen mode.
author Mikhail Kryshen <mikhail@kryshen.net>
date Tue, 29 Mar 2016 22:15:05 +0300
parents b32efce2d860
children 5bb39198a9be
line source
1 Smalltalk createPackage: 'Serpentron'!
2 (Smalltalk packageAt: 'Serpentron') imports: {'silk/Silk'}!
3 Object subclass: #Serpentron
4 instanceVariableNames: '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 status: (self winnerDOM: color) << ' won the round.'.
37 timeoutId := window
38 setTimeout: [ self nextRound ]
39 after: 2000.
40 ^ self ].
41 self status: self scoreDOM.
42 timeoutId := window
43 setTimeout: [ self showGameWinner: color ]
44 after: 1000.
45 !
47 hideMessage
48 (Silk at: '#message') element className: 'hidden'.
49 !
51 initializeControllers
52 controllerPrototypes := {
53 TronKeyboardController new
54 keyMap: #{
55 38 -> (0 @ -1).
56 39 -> (1 @ 0).
57 40 -> (0 @ 1).
58 37 -> (-1 @ 0)}
59 name: 'arrows'.
60 TronKeyboardController new
61 keyMap: #{
62 87 -> (0 @ -1).
63 68 -> (1 @ 0).
64 83 -> (0 @ 1).
65 65 -> (-1 @ 0)}
66 name: 'WASD'.
67 TronKeyboardController new
68 keyMap: #{
69 89 -> (0 @ -1).
70 74 -> (1 @ 0).
71 72 -> (0 @ 1).
72 71 -> (-1 @ 0)}
73 name: 'YGHJ'.
74 TronKeyboardController new
75 keyMap: #{
76 80 -> (0 @ -1).
77 222 -> (1 @ 0).
78 59 -> (0 @ 1).
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 self status: self scoreDOM.
124 field start: field players.
125 !
127 randomizePlayerColors
128 | enabledPlayers |
129 enabledPlayers := players select: #isEnabled.
130 enabledPlayers do: [ :each |
131 | color |
132 [ color := playerColors atRandom.
133 enabledPlayers allSatisfy: [ :p |
134 p = each | (p color ~= color) ]
135 ] whileFalse.
136 each color: color.
137 ].
138 !
140 randomizeTeams
141 | colorA colorB teamA |
142 players do: [ :each | each enabled: true ].
143 colorA := playerColors atRandom.
144 [ colorB := playerColors atRandom.
145 colorB = colorA ] whileTrue.
146 teamA := players copy.
147 players size // 2
148 timesRepeat: [ (teamA remove: teamA atRandom) color: colorB ].
149 teamA do: [ :each | each color: colorA ].
150 !
152 renderColorSelectorFor: aPlayer on: aSilk
153 | container buttons onChange |
154 container := aSilk SPAN: {#class -> 'color-selector'}.
155 buttons := Dictionary from:
156 (playerColors collect: [ :each |
157 | button |
158 button := container A.
159 button on: #click bind: [
160 aPlayer enabled: true; color: each.
161 self validatePlayerColor: aPlayer ].
162 button element style background: each.
163 each -> button ]).
164 buttons at: #disabled put: (container A
165 SPAN: '✗';
166 on: #click bind: [
167 aPlayer enabled: false.
168 self validatePlayerColor: aPlayer ]).
169 onChange := [
170 buttons keysAndValuesDo: [ :k :v |
171 (aPlayer isEnabled not & k = #disabled) |
172 (aPlayer isEnabled & k = aPlayer color)
173 ifTrue: [ v element className: 'selected-color-button' ]
174 ifFalse: [ v element className: 'color-button' ] ] ].
175 aPlayer
176 onEnabledChange: onChange;
177 onColorChange: onChange.
178 onChange value.
179 !
181 renderControllerSelectorFor: aPlayer on: aSilk
182 | select options onChange |
183 select := aSilk SELECT.
184 options := Dictionary new.
185 controllerPrototypes do: [ :each |
186 options at: each name put: (select OPTION: each name) element ].
187 select
188 on: #change
189 bind: [
190 aPlayer controller:
191 (controllerPrototypes
192 detect: [ :each | each name = select element value ])
193 copy ].
194 onChange := [
195 | value |
196 value := aPlayer controller name.
197 (options at: value) selected: 'selected'.
198 aPlayer controller class = TronKeyboardController
199 ifTrue: [
200 players do: [ :each |
201 (each ~= aPlayer and: [ each controller name = value ])
202 ifTrue: [ each controller: TronComputerController1 new ]]]].
203 aPlayer onControllerChange: onChange.
204 onChange value.
205 !
207 renderPlayer: aPlayer on: aSilk
208 | container |
209 container := aSilk DIV: { #class -> 'player' }.
210 self renderColorSelectorFor: aPlayer on: container.
211 (container INPUT
212 on: #change bind: [ :event |
213 aPlayer name: event target value ])
214 element value: aPlayer name.
215 self renderControllerSelectorFor: aPlayer on: container.
216 !
218 renderStartScreenOn: aSilk
219 | startScreen |
220 startScreen := aSilk DIV: {#id -> 'start-screen'}.
222 players do: [ :each | self renderPlayer: each on: startScreen ].
224 (startScreen BUTTON: 'Random colors')
225 on: #click bind: [ self randomizePlayerColors ].
226 (startScreen BUTTON: 'Random teams')
227 on: #click bind: [ self randomizeTeams ].
229 startScreen BR.
231 (startScreen BUTTON: 'Start')
232 on: #click bind: [ :event | self startGame; status: self scoreDOM ].
233 !
235 scoreDOM
236 | message |
237 message := Silk SPAN: 'Score: '.
238 score associations
239 do: [ :each |
240 (message SPAN: {#class -> 'player-color'. (each value * 10) rounded / 10})
241 element style color: each key ]
242 separatedBy: [ message << ':' ].
243 message << ('/', pointsToWin).
244 ^ message.
245 !
247 showGameWinner: color
248 self showMessage: (self winnerDOM: color) << ' won the game!!'.
249 timeoutId := window
250 setTimeout: [
251 self
252 startScreenVisible: true;
253 hideMessage;
254 status: self scoreDOM << '. ' << (self winnerDOM: color) << ' won the game!!' ]
255 after: 1000.
256 !
258 showMessage: anObject
259 ((Silk at: '#message') resetContents << anObject)
260 element className: 'visible'.
261 !
263 startGame
264 | enabledPlayers |
265 self startScreenVisible: false.
266 field start: (players select: #isEnabled).
267 pointsToWin := (30 / (field players size + 1)) ceiling.
268 score := field colors collect: [ :each | 0 ].
269 !
271 startScreenVisible: aBoolean
272 startScreenVisible := aBoolean.
273 (Silk at: '#start-screen') element
274 className: (aBoolean ifTrue: [ 'visible' ] ifFalse: [ 'hidden' ]).
275 !
277 status: anObject
278 '#status' asSilk resetContents << anObject
279 !
281 updateScore
282 | color teamSize alive points defeated |
283 color := field winningColorIfNone: [ ^ self ].
284 teamSize := field colors at: color.
285 defeated := field players size - teamSize.
286 alive := field liveColors at: color.
287 points := defeated * alive / teamSize * (teamSize + 1) / 2.
288 points := points / (field players size - 1).
289 score at: color put: (score at: color) + points.
290 !
292 updateSize
293 | w h fw fh ratio |
294 w := window innerWidth.
295 h := window innerHeight - ('#title' asSilk element offsetHeight).
296 ratio := field fieldSize x / field fieldSize y.
297 (w / ratio <= h)
298 ifTrue: [ fw := w. fh := w / ratio ]
299 ifFalse: [ fh := h. fw := h * ratio ].
300 '#serpentron' asSilk element style
301 width: fw rounded asString, 'px';
302 marginLeft: ((w - fw) / 2) rounded asString, 'px';
303 marginTop: ((h - fh) / 2) rounded asString, 'px'.
304 '#field' asSilk element style
305 width: fw rounded asString, 'px';
306 height: fh rounded asString, 'px'.
307 !
309 validatePlayerColor: aPlayer
310 | enabledPlayers otherPlayers freeColor |
311 enabledPlayers := players select: #isEnabled.
312 ((enabledPlayers collect: #color) asSet size > 1)
313 ifTrue: [ ^ self ].
314 freeColor := playerColors detect: [ :each | each ~= enabledPlayers anyOne color ].
315 otherPlayers := players select: [ :each | each ~~ aPlayer ].
316 (otherPlayers
317 detect: [ :each | each isEnabled not ]
318 ifNone: [ otherPlayers first ])
319 enabled: true;
320 color: freeColor.
321 !
323 winnerDOM: color
324 | names team message |
325 team := field players select: [ :each | each color = color ].
326 names := ''.
327 (team collect: #name)
328 do: [ :each | names := names, each]
329 separatedBy: [ names := names, ' and ' ].
330 message := Silk SPAN.
331 (message SPAN: {#class -> 'player-color'. names})
332 element style color: color.
333 ^ message
334 ! !
336 !Serpentron methodsFor: 'rendering'!
338 augmentPage
339 Serpentron isCompatibleBrowser ifFalse: [
340 '#serpentron' asSilk resetContents
341 << 'Your browser is not supported.'
342 << Silk BR
343 << 'Please use a modern browser to run the game.'.
344 ^ self ].
345 '#serpentron' asSilk resetContents << 'Loading...'.
346 skin
347 load: 'resources/skin.png'
348 andDo: [ '#serpentron' asSilk resetContents << self ]
349 !
351 renderOnSilk: aSilk
352 | scale title container width height |
353 (title := aSilk DIV: {#id -> 'title'})
354 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
355 #target -> '_top'.
356 'Serpentron'});
357 SPAN: {#id -> 'status'. '1.0-beta2'}.
359 (title BUTTON: {#id -> 'fullscreen-button'.
360 #title -> 'Toggle fullscreen'.
361 '▣'})
362 on: #click bind: [ Serpentron toggleFullscreen ].
364 container := aSilk DIV: {#id -> 'field'}.
366 self updateSize.
367 window onresize: [ self updateSize ].
369 container << field.
370 self renderStartScreenOn: container.
372 (container DIV: {#id -> 'message'. #class -> 'hidden'})
373 element style
374 margin: '0 auto'.
375 ! !
377 Serpentron class instanceVariableNames: 'Instance'!
379 !Serpentron class methodsFor: 'compatibility'!
381 isCompatibleBrowser
382 "No reason to polyfill requestAnimationFrame
383 or use vendor prefixes as browsers that do not have it
384 will likely have other incompatibilities."
385 < return window.requestAnimationFrame && true || false >
386 !
388 toggleFullscreen
389 "Sample code from https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API"
390 <
391 if (!!document.fullscreenElement &&
392 !!document.mozFullScreenElement && !!document.webkitFullscreenElement && !!document.msFullscreenElement ) {
393 if (document.documentElement.requestFullscreen) {
394 document.documentElement.requestFullscreen();
395 } else if (document.documentElement.msRequestFullscreen) {
396 document.documentElement.msRequestFullscreen();
397 } else if (document.documentElement.mozRequestFullScreen) {
398 document.documentElement.mozRequestFullScreen();
399 } else if (document.documentElement.webkitRequestFullscreen) {
400 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
401 }
402 } else {
403 if (document.exitFullscreen) {
404 document.exitFullscreen();
405 } else if (document.msExitFullscreen) {
406 document.msExitFullscreen();
407 } else if (document.mozCancelFullScreen) {
408 document.mozCancelFullScreen();
409 } else if (document.webkitExitFullscreen) {
410 document.webkitExitFullscreen();
411 }
412 }
413 >
414 ! !
416 !Serpentron class methodsFor: 'starting'!
418 start
419 (Instance := self new) augmentPage
420 ! !
422 Object subclass: #TronCollider
423 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
424 package: 'Serpentron'!
425 !TronCollider commentStamp!
426 Particle system for collision effect.!
428 !TronCollider methodsFor: 'accessing'!
430 canvas: aCanvas
431 canvas := aCanvas.
432 context := canvas getContext: '2d'.
433 !
435 skin: aTronSkin
436 skin := aTronSkin
437 ! !
439 !TronCollider methodsFor: 'initialization'!
441 initialize
442 super initialize.
443 particlesPerCollision := 200.
444 particlePool := Queue new.
445 particlesPerCollision * 2 + 10 timesRepeat: [
446 particlePool nextPut: TronColliderParticle new ].
447 particles := OrderedCollection new
448 ! !
450 !TronCollider methodsFor: 'private'!
452 renderFrame: timestamp
453 | liveParticles delta |
454 delta := lastFrameTime
455 ifNil: [ 0 ]
456 ifNotNil: [ timestamp - lastFrameTime ].
457 lastFrameTime := timestamp.
458 "Transcript show: 'renderFrame: ';
459 show: timestamp;
460 show: '; ';
461 show: delta;
462 cr."
463 liveParticles := OrderedCollection new.
464 context clearRect: 0 y: 0 w: canvas width h: canvas height.
465 particles do: [ :each |
466 each
467 update: delta;
468 drawOn: context tileSize: skin tileSize.
469 each
470 ifAlive: [ liveParticles add: each ]
471 ifNotAlive: [ particlePool nextPut: each ] ].
472 particles := liveParticles.
473 animationRequest := particles
474 ifEmpty: [ nil ]
475 ifNotEmpty: [
476 window
477 requestAnimationFrame: [ :ts |
478 self renderFrame: ts ]
479 on: canvas ]
480 !
482 startAnimation
483 animationRequest ifNotNil: [ ^ self ].
484 lastFrameTime := nil.
485 animationRequest := window
486 requestAnimationFrame: [ :ts | self renderFrame: ts ]
487 on: canvas.
488 ! !
490 !TronCollider methodsFor: 'starting'!
492 animateCollisionFor: aPlayer
493 | particle |
494 particlesPerCollision timesRepeat: [
495 particles add:
496 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
497 resetPosition:
498 aPlayer location - 0.5 - (aPlayer direction / 2)
499 color: (Math random < 0.5
500 ifTrue: [ aPlayer color ]
501 ifFalse: [ '#ff3000' ])) ].
502 self startAnimation.
503 ! !
505 Object subclass: #TronColliderParticle
506 instanceVariableNames: 'color size velocity position alpha decay'
507 package: 'Serpentron'!
509 !TronColliderParticle methodsFor: 'accessing'!
511 ifAlive: aliveBlock ifNotAlive: notAliveBlock
512 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
513 !
515 resetPosition: positionPoint color: aString
516 position := positionPoint.
517 color := aString.
518 "Particles return to the pool as soon as they die.
519 Randomize on each reset or the pool will end up sotrted by particles' life time."
520 alpha := 1 - (Math random * 0.5).
521 decay := 0.997 - (Math random * 0.01)
522 ! !
524 !TronColliderParticle methodsFor: 'drawing'!
526 drawOn: aContext tileSize: tileSize
527 aContext
528 globalAlpha: alpha;
529 fillStyle: color;
530 fillRect: (position x - (size / 2)) * tileSize
531 y: (position y - (size / 2)) * tileSize
532 w: size * tileSize
533 h: size * tileSize.
534 ! !
536 !TronColliderParticle methodsFor: 'initialization'!
538 initialize
539 super initialize.
540 size := (Math random * 0.3 + 0.7).
541 velocity := (Math random - 0.5) @ (Math random - 0.5).
542 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
543 ! !
545 !TronColliderParticle methodsFor: 'updating'!
547 update: delta
548 position := position + (velocity * delta).
549 alpha := alpha * (decay raisedTo: delta).
550 ! !
552 Object subclass: #TronController
553 instanceVariableNames: 'field player'
554 package: 'Serpentron'!
555 !TronController commentStamp!
556 Abstract superclass for controlling input devices and algorithms.!
558 !TronController methodsFor: 'accessing'!
560 field: aTronField
561 field := aTronField
562 !
564 name
565 self subclassResponsibility.
566 !
568 nextDirection: aPoint
569 "Do not turn back."
570 player nextDirection = (aPoint * (-1 @ -1))
571 ifTrue: [ ^ self ].
572 player nextDirection: aPoint.
573 !
575 player: aTronPlayer
576 player := aTronPlayer
577 ! !
579 !TronController methodsFor: 'computing'!
581 compute
582 !
584 isAtDecisionPoint
585 | loc dir turn |
586 player isFirstMove ifTrue: [ ^ true ].
587 dir := player nextDirection.
588 loc := player location.
589 (field isFreeAt: loc + dir)
590 ifFalse: [ ^ true ].
591 turn := dir rotate90ccw.
592 ((field isFreeAt: loc + turn)
593 and: [ (field isFreeAt: loc + dir + turn) not ])
594 ifTrue: [ ^ true ].
595 turn := dir rotate90cw.
596 ((field isFreeAt: loc + turn)
597 and: [ (field isFreeAt: loc + dir + turn) not ])
598 ifTrue: [ ^ true ].
599 ^ false
600 ! !
602 !TronController methodsFor: 'event handling'!
604 keyDown: keyCode
605 ^ false
606 ! !
608 !TronController methodsFor: 'initialization'!
610 reset
611 ! !
613 TronController class instanceVariableNames: 'directions'!
615 TronController subclass: #TronComputerController1
616 instanceVariableNames: 'weight aggressiveness'
617 package: 'Serpentron'!
619 !TronComputerController1 methodsFor: 'accessing'!
621 name
622 ^ 'Computer'
623 ! !
625 !TronComputerController1 methodsFor: 'computing'!
627 compute
628 | best bestDirection |
629 "player isFirstMove
630 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
631 (self isAtDecisionPoint
632 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
633 ifFalse: [ ^ self ].
634 aggressiveness := 200 atRandom.
635 best := 0.
636 TronPlayer directions do: [ :each |
637 weight := 0.
638 self scan: each.
639 weight > best
640 ifTrue: [
641 best := weight.
642 bestDirection := each ]].
643 bestDirection ifNil: [ ^ self ].
644 self nextDirection: bestDirection.
645 ! !
647 !TronComputerController1 methodsFor: 'private'!
649 extend: directionPoint from: nwPoint to: sePoint
650 | point scanDir |
651 scanDir := directionPoint y abs @ directionPoint x abs.
652 point := Point
653 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
654 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
655 [ point <= sePoint ]
656 whileTrue: [
657 weight := weight + 1.
658 (field isFreeAt: point)
659 ifFalse: [
660 (self isEnemyHeadAt: point)
661 ifTrue: [ weight := weight + aggressiveness ]
662 ifFalse: [ ^ false ] ].
663 point := point + scanDir ].
664 ^ true
665 !
667 isEnemyHeadAt: aPoint
668 (field playerWithHeadAt: aPoint)
669 ifNotNil: [ :p | ^ p color ~= player color ].
670 ^ false
671 !
673 scan: directionPoint
674 | nw se nextNW nextSE directions scanDir |
675 nw := se := player location + directionPoint.
676 (field isFreeAt: nw) ifFalse: [ ^ self ].
677 directions := TronPlayer directions copy
678 remove: directionPoint * -1;
679 yourself.
680 [ scanDir := directions atRandom.
681 scanDir <= 0 asPoint
682 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
683 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
684 (self extend: scanDir from: nextNW to: nextSE)
685 ifTrue: [ nw := nextNW. se := nextSE ]
686 ifFalse: [ directions remove: scanDir ].
687 directions isEmpty not
688 ] whileTrue.
689 ! !
691 TronController subclass: #TronKeyboardController
692 instanceVariableNames: 'keyMap name'
693 package: 'Serpentron'!
695 !TronKeyboardController methodsFor: 'accessing'!
697 keyMap: aDictionary name: aString
698 keyMap := aDictionary.
699 name := 'Keyboard: ', aString.
700 !
702 name
703 ^ name
704 ! !
706 !TronKeyboardController methodsFor: 'event handling'!
708 keyDown: keyCode
709 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
710 ^ true
711 ! !
713 TronKeyboardController class instanceVariableNames: 'keyMaps'!
715 TronController subclass: #TronRandomController
716 instanceVariableNames: ''
717 package: 'Serpentron'!
719 !TronRandomController methodsFor: 'accessing'!
721 name
722 ^ 'Computer (random)'
723 ! !
725 !TronRandomController methodsFor: 'computing'!
727 compute
728 | dirs |
729 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
730 ifFalse: [ ^ self ].
731 dirs := TronPlayer directions
732 select: [ :each | field isFreeAt: player location + each ].
733 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
734 ! !
736 Object subclass: #TronField
737 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
738 package: 'Serpentron'!
739 !TronField commentStamp!
740 The game field. Provides game logic and rendering.!
742 !TronField methodsFor: 'accessing'!
744 at: aPoint
745 ^ (matrix at: aPoint y) at: aPoint x
746 !
748 at: aPoint ifAbsent: aBlock
749 ^ matrix
750 at: aPoint y
751 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
752 ifAbsent: aBlock
753 !
755 at: aPoint put: aSegment
756 (matrix at: aPoint y) at: aPoint x put: aSegment
757 !
759 colors
760 ^ colors
761 !
763 fieldSize
764 ^ size
765 !
767 isFreeAt: aPoint
768 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
769 !
771 isGameFinished
772 ^ liveColors size < 2
773 !
775 liveColors
776 ^ liveColors
777 !
779 playerWithHeadAt: aPoint
780 | s |
781 s := self at: aPoint ifAbsent: [ ^ nil ].
782 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
783 !
785 players
786 ^ players
787 !
789 skin
790 ^ skin
791 !
793 skin: aTronSkin
794 skin := aTronSkin
795 !
797 winningColorIfNone: aBlock
798 ^ (liveColors size = 1)
799 ifTrue: [ liveColors keys anyOne ]
800 ifFalse: aBlock.
801 ! !
803 !TronField methodsFor: 'event handling'!
805 keyDown: event
806 "Check all players rather than livePlayers
807 so that preventDefault is called for all used keys."
808 (players anySatisfy: [ :each | each keyDown: event keyCode ])
809 ifTrue: [ event preventDefault ].
810 timerId ifNil: [ self startIfAllReady ].
811 ! !
813 !TronField methodsFor: 'initialization'!
815 initialize
816 super initialize.
817 livePlayers := players := #().
818 size := 50 @ 35
819 ! !
821 !TronField methodsFor: 'observing'!
823 onEndGame: aBlock
824 onEndGame := aBlock
825 ! !
827 !TronField methodsFor: 'private'!
829 endGame
830 self stopTimer.
831 livePlayers := #().
832 onEndGame ifNotNil: #value.
833 !
835 killPlayer: aPlayer
836 collider animateCollisionFor: aPlayer.
837 livePlayers remove: aPlayer.
838 self updateLiveColors.
839 !
841 locatePlayers
842 players size = 2 ifTrue: [
843 (players at: 1) location: 16 @ 18.
844 (players at: 2) location: 35 @ 18.
845 ^ self ].
846 players size = 3 ifTrue: [
847 (players at: 1) location: 18 @ 13.
848 (players at: 2) location: 18 @ 23.
849 (players at: 3) location: 33 @ 18.
850 ^ self ].
851 players size = 4 ifTrue: [
852 (players at: 1) location: 16 @ 11.
853 (players at: 2) location: 35 @ 11.
854 (players at: 3) location: 16 @ 25.
855 (players at: 4) location: 35 @ 25.
856 ^ self ].
857 self error: 'Invalid number of players.'
858 !
860 renderCanvasOn: aSilk
861 ^ aSilk CANVAS: {
862 #width -> (size x * skin tileSize).
863 #height -> (size y * skin tileSize)}.
864 !
866 startIfAllReady
867 (livePlayers isEmpty not
868 and: [ livePlayers allSatisfy: #isReady ])
869 ifTrue: [ self startTimer ]
870 !
872 startTimer
873 timerId ifNotNil: [ self error: 'Timer already running.' ].
874 timerId := window setInterval: [ self update ] every: 75.
875 !
877 update
878 | lpCopy |
879 lpCopy := livePlayers copy.
880 lpCopy do: [ :each |
881 | l |
882 l := each location.
883 self at: l put: each move.
884 skin drawField: self on: context at: l ].
885 lpCopy do: [ :each |
886 | l other |
887 l := each location.
888 (self isFreeAt: l)
889 ifTrue: [ self at: l put: each headSegment. ]
890 ifFalse: [
891 self killPlayer: each.
892 "Check for head-to-head collision."
893 other := self playerWithHeadAt: l.
894 (other isNil | (other = each))
895 ifFalse: [
896 self killPlayer: other.
897 self at: l put: nil. ]]].
898 livePlayers do: [ :each |
899 skin drawField: self on: context at: each location ].
900 self isGameFinished ifTrue: [ ^ self endGame ].
901 livePlayers do: #compute.
902 !
904 updateLiveColors
905 liveColors := Dictionary new.
906 livePlayers do: [ :each |
907 liveColors
908 at: each color
909 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
910 ifAbsent: [ liveColors at: each color put: 1 ]].
911 ! !
913 !TronField methodsFor: 'rendering'!
915 renderOnSilk: aSilk
916 | backgroundCanvas |
918 backgroundCanvas := (self renderCanvasOn: aSilk) element.
920 canvas := (self renderCanvasOn: aSilk) element.
921 context := canvas getContext: '2d'.
923 collider := TronCollider new
924 canvas: (self renderCanvasOn: aSilk) element;
925 skin: skin.
927 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
928 ! !
930 !TronField methodsFor: 'starting'!
932 start: anArrayOfPlayers
933 players := anArrayOfPlayers.
934 livePlayers := players copy.
935 self updateLiveColors.
936 colors := liveColors.
937 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
938 context clearRect: 0 y: 0 w: canvas width h: canvas height.
939 self locatePlayers.
940 players do: [ :each | each field: self; reset; compute ].
941 players do: [ :each |
942 self at: each location put: each headSegment.
943 skin drawField: self on: context at: each location ].
944 self startIfAllReady.
945 ! !
947 !TronField methodsFor: 'stopping'!
949 stopTimer
950 timerId ifNotNil: [
951 window clearInterval: timerId.
952 timerId := nil ]
953 ! !
955 Object subclass: #TronPlayer
956 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
957 package: 'Serpentron'!
959 !TronPlayer methodsFor: 'accessing'!
961 color
962 ^ color
963 !
965 color: anObject
966 color := anObject.
967 onColorChange ifNotNil: #value.
968 !
970 controller
971 ^ controller
972 !
974 controller: aTronController
975 aTronController player: self.
976 controller := aTronController.
977 onControllerChange ifNotNil: #value.
978 !
980 direction
981 ^ direction
982 !
984 enabled: aBoolean
985 enabled := aBoolean.
986 onEnabledChange ifNotNil: #value.
987 !
989 field: aField
990 controller field: aField
991 !
993 headSegment
994 ^ segments at: direction
995 !
997 isEnabled
998 ^ enabled
999 !
1001 isFirstMove
1002 ^ moved not
1005 isReady
1006 ^ nextDirection notNil
1009 location
1010 ^ location
1013 location: anObject
1014 location := anObject
1017 name
1018 ^ name
1021 name: anObject
1022 name := anObject.
1025 nextDirection
1026 ^ nextDirection
1029 nextDirection: aPoint
1030 moved ifFalse: [ direction := aPoint ].
1031 nextDirection := aPoint
1032 ! !
1034 !TronPlayer methodsFor: 'event handling'!
1036 keyDown: keyCode
1037 ^ controller keyDown: keyCode
1038 ! !
1040 !TronPlayer methodsFor: 'initialization'!
1042 initialize
1043 super initialize.
1044 enabled := true.
1045 self reset; initializeSegments.
1048 reset
1049 direction := 0 @ -1.
1050 nextDirection := nil.
1051 moved := false.
1052 controller ifNotNil: [ controller reset ].
1053 ! !
1055 !TronPlayer methodsFor: 'observing'!
1057 onColorChange: aBlock
1058 onColorChange := aBlock.
1061 onControllerChange: aBlock
1062 onControllerChange := aBlock.
1065 onEnabledChange: aBlock
1066 onEnabledChange := aBlock.
1067 ! !
1069 !TronPlayer methodsFor: 'private'!
1071 initializeSegments
1072 segments := Dictionary new.
1073 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1074 segments
1075 at: to
1076 put: (TronHead new
1077 player: self;
1078 sprite: 'head', toName;
1079 direction: to);
1080 at: {to. to}
1081 put: (TronSegment new
1082 player: self;
1083 sprite: 'body', toName).
1084 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1085 (from x = to x) | (from y = to y) ifFalse: [
1086 segments
1087 at: {from. to}
1088 put: (TronSegment new
1089 player: self;
1090 sprite: 'body', fromName, 'To', toName) ]]].
1091 ! !
1093 !TronPlayer methodsFor: 'updating'!
1095 compute
1096 controller compute
1099 move
1100 | segment |
1101 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1102 direction := nextDirection.
1103 location := location + direction.
1104 moved := true.
1105 ^ segment
1106 ! !
1108 TronPlayer class instanceVariableNames: 'directions directionNames'!
1110 !TronPlayer class methodsFor: 'initialization'!
1112 directionNames
1113 ^ directionNames
1116 directions
1117 ^ directions
1120 initialize
1121 super initialize.
1122 directionNames := Dictionary new
1123 at: 0 @ -1 put: 'North';
1124 at: 1 @ 0 put: 'East';
1125 at: 0 @ 1 put: 'South';
1126 at: -1 @ 0 put: 'West';
1127 yourself.
1128 directions := directionNames keys.
1129 ! !
1131 Object subclass: #TronSegment
1132 instanceVariableNames: 'player sprite'
1133 package: 'Serpentron'!
1135 !TronSegment methodsFor: 'accessing'!
1137 color
1138 ^ player color
1141 isHead
1142 ^ false
1145 player
1146 ^ player
1149 player: anObject
1150 player := anObject
1153 sprite
1154 ^ sprite
1157 sprite: anObject
1158 sprite := anObject
1159 ! !
1161 TronSegment subclass: #TronHead
1162 instanceVariableNames: 'direction'
1163 package: 'Serpentron'!
1165 !TronHead methodsFor: 'accessing'!
1167 direction
1168 ^ direction
1171 direction: anObject
1172 direction := anObject
1175 isHead
1176 ^ true
1177 ! !
1179 Object subclass: #TronSkin
1180 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1181 package: 'Serpentron'!
1183 !TronSkin methodsFor: 'accessing'!
1185 tileSize
1186 ^ tileSize
1187 ! !
1189 !TronSkin methodsFor: 'drawing'!
1191 drawBackgroundOn: aContext at: aPoint
1192 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1193 drawTile: #backgroundTile on: aContext at: aPoint
1196 drawBackgroundOn: aContext from: nwPoint to: sePoint
1197 nwPoint y to: sePoint y do: [ :row |
1198 nwPoint x to: sePoint x do: [ :col |
1199 self drawBackgroundOn: aContext at: col @ row ]]
1202 drawField: aField on: aContext at: aPoint
1203 | x y segment |
1205 x := (aPoint - 1) x * tileSize.
1206 y := (aPoint - 1) y * tileSize.
1208 aContext
1209 clearRect: x and: y and: tileSize and: tileSize.
1211 segment := (aField at: aPoint) ifNil: [ ^ self ].
1213 self
1214 drawTile: segment sprite
1215 offset: maskOffset
1216 on: aContext
1217 at: aPoint.
1218 aContext
1219 globalCompositeOperation: 'source-atop';
1220 fillStyle: segment color;
1221 fillRect: x and: y and: tileSize and: tileSize.
1222 self drawTile: segment sprite on: aContext at: aPoint.
1224 aContext globalCompositeOperation: 'source-over'.
1227 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1228 aContext drawImage: skinImage
1229 sx: sourcePoint x
1230 sy: sourcePoint y
1231 sw: tileSize
1232 sh: tileSize
1233 dx: destinationPoint x
1234 dy: destinationPoint y
1235 dw: tileSize
1236 dh: tileSize
1239 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1240 self
1241 drawSkinImageOn: aContext
1242 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1243 destination: (targetPoint - 1) * tileSize
1246 drawTile: aSymbol on: aContext at: aPoint
1247 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1248 ! !
1250 !TronSkin methodsFor: 'initialization'!
1252 initialize
1253 super initialize.
1254 skinMap := Dictionary new
1255 at: #background put: 0@0;
1256 at: #backgroundBottomRight put: 49@34;
1257 at: #backgroundTile put: 0@35;
1258 at: #headNorth put: 1@35;
1259 at: #headEast put: 2@35;
1260 at: #headSouth put: 3@35;
1261 at: #headWest put: 4@35;
1262 at: #bodyNorth put: 5@35;
1263 at: #bodyEast put: 6@35;
1264 at: #bodySouth put: 7@35;
1265 at: #bodyWest put: 8@35;
1266 at: #bodySouthToEast put: 9@35;
1267 at: #bodyWestToNorth put: 9@35;
1268 at: #bodyNorthToEast put: 10@35;
1269 at: #bodyWestToSouth put: 10@35;
1270 at: #bodyNorthToWest put: 11@35;
1271 at: #bodyEastToSouth put: 11@35;
1272 at: #bodySouthToWest put: 12@35;
1273 at: #bodyEastToNorth put: 12@35;
1274 yourself.
1275 maskOffset := 12 @ 0.
1276 skinImage := document createElement: 'img'
1277 ! !
1279 !TronSkin methodsFor: 'loading'!
1281 load: url andDo: aBlock
1282 skinImage onload: [
1283 tileSize := skinImage width / 50.
1284 aBlock value ].
1285 skinImage src: url.
1286 ! !
1288 !Point methodsFor: '*Serpentron'!
1290 rotate90ccw
1291 ^ self y @ self x negated
1294 rotate90cw
1295 ^ self y negated @ self x
1296 ! !