view src/Serpentron.st @ 7:66d8fd6a64e8

Browser compatibility check.
author Mikhail Kryshen <mikhail@kryshen.net>
date Wed, 23 Mar 2016 21:34:08 +0300
parents c02b5a225a10
children b32efce2d860
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 validatePlayerColor: aPlayer
293 | enabledPlayers otherPlayers freeColor |
294 enabledPlayers := players select: #isEnabled.
295 ((enabledPlayers collect: #color) asSet size > 1)
296 ifTrue: [ ^ self ].
297 freeColor := playerColors detect: [ :each | each ~= enabledPlayers anyOne color ].
298 otherPlayers := players select: [ :each | each ~~ aPlayer ].
299 (otherPlayers
300 detect: [ :each | each isEnabled not ]
301 ifNone: [ otherPlayers first ])
302 enabled: true;
303 color: freeColor.
304 !
306 winnerDOM: color
307 | names team message |
308 team := field players select: [ :each | each color = color ].
309 names := ''.
310 (team collect: #name)
311 do: [ :each | names := names, each]
312 separatedBy: [ names := names, ' and ' ].
313 message := Silk SPAN.
314 (message SPAN: {#class -> 'player-color'. names})
315 element style color: color.
316 ^ message
317 ! !
319 !Serpentron methodsFor: 'rendering'!
321 augmentPage
322 Serpentron isCompatibleBrowser ifFalse: [
323 '#serpentron' asSilk resetContents
324 << 'Your browser is not supported.'
325 << Silk BR
326 << 'Please use a modern browser to run the game.'.
327 ^ self ].
328 '#serpentron' asSilk resetContents << 'Loading...'.
329 skin
330 load: 'resources/skin.png'
331 andDo: [ '#serpentron' asSilk resetContents << self ]
332 !
334 renderOnSilk: aSilk
335 | scale container width height |
336 scale := 1.
337 skin tileSize >= 20
338 ifTrue: [ scale := 0.5 ].
339 skin tileSize < 10
340 ifTrue: [ scale := 2].
342 width := field fieldSize x * skin tileSize * scale.
343 height := field fieldSize y * skin tileSize * scale.
345 aSilk element style
346 width: width asString , 'px'.
348 (aSilk DIV: {#id -> 'title'})
349 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
350 #target -> '_top'.
351 'Serpentron'});
352 SPAN: {#id -> 'status'. '1.0-beta2'}.
354 container := aSilk DIV: {#id -> 'field'}.
355 container element style
356 width: width asString , 'px';
357 height: height asString , 'px'.
359 container << field.
360 self renderStartScreenOn: container.
362 (container DIV: {#id -> 'message'. #class -> 'hidden'})
363 element style
364 margin: '0 auto'.
366 aSilk << container.
367 ! !
369 Serpentron class instanceVariableNames: 'Instance'!
371 !Serpentron class methodsFor: 'compatibility'!
373 isCompatibleBrowser
374 "No reason to polyfill requestAnimationFrame
375 or use vendor prefixes as browsers that do not have it
376 will likely have other incompatibilities."
377 < return window.requestAnimationFrame && true || false >
378 ! !
380 !Serpentron class methodsFor: 'starting'!
382 start
383 (Instance := self new) augmentPage
384 ! !
386 Object subclass: #TronCollider
387 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
388 package: 'Serpentron'!
389 !TronCollider commentStamp!
390 Particle system for collision effect.!
392 !TronCollider methodsFor: 'accessing'!
394 canvas: aCanvas
395 canvas := aCanvas.
396 context := canvas getContext: '2d'.
397 !
399 skin: aTronSkin
400 skin := aTronSkin
401 ! !
403 !TronCollider methodsFor: 'initialization'!
405 initialize
406 super initialize.
407 particlesPerCollision := 200.
408 particlePool := Queue new.
409 particlesPerCollision * 2 + 10 timesRepeat: [
410 particlePool nextPut: TronColliderParticle new ].
411 particles := OrderedCollection new
412 ! !
414 !TronCollider methodsFor: 'private'!
416 renderFrame: timestamp
417 | liveParticles delta |
418 delta := lastFrameTime
419 ifNil: [ 0 ]
420 ifNotNil: [ timestamp - lastFrameTime ].
421 lastFrameTime := timestamp.
422 "Transcript show: 'renderFrame: ';
423 show: timestamp;
424 show: '; ';
425 show: delta;
426 cr."
427 liveParticles := OrderedCollection new.
428 context clearRect: 0 y: 0 w: canvas width h: canvas height.
429 particles do: [ :each |
430 each
431 update: delta;
432 drawOn: context tileSize: skin tileSize.
433 each
434 ifAlive: [ liveParticles add: each ]
435 ifNotAlive: [ particlePool nextPut: each ] ].
436 particles := liveParticles.
437 animationRequest := particles
438 ifEmpty: [ nil ]
439 ifNotEmpty: [
440 window
441 requestAnimationFrame: [ :ts |
442 self renderFrame: ts ]
443 on: canvas ]
444 !
446 startAnimation
447 animationRequest ifNotNil: [ ^ self ].
448 lastFrameTime := nil.
449 animationRequest := window
450 requestAnimationFrame: [ :ts | self renderFrame: ts ]
451 on: canvas.
452 ! !
454 !TronCollider methodsFor: 'starting'!
456 animateCollisionFor: aPlayer
457 | particle |
458 particlesPerCollision timesRepeat: [
459 particles add:
460 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
461 resetPosition:
462 aPlayer location - 0.5 - (aPlayer direction / 2)
463 color: (Math random < 0.5
464 ifTrue: [ aPlayer color ]
465 ifFalse: [ '#ff3000' ])) ].
466 self startAnimation.
467 ! !
469 Object subclass: #TronColliderParticle
470 instanceVariableNames: 'color size velocity position alpha decay'
471 package: 'Serpentron'!
473 !TronColliderParticle methodsFor: 'accessing'!
475 ifAlive: aliveBlock ifNotAlive: notAliveBlock
476 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
477 !
479 resetPosition: positionPoint color: aString
480 position := positionPoint.
481 color := aString.
482 "Particles return to the pool as soon as they die.
483 Randomize on each reset or the pool will end up sotrted by particles' life time."
484 alpha := 1 - (Math random * 0.5).
485 decay := 0.997 - (Math random * 0.01)
486 ! !
488 !TronColliderParticle methodsFor: 'drawing'!
490 drawOn: aContext tileSize: tileSize
491 aContext
492 globalAlpha: alpha;
493 fillStyle: color;
494 fillRect: (position x - (size / 2)) * tileSize
495 y: (position y - (size / 2)) * tileSize
496 w: size * tileSize
497 h: size * tileSize.
498 ! !
500 !TronColliderParticle methodsFor: 'initialization'!
502 initialize
503 super initialize.
504 size := (Math random * 0.3 + 0.7).
505 velocity := (Math random - 0.5) @ (Math random - 0.5).
506 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
507 ! !
509 !TronColliderParticle methodsFor: 'updating'!
511 update: delta
512 position := position + (velocity * delta).
513 alpha := alpha * (decay raisedTo: delta).
514 ! !
516 Object subclass: #TronController
517 instanceVariableNames: 'field player'
518 package: 'Serpentron'!
519 !TronController commentStamp!
520 Abstract superclass for controlling input devices and algorithms.!
522 !TronController methodsFor: 'accessing'!
524 field: aTronField
525 field := aTronField
526 !
528 name
529 self subclassResponsibility.
530 !
532 nextDirection: aPoint
533 "Do not turn back."
534 player nextDirection = (aPoint * (-1 @ -1))
535 ifTrue: [ ^ self ].
536 player nextDirection: aPoint.
537 !
539 player: aTronPlayer
540 player := aTronPlayer
541 ! !
543 !TronController methodsFor: 'computing'!
545 compute
546 !
548 isAtDecisionPoint
549 | loc dir turn |
550 player isFirstMove ifTrue: [ ^ true ].
551 dir := player nextDirection.
552 loc := player location.
553 (field isFreeAt: loc + dir)
554 ifFalse: [ ^ true ].
555 turn := dir rotate90ccw.
556 ((field isFreeAt: loc + turn)
557 and: [ (field isFreeAt: loc + dir + turn) not ])
558 ifTrue: [ ^ true ].
559 turn := dir rotate90cw.
560 ((field isFreeAt: loc + turn)
561 and: [ (field isFreeAt: loc + dir + turn) not ])
562 ifTrue: [ ^ true ].
563 ^ false
564 ! !
566 !TronController methodsFor: 'event handling'!
568 keyDown: keyCode
569 ^ false
570 ! !
572 !TronController methodsFor: 'initialization'!
574 reset
575 ! !
577 TronController class instanceVariableNames: 'directions'!
579 TronController subclass: #TronComputerController1
580 instanceVariableNames: 'weight aggressiveness'
581 package: 'Serpentron'!
583 !TronComputerController1 methodsFor: 'accessing'!
585 name
586 ^ 'Computer'
587 ! !
589 !TronComputerController1 methodsFor: 'computing'!
591 compute
592 | best bestDirection |
593 "player isFirstMove
594 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
595 (self isAtDecisionPoint
596 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
597 ifFalse: [ ^ self ].
598 aggressiveness := 200 atRandom.
599 best := 0.
600 TronPlayer directions do: [ :each |
601 weight := 0.
602 self scan: each.
603 weight > best
604 ifTrue: [
605 best := weight.
606 bestDirection := each ]].
607 bestDirection ifNil: [ ^ self ].
608 self nextDirection: bestDirection.
609 ! !
611 !TronComputerController1 methodsFor: 'private'!
613 extend: directionPoint from: nwPoint to: sePoint
614 | point scanDir |
615 scanDir := directionPoint y abs @ directionPoint x abs.
616 point := Point
617 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
618 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
619 [ point <= sePoint ]
620 whileTrue: [
621 weight := weight + 1.
622 (field isFreeAt: point)
623 ifFalse: [
624 (self isEnemyHeadAt: point)
625 ifTrue: [ weight := weight + aggressiveness ]
626 ifFalse: [ ^ false ] ].
627 point := point + scanDir ].
628 ^ true
629 !
631 isEnemyHeadAt: aPoint
632 (field playerWithHeadAt: aPoint)
633 ifNotNil: [ :p | ^ p color ~= player color ].
634 ^ false
635 !
637 scan: directionPoint
638 | nw se nextNW nextSE directions scanDir |
639 nw := se := player location + directionPoint.
640 (field isFreeAt: nw) ifFalse: [ ^ self ].
641 directions := TronPlayer directions copy
642 remove: directionPoint * -1;
643 yourself.
644 [ scanDir := directions atRandom.
645 scanDir <= 0 asPoint
646 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
647 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
648 (self extend: scanDir from: nextNW to: nextSE)
649 ifTrue: [ nw := nextNW. se := nextSE ]
650 ifFalse: [ directions remove: scanDir ].
651 directions isEmpty not
652 ] whileTrue.
653 ! !
655 TronController subclass: #TronKeyboardController
656 instanceVariableNames: 'keyMap name'
657 package: 'Serpentron'!
659 !TronKeyboardController methodsFor: 'accessing'!
661 keyMap: aDictionary name: aString
662 keyMap := aDictionary.
663 name := 'Keyboard: ', aString.
664 !
666 name
667 ^ name
668 ! !
670 !TronKeyboardController methodsFor: 'event handling'!
672 keyDown: keyCode
673 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
674 ^ true
675 ! !
677 TronKeyboardController class instanceVariableNames: 'keyMaps'!
679 TronController subclass: #TronRandomController
680 instanceVariableNames: ''
681 package: 'Serpentron'!
683 !TronRandomController methodsFor: 'accessing'!
685 name
686 ^ 'Computer (random)'
687 ! !
689 !TronRandomController methodsFor: 'computing'!
691 compute
692 | dirs |
693 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
694 ifFalse: [ ^ self ].
695 dirs := TronPlayer directions
696 select: [ :each | field isFreeAt: player location + each ].
697 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
698 ! !
700 Object subclass: #TronField
701 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
702 package: 'Serpentron'!
703 !TronField commentStamp!
704 The game field. Provides game logic and rendering.!
706 !TronField methodsFor: 'accessing'!
708 at: aPoint
709 ^ (matrix at: aPoint y) at: aPoint x
710 !
712 at: aPoint ifAbsent: aBlock
713 ^ matrix
714 at: aPoint y
715 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
716 ifAbsent: aBlock
717 !
719 at: aPoint put: aSegment
720 (matrix at: aPoint y) at: aPoint x put: aSegment
721 !
723 colors
724 ^ colors
725 !
727 fieldSize
728 ^ size
729 !
731 isFreeAt: aPoint
732 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
733 !
735 isGameFinished
736 ^ liveColors size < 2
737 !
739 liveColors
740 ^ liveColors
741 !
743 playerWithHeadAt: aPoint
744 | s |
745 s := self at: aPoint ifAbsent: [ ^ nil ].
746 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
747 !
749 players
750 ^ players
751 !
753 skin
754 ^ skin
755 !
757 skin: aTronSkin
758 skin := aTronSkin
759 !
761 winningColorIfNone: aBlock
762 ^ (liveColors size = 1)
763 ifTrue: [ liveColors keys anyOne ]
764 ifFalse: aBlock.
765 ! !
767 !TronField methodsFor: 'event handling'!
769 keyDown: event
770 "Check all players rather than livePlayers
771 so that preventDefault is called for all used keys."
772 (players anySatisfy: [ :each | each keyDown: event keyCode ])
773 ifTrue: [ event preventDefault ].
774 timerId ifNil: [ self startIfAllReady ].
775 ! !
777 !TronField methodsFor: 'initialization'!
779 initialize
780 super initialize.
781 livePlayers := players := #().
782 size := 50 @ 35
783 ! !
785 !TronField methodsFor: 'observing'!
787 onEndGame: aBlock
788 onEndGame := aBlock
789 ! !
791 !TronField methodsFor: 'private'!
793 endGame
794 self stopTimer.
795 livePlayers := #().
796 onEndGame ifNotNil: #value.
797 !
799 killPlayer: aPlayer
800 collider animateCollisionFor: aPlayer.
801 livePlayers remove: aPlayer.
802 self updateLiveColors.
803 !
805 locatePlayers
806 players size = 2 ifTrue: [
807 (players at: 1) location: 16 @ 18.
808 (players at: 2) location: 35 @ 18.
809 ^ self ].
810 players size = 3 ifTrue: [
811 (players at: 1) location: 18 @ 13.
812 (players at: 2) location: 18 @ 23.
813 (players at: 3) location: 33 @ 18.
814 ^ self ].
815 players size = 4 ifTrue: [
816 (players at: 1) location: 16 @ 11.
817 (players at: 2) location: 35 @ 11.
818 (players at: 3) location: 16 @ 25.
819 (players at: 4) location: 35 @ 25.
820 ^ self ].
821 self error: 'Invalid number of players.'
822 !
824 renderCanvasOn: aSilk
825 ^ aSilk CANVAS: {
826 #width -> (size x * skin tileSize).
827 #height -> (size y * skin tileSize)}.
828 !
830 startIfAllReady
831 (livePlayers isEmpty not
832 and: [ livePlayers allSatisfy: #isReady ])
833 ifTrue: [ self startTimer ]
834 !
836 startTimer
837 timerId ifNotNil: [ self error: 'Timer already running.' ].
838 timerId := window setInterval: [ self update ] every: 75.
839 !
841 update
842 | lpCopy |
843 lpCopy := livePlayers copy.
844 lpCopy do: [ :each |
845 | l |
846 l := each location.
847 self at: l put: each move.
848 skin drawField: self on: context at: l ].
849 lpCopy do: [ :each |
850 | l other |
851 l := each location.
852 (self isFreeAt: l)
853 ifTrue: [ self at: l put: each headSegment. ]
854 ifFalse: [
855 self killPlayer: each.
856 "Check for head-to-head collision."
857 other := self playerWithHeadAt: l.
858 (other isNil | (other = each))
859 ifFalse: [
860 self killPlayer: other.
861 self at: l put: nil. ]]].
862 livePlayers do: [ :each |
863 skin drawField: self on: context at: each location ].
864 self isGameFinished ifTrue: [ ^ self endGame ].
865 livePlayers do: #compute.
866 !
868 updateLiveColors
869 liveColors := Dictionary new.
870 livePlayers do: [ :each |
871 liveColors
872 at: each color
873 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
874 ifAbsent: [ liveColors at: each color put: 1 ]].
875 ! !
877 !TronField methodsFor: 'rendering'!
879 renderOnSilk: aSilk
880 | backgroundCanvas |
882 backgroundCanvas := (self renderCanvasOn: aSilk) element.
884 canvas := (self renderCanvasOn: aSilk) element.
885 context := canvas getContext: '2d'.
887 collider := TronCollider new
888 canvas: (self renderCanvasOn: aSilk) element;
889 skin: skin.
891 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
892 ! !
894 !TronField methodsFor: 'starting'!
896 start: anArrayOfPlayers
897 players := anArrayOfPlayers.
898 livePlayers := players copy.
899 self updateLiveColors.
900 colors := liveColors.
901 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
902 context clearRect: 0 y: 0 w: canvas width h: canvas height.
903 self locatePlayers.
904 players do: [ :each | each field: self; reset; compute ].
905 players do: [ :each |
906 self at: each location put: each headSegment.
907 skin drawField: self on: context at: each location ].
908 self startIfAllReady.
909 ! !
911 !TronField methodsFor: 'stopping'!
913 stopTimer
914 timerId ifNotNil: [
915 window clearInterval: timerId.
916 timerId := nil ]
917 ! !
919 Object subclass: #TronPlayer
920 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
921 package: 'Serpentron'!
923 !TronPlayer methodsFor: 'accessing'!
925 color
926 ^ color
927 !
929 color: anObject
930 color := anObject.
931 onColorChange ifNotNil: #value.
932 !
934 controller
935 ^ controller
936 !
938 controller: aTronController
939 aTronController player: self.
940 controller := aTronController.
941 onControllerChange ifNotNil: #value.
942 !
944 direction
945 ^ direction
946 !
948 enabled: aBoolean
949 enabled := aBoolean.
950 onEnabledChange ifNotNil: #value.
951 !
953 field: aField
954 controller field: aField
955 !
957 headSegment
958 ^ segments at: direction
959 !
961 isEnabled
962 ^ enabled
963 !
965 isFirstMove
966 ^ moved not
967 !
969 isReady
970 ^ nextDirection notNil
971 !
973 location
974 ^ location
975 !
977 location: anObject
978 location := anObject
979 !
981 name
982 ^ name
983 !
985 name: anObject
986 name := anObject.
987 !
989 nextDirection
990 ^ nextDirection
991 !
993 nextDirection: aPoint
994 moved ifFalse: [ direction := aPoint ].
995 nextDirection := aPoint
996 ! !
998 !TronPlayer methodsFor: 'event handling'!
1000 keyDown: keyCode
1001 ^ controller keyDown: keyCode
1002 ! !
1004 !TronPlayer methodsFor: 'initialization'!
1006 initialize
1007 super initialize.
1008 enabled := true.
1009 self reset; initializeSegments.
1012 reset
1013 direction := 0 @ -1.
1014 nextDirection := nil.
1015 moved := false.
1016 controller ifNotNil: [ controller reset ].
1017 ! !
1019 !TronPlayer methodsFor: 'observing'!
1021 onColorChange: aBlock
1022 onColorChange := aBlock.
1025 onControllerChange: aBlock
1026 onControllerChange := aBlock.
1029 onEnabledChange: aBlock
1030 onEnabledChange := aBlock.
1031 ! !
1033 !TronPlayer methodsFor: 'private'!
1035 initializeSegments
1036 segments := Dictionary new.
1037 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1038 segments
1039 at: to
1040 put: (TronHead new
1041 player: self;
1042 sprite: 'head', toName;
1043 direction: to);
1044 at: {to. to}
1045 put: (TronSegment new
1046 player: self;
1047 sprite: 'body', toName).
1048 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1049 (from x = to x) | (from y = to y) ifFalse: [
1050 segments
1051 at: {from. to}
1052 put: (TronSegment new
1053 player: self;
1054 sprite: 'body', fromName, 'To', toName) ]]].
1055 ! !
1057 !TronPlayer methodsFor: 'updating'!
1059 compute
1060 controller compute
1063 move
1064 | segment |
1065 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1066 direction := nextDirection.
1067 location := location + direction.
1068 moved := true.
1069 ^ segment
1070 ! !
1072 TronPlayer class instanceVariableNames: 'directions directionNames'!
1074 !TronPlayer class methodsFor: 'initialization'!
1076 directionNames
1077 ^ directionNames
1080 directions
1081 ^ directions
1084 initialize
1085 super initialize.
1086 directionNames := Dictionary new
1087 at: 0 @ -1 put: 'North';
1088 at: 1 @ 0 put: 'East';
1089 at: 0 @ 1 put: 'South';
1090 at: -1 @ 0 put: 'West';
1091 yourself.
1092 directions := directionNames keys.
1093 ! !
1095 Object subclass: #TronSegment
1096 instanceVariableNames: 'player sprite'
1097 package: 'Serpentron'!
1099 !TronSegment methodsFor: 'accessing'!
1101 color
1102 ^ player color
1105 isHead
1106 ^ false
1109 player
1110 ^ player
1113 player: anObject
1114 player := anObject
1117 sprite
1118 ^ sprite
1121 sprite: anObject
1122 sprite := anObject
1123 ! !
1125 TronSegment subclass: #TronHead
1126 instanceVariableNames: 'direction'
1127 package: 'Serpentron'!
1129 !TronHead methodsFor: 'accessing'!
1131 direction
1132 ^ direction
1135 direction: anObject
1136 direction := anObject
1139 isHead
1140 ^ true
1141 ! !
1143 Object subclass: #TronSkin
1144 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1145 package: 'Serpentron'!
1147 !TronSkin methodsFor: 'accessing'!
1149 tileSize
1150 ^ tileSize
1151 ! !
1153 !TronSkin methodsFor: 'drawing'!
1155 drawBackgroundOn: aContext at: aPoint
1156 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1157 drawTile: #backgroundTile on: aContext at: aPoint
1160 drawBackgroundOn: aContext from: nwPoint to: sePoint
1161 nwPoint y to: sePoint y do: [ :row |
1162 nwPoint x to: sePoint x do: [ :col |
1163 self drawBackgroundOn: aContext at: col @ row ]]
1166 drawField: aField on: aContext at: aPoint
1167 | x y segment |
1169 x := (aPoint - 1) x * tileSize.
1170 y := (aPoint - 1) y * tileSize.
1172 aContext
1173 clearRect: x and: y and: tileSize and: tileSize.
1175 segment := (aField at: aPoint) ifNil: [ ^ self ].
1177 self
1178 drawTile: segment sprite
1179 offset: maskOffset
1180 on: aContext
1181 at: aPoint.
1182 aContext
1183 globalCompositeOperation: 'source-atop';
1184 fillStyle: segment color;
1185 fillRect: x and: y and: tileSize and: tileSize.
1186 self drawTile: segment sprite on: aContext at: aPoint.
1188 aContext globalCompositeOperation: 'source-over'.
1191 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1192 aContext drawImage: skinImage
1193 sx: sourcePoint x
1194 sy: sourcePoint y
1195 sw: tileSize
1196 sh: tileSize
1197 dx: destinationPoint x
1198 dy: destinationPoint y
1199 dw: tileSize
1200 dh: tileSize
1203 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1204 self
1205 drawSkinImageOn: aContext
1206 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1207 destination: (targetPoint - 1) * tileSize
1210 drawTile: aSymbol on: aContext at: aPoint
1211 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1212 ! !
1214 !TronSkin methodsFor: 'initialization'!
1216 initialize
1217 super initialize.
1218 skinMap := Dictionary new
1219 at: #background put: 0@0;
1220 at: #backgroundBottomRight put: 49@34;
1221 at: #backgroundTile put: 0@35;
1222 at: #headNorth put: 1@35;
1223 at: #headEast put: 2@35;
1224 at: #headSouth put: 3@35;
1225 at: #headWest put: 4@35;
1226 at: #bodyNorth put: 5@35;
1227 at: #bodyEast put: 6@35;
1228 at: #bodySouth put: 7@35;
1229 at: #bodyWest put: 8@35;
1230 at: #bodySouthToEast put: 9@35;
1231 at: #bodyWestToNorth put: 9@35;
1232 at: #bodyNorthToEast put: 10@35;
1233 at: #bodyWestToSouth put: 10@35;
1234 at: #bodyNorthToWest put: 11@35;
1235 at: #bodyEastToSouth put: 11@35;
1236 at: #bodySouthToWest put: 12@35;
1237 at: #bodyEastToNorth put: 12@35;
1238 yourself.
1239 maskOffset := 12 @ 0.
1240 skinImage := document createElement: 'img'
1241 ! !
1243 !TronSkin methodsFor: 'loading'!
1245 load: url andDo: aBlock
1246 skinImage onload: [
1247 tileSize := skinImage width / 50.
1248 aBlock value ].
1249 skinImage src: url.
1250 ! !
1252 !Point methodsFor: '*Serpentron'!
1254 rotate90ccw
1255 ^ self y @ self x negated
1258 rotate90cw
1259 ^ self y negated @ self x
1260 ! !