view src/Serpentron.st @ 8:b32efce2d860

Automatically adjust size to fit the window.
author Mikhail Kryshen <mikhail@kryshen.net>
date Wed, 23 Mar 2016 23:56:26 +0300
parents 66d8fd6a64e8
children 8c7e910cb328
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 container width height |
353 (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 container := aSilk DIV: {#id -> 'field'}.
361 self updateSize.
362 window onresize: [ self updateSize ].
364 container << field.
365 self renderStartScreenOn: container.
367 (container DIV: {#id -> 'message'. #class -> 'hidden'})
368 element style
369 margin: '0 auto'.
370 ! !
372 Serpentron class instanceVariableNames: 'Instance'!
374 !Serpentron class methodsFor: 'compatibility'!
376 isCompatibleBrowser
377 "No reason to polyfill requestAnimationFrame
378 or use vendor prefixes as browsers that do not have it
379 will likely have other incompatibilities."
380 < return window.requestAnimationFrame && true || false >
381 ! !
383 !Serpentron class methodsFor: 'starting'!
385 start
386 (Instance := self new) augmentPage
387 ! !
389 Object subclass: #TronCollider
390 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
391 package: 'Serpentron'!
392 !TronCollider commentStamp!
393 Particle system for collision effect.!
395 !TronCollider methodsFor: 'accessing'!
397 canvas: aCanvas
398 canvas := aCanvas.
399 context := canvas getContext: '2d'.
400 !
402 skin: aTronSkin
403 skin := aTronSkin
404 ! !
406 !TronCollider methodsFor: 'initialization'!
408 initialize
409 super initialize.
410 particlesPerCollision := 200.
411 particlePool := Queue new.
412 particlesPerCollision * 2 + 10 timesRepeat: [
413 particlePool nextPut: TronColliderParticle new ].
414 particles := OrderedCollection new
415 ! !
417 !TronCollider methodsFor: 'private'!
419 renderFrame: timestamp
420 | liveParticles delta |
421 delta := lastFrameTime
422 ifNil: [ 0 ]
423 ifNotNil: [ timestamp - lastFrameTime ].
424 lastFrameTime := timestamp.
425 "Transcript show: 'renderFrame: ';
426 show: timestamp;
427 show: '; ';
428 show: delta;
429 cr."
430 liveParticles := OrderedCollection new.
431 context clearRect: 0 y: 0 w: canvas width h: canvas height.
432 particles do: [ :each |
433 each
434 update: delta;
435 drawOn: context tileSize: skin tileSize.
436 each
437 ifAlive: [ liveParticles add: each ]
438 ifNotAlive: [ particlePool nextPut: each ] ].
439 particles := liveParticles.
440 animationRequest := particles
441 ifEmpty: [ nil ]
442 ifNotEmpty: [
443 window
444 requestAnimationFrame: [ :ts |
445 self renderFrame: ts ]
446 on: canvas ]
447 !
449 startAnimation
450 animationRequest ifNotNil: [ ^ self ].
451 lastFrameTime := nil.
452 animationRequest := window
453 requestAnimationFrame: [ :ts | self renderFrame: ts ]
454 on: canvas.
455 ! !
457 !TronCollider methodsFor: 'starting'!
459 animateCollisionFor: aPlayer
460 | particle |
461 particlesPerCollision timesRepeat: [
462 particles add:
463 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
464 resetPosition:
465 aPlayer location - 0.5 - (aPlayer direction / 2)
466 color: (Math random < 0.5
467 ifTrue: [ aPlayer color ]
468 ifFalse: [ '#ff3000' ])) ].
469 self startAnimation.
470 ! !
472 Object subclass: #TronColliderParticle
473 instanceVariableNames: 'color size velocity position alpha decay'
474 package: 'Serpentron'!
476 !TronColliderParticle methodsFor: 'accessing'!
478 ifAlive: aliveBlock ifNotAlive: notAliveBlock
479 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
480 !
482 resetPosition: positionPoint color: aString
483 position := positionPoint.
484 color := aString.
485 "Particles return to the pool as soon as they die.
486 Randomize on each reset or the pool will end up sotrted by particles' life time."
487 alpha := 1 - (Math random * 0.5).
488 decay := 0.997 - (Math random * 0.01)
489 ! !
491 !TronColliderParticle methodsFor: 'drawing'!
493 drawOn: aContext tileSize: tileSize
494 aContext
495 globalAlpha: alpha;
496 fillStyle: color;
497 fillRect: (position x - (size / 2)) * tileSize
498 y: (position y - (size / 2)) * tileSize
499 w: size * tileSize
500 h: size * tileSize.
501 ! !
503 !TronColliderParticle methodsFor: 'initialization'!
505 initialize
506 super initialize.
507 size := (Math random * 0.3 + 0.7).
508 velocity := (Math random - 0.5) @ (Math random - 0.5).
509 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
510 ! !
512 !TronColliderParticle methodsFor: 'updating'!
514 update: delta
515 position := position + (velocity * delta).
516 alpha := alpha * (decay raisedTo: delta).
517 ! !
519 Object subclass: #TronController
520 instanceVariableNames: 'field player'
521 package: 'Serpentron'!
522 !TronController commentStamp!
523 Abstract superclass for controlling input devices and algorithms.!
525 !TronController methodsFor: 'accessing'!
527 field: aTronField
528 field := aTronField
529 !
531 name
532 self subclassResponsibility.
533 !
535 nextDirection: aPoint
536 "Do not turn back."
537 player nextDirection = (aPoint * (-1 @ -1))
538 ifTrue: [ ^ self ].
539 player nextDirection: aPoint.
540 !
542 player: aTronPlayer
543 player := aTronPlayer
544 ! !
546 !TronController methodsFor: 'computing'!
548 compute
549 !
551 isAtDecisionPoint
552 | loc dir turn |
553 player isFirstMove ifTrue: [ ^ true ].
554 dir := player nextDirection.
555 loc := player location.
556 (field isFreeAt: loc + dir)
557 ifFalse: [ ^ true ].
558 turn := dir rotate90ccw.
559 ((field isFreeAt: loc + turn)
560 and: [ (field isFreeAt: loc + dir + turn) not ])
561 ifTrue: [ ^ true ].
562 turn := dir rotate90cw.
563 ((field isFreeAt: loc + turn)
564 and: [ (field isFreeAt: loc + dir + turn) not ])
565 ifTrue: [ ^ true ].
566 ^ false
567 ! !
569 !TronController methodsFor: 'event handling'!
571 keyDown: keyCode
572 ^ false
573 ! !
575 !TronController methodsFor: 'initialization'!
577 reset
578 ! !
580 TronController class instanceVariableNames: 'directions'!
582 TronController subclass: #TronComputerController1
583 instanceVariableNames: 'weight aggressiveness'
584 package: 'Serpentron'!
586 !TronComputerController1 methodsFor: 'accessing'!
588 name
589 ^ 'Computer'
590 ! !
592 !TronComputerController1 methodsFor: 'computing'!
594 compute
595 | best bestDirection |
596 "player isFirstMove
597 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
598 (self isAtDecisionPoint
599 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
600 ifFalse: [ ^ self ].
601 aggressiveness := 200 atRandom.
602 best := 0.
603 TronPlayer directions do: [ :each |
604 weight := 0.
605 self scan: each.
606 weight > best
607 ifTrue: [
608 best := weight.
609 bestDirection := each ]].
610 bestDirection ifNil: [ ^ self ].
611 self nextDirection: bestDirection.
612 ! !
614 !TronComputerController1 methodsFor: 'private'!
616 extend: directionPoint from: nwPoint to: sePoint
617 | point scanDir |
618 scanDir := directionPoint y abs @ directionPoint x abs.
619 point := Point
620 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
621 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
622 [ point <= sePoint ]
623 whileTrue: [
624 weight := weight + 1.
625 (field isFreeAt: point)
626 ifFalse: [
627 (self isEnemyHeadAt: point)
628 ifTrue: [ weight := weight + aggressiveness ]
629 ifFalse: [ ^ false ] ].
630 point := point + scanDir ].
631 ^ true
632 !
634 isEnemyHeadAt: aPoint
635 (field playerWithHeadAt: aPoint)
636 ifNotNil: [ :p | ^ p color ~= player color ].
637 ^ false
638 !
640 scan: directionPoint
641 | nw se nextNW nextSE directions scanDir |
642 nw := se := player location + directionPoint.
643 (field isFreeAt: nw) ifFalse: [ ^ self ].
644 directions := TronPlayer directions copy
645 remove: directionPoint * -1;
646 yourself.
647 [ scanDir := directions atRandom.
648 scanDir <= 0 asPoint
649 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
650 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
651 (self extend: scanDir from: nextNW to: nextSE)
652 ifTrue: [ nw := nextNW. se := nextSE ]
653 ifFalse: [ directions remove: scanDir ].
654 directions isEmpty not
655 ] whileTrue.
656 ! !
658 TronController subclass: #TronKeyboardController
659 instanceVariableNames: 'keyMap name'
660 package: 'Serpentron'!
662 !TronKeyboardController methodsFor: 'accessing'!
664 keyMap: aDictionary name: aString
665 keyMap := aDictionary.
666 name := 'Keyboard: ', aString.
667 !
669 name
670 ^ name
671 ! !
673 !TronKeyboardController methodsFor: 'event handling'!
675 keyDown: keyCode
676 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
677 ^ true
678 ! !
680 TronKeyboardController class instanceVariableNames: 'keyMaps'!
682 TronController subclass: #TronRandomController
683 instanceVariableNames: ''
684 package: 'Serpentron'!
686 !TronRandomController methodsFor: 'accessing'!
688 name
689 ^ 'Computer (random)'
690 ! !
692 !TronRandomController methodsFor: 'computing'!
694 compute
695 | dirs |
696 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
697 ifFalse: [ ^ self ].
698 dirs := TronPlayer directions
699 select: [ :each | field isFreeAt: player location + each ].
700 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
701 ! !
703 Object subclass: #TronField
704 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
705 package: 'Serpentron'!
706 !TronField commentStamp!
707 The game field. Provides game logic and rendering.!
709 !TronField methodsFor: 'accessing'!
711 at: aPoint
712 ^ (matrix at: aPoint y) at: aPoint x
713 !
715 at: aPoint ifAbsent: aBlock
716 ^ matrix
717 at: aPoint y
718 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
719 ifAbsent: aBlock
720 !
722 at: aPoint put: aSegment
723 (matrix at: aPoint y) at: aPoint x put: aSegment
724 !
726 colors
727 ^ colors
728 !
730 fieldSize
731 ^ size
732 !
734 isFreeAt: aPoint
735 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
736 !
738 isGameFinished
739 ^ liveColors size < 2
740 !
742 liveColors
743 ^ liveColors
744 !
746 playerWithHeadAt: aPoint
747 | s |
748 s := self at: aPoint ifAbsent: [ ^ nil ].
749 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
750 !
752 players
753 ^ players
754 !
756 skin
757 ^ skin
758 !
760 skin: aTronSkin
761 skin := aTronSkin
762 !
764 winningColorIfNone: aBlock
765 ^ (liveColors size = 1)
766 ifTrue: [ liveColors keys anyOne ]
767 ifFalse: aBlock.
768 ! !
770 !TronField methodsFor: 'event handling'!
772 keyDown: event
773 "Check all players rather than livePlayers
774 so that preventDefault is called for all used keys."
775 (players anySatisfy: [ :each | each keyDown: event keyCode ])
776 ifTrue: [ event preventDefault ].
777 timerId ifNil: [ self startIfAllReady ].
778 ! !
780 !TronField methodsFor: 'initialization'!
782 initialize
783 super initialize.
784 livePlayers := players := #().
785 size := 50 @ 35
786 ! !
788 !TronField methodsFor: 'observing'!
790 onEndGame: aBlock
791 onEndGame := aBlock
792 ! !
794 !TronField methodsFor: 'private'!
796 endGame
797 self stopTimer.
798 livePlayers := #().
799 onEndGame ifNotNil: #value.
800 !
802 killPlayer: aPlayer
803 collider animateCollisionFor: aPlayer.
804 livePlayers remove: aPlayer.
805 self updateLiveColors.
806 !
808 locatePlayers
809 players size = 2 ifTrue: [
810 (players at: 1) location: 16 @ 18.
811 (players at: 2) location: 35 @ 18.
812 ^ self ].
813 players size = 3 ifTrue: [
814 (players at: 1) location: 18 @ 13.
815 (players at: 2) location: 18 @ 23.
816 (players at: 3) location: 33 @ 18.
817 ^ self ].
818 players size = 4 ifTrue: [
819 (players at: 1) location: 16 @ 11.
820 (players at: 2) location: 35 @ 11.
821 (players at: 3) location: 16 @ 25.
822 (players at: 4) location: 35 @ 25.
823 ^ self ].
824 self error: 'Invalid number of players.'
825 !
827 renderCanvasOn: aSilk
828 ^ aSilk CANVAS: {
829 #width -> (size x * skin tileSize).
830 #height -> (size y * skin tileSize)}.
831 !
833 startIfAllReady
834 (livePlayers isEmpty not
835 and: [ livePlayers allSatisfy: #isReady ])
836 ifTrue: [ self startTimer ]
837 !
839 startTimer
840 timerId ifNotNil: [ self error: 'Timer already running.' ].
841 timerId := window setInterval: [ self update ] every: 75.
842 !
844 update
845 | lpCopy |
846 lpCopy := livePlayers copy.
847 lpCopy do: [ :each |
848 | l |
849 l := each location.
850 self at: l put: each move.
851 skin drawField: self on: context at: l ].
852 lpCopy do: [ :each |
853 | l other |
854 l := each location.
855 (self isFreeAt: l)
856 ifTrue: [ self at: l put: each headSegment. ]
857 ifFalse: [
858 self killPlayer: each.
859 "Check for head-to-head collision."
860 other := self playerWithHeadAt: l.
861 (other isNil | (other = each))
862 ifFalse: [
863 self killPlayer: other.
864 self at: l put: nil. ]]].
865 livePlayers do: [ :each |
866 skin drawField: self on: context at: each location ].
867 self isGameFinished ifTrue: [ ^ self endGame ].
868 livePlayers do: #compute.
869 !
871 updateLiveColors
872 liveColors := Dictionary new.
873 livePlayers do: [ :each |
874 liveColors
875 at: each color
876 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
877 ifAbsent: [ liveColors at: each color put: 1 ]].
878 ! !
880 !TronField methodsFor: 'rendering'!
882 renderOnSilk: aSilk
883 | backgroundCanvas |
885 backgroundCanvas := (self renderCanvasOn: aSilk) element.
887 canvas := (self renderCanvasOn: aSilk) element.
888 context := canvas getContext: '2d'.
890 collider := TronCollider new
891 canvas: (self renderCanvasOn: aSilk) element;
892 skin: skin.
894 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
895 ! !
897 !TronField methodsFor: 'starting'!
899 start: anArrayOfPlayers
900 players := anArrayOfPlayers.
901 livePlayers := players copy.
902 self updateLiveColors.
903 colors := liveColors.
904 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
905 context clearRect: 0 y: 0 w: canvas width h: canvas height.
906 self locatePlayers.
907 players do: [ :each | each field: self; reset; compute ].
908 players do: [ :each |
909 self at: each location put: each headSegment.
910 skin drawField: self on: context at: each location ].
911 self startIfAllReady.
912 ! !
914 !TronField methodsFor: 'stopping'!
916 stopTimer
917 timerId ifNotNil: [
918 window clearInterval: timerId.
919 timerId := nil ]
920 ! !
922 Object subclass: #TronPlayer
923 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
924 package: 'Serpentron'!
926 !TronPlayer methodsFor: 'accessing'!
928 color
929 ^ color
930 !
932 color: anObject
933 color := anObject.
934 onColorChange ifNotNil: #value.
935 !
937 controller
938 ^ controller
939 !
941 controller: aTronController
942 aTronController player: self.
943 controller := aTronController.
944 onControllerChange ifNotNil: #value.
945 !
947 direction
948 ^ direction
949 !
951 enabled: aBoolean
952 enabled := aBoolean.
953 onEnabledChange ifNotNil: #value.
954 !
956 field: aField
957 controller field: aField
958 !
960 headSegment
961 ^ segments at: direction
962 !
964 isEnabled
965 ^ enabled
966 !
968 isFirstMove
969 ^ moved not
970 !
972 isReady
973 ^ nextDirection notNil
974 !
976 location
977 ^ location
978 !
980 location: anObject
981 location := anObject
982 !
984 name
985 ^ name
986 !
988 name: anObject
989 name := anObject.
990 !
992 nextDirection
993 ^ nextDirection
994 !
996 nextDirection: aPoint
997 moved ifFalse: [ direction := aPoint ].
998 nextDirection := aPoint
999 ! !
1001 !TronPlayer methodsFor: 'event handling'!
1003 keyDown: keyCode
1004 ^ controller keyDown: keyCode
1005 ! !
1007 !TronPlayer methodsFor: 'initialization'!
1009 initialize
1010 super initialize.
1011 enabled := true.
1012 self reset; initializeSegments.
1015 reset
1016 direction := 0 @ -1.
1017 nextDirection := nil.
1018 moved := false.
1019 controller ifNotNil: [ controller reset ].
1020 ! !
1022 !TronPlayer methodsFor: 'observing'!
1024 onColorChange: aBlock
1025 onColorChange := aBlock.
1028 onControllerChange: aBlock
1029 onControllerChange := aBlock.
1032 onEnabledChange: aBlock
1033 onEnabledChange := aBlock.
1034 ! !
1036 !TronPlayer methodsFor: 'private'!
1038 initializeSegments
1039 segments := Dictionary new.
1040 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1041 segments
1042 at: to
1043 put: (TronHead new
1044 player: self;
1045 sprite: 'head', toName;
1046 direction: to);
1047 at: {to. to}
1048 put: (TronSegment new
1049 player: self;
1050 sprite: 'body', toName).
1051 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1052 (from x = to x) | (from y = to y) ifFalse: [
1053 segments
1054 at: {from. to}
1055 put: (TronSegment new
1056 player: self;
1057 sprite: 'body', fromName, 'To', toName) ]]].
1058 ! !
1060 !TronPlayer methodsFor: 'updating'!
1062 compute
1063 controller compute
1066 move
1067 | segment |
1068 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1069 direction := nextDirection.
1070 location := location + direction.
1071 moved := true.
1072 ^ segment
1073 ! !
1075 TronPlayer class instanceVariableNames: 'directions directionNames'!
1077 !TronPlayer class methodsFor: 'initialization'!
1079 directionNames
1080 ^ directionNames
1083 directions
1084 ^ directions
1087 initialize
1088 super initialize.
1089 directionNames := Dictionary new
1090 at: 0 @ -1 put: 'North';
1091 at: 1 @ 0 put: 'East';
1092 at: 0 @ 1 put: 'South';
1093 at: -1 @ 0 put: 'West';
1094 yourself.
1095 directions := directionNames keys.
1096 ! !
1098 Object subclass: #TronSegment
1099 instanceVariableNames: 'player sprite'
1100 package: 'Serpentron'!
1102 !TronSegment methodsFor: 'accessing'!
1104 color
1105 ^ player color
1108 isHead
1109 ^ false
1112 player
1113 ^ player
1116 player: anObject
1117 player := anObject
1120 sprite
1121 ^ sprite
1124 sprite: anObject
1125 sprite := anObject
1126 ! !
1128 TronSegment subclass: #TronHead
1129 instanceVariableNames: 'direction'
1130 package: 'Serpentron'!
1132 !TronHead methodsFor: 'accessing'!
1134 direction
1135 ^ direction
1138 direction: anObject
1139 direction := anObject
1142 isHead
1143 ^ true
1144 ! !
1146 Object subclass: #TronSkin
1147 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1148 package: 'Serpentron'!
1150 !TronSkin methodsFor: 'accessing'!
1152 tileSize
1153 ^ tileSize
1154 ! !
1156 !TronSkin methodsFor: 'drawing'!
1158 drawBackgroundOn: aContext at: aPoint
1159 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1160 drawTile: #backgroundTile on: aContext at: aPoint
1163 drawBackgroundOn: aContext from: nwPoint to: sePoint
1164 nwPoint y to: sePoint y do: [ :row |
1165 nwPoint x to: sePoint x do: [ :col |
1166 self drawBackgroundOn: aContext at: col @ row ]]
1169 drawField: aField on: aContext at: aPoint
1170 | x y segment |
1172 x := (aPoint - 1) x * tileSize.
1173 y := (aPoint - 1) y * tileSize.
1175 aContext
1176 clearRect: x and: y and: tileSize and: tileSize.
1178 segment := (aField at: aPoint) ifNil: [ ^ self ].
1180 self
1181 drawTile: segment sprite
1182 offset: maskOffset
1183 on: aContext
1184 at: aPoint.
1185 aContext
1186 globalCompositeOperation: 'source-atop';
1187 fillStyle: segment color;
1188 fillRect: x and: y and: tileSize and: tileSize.
1189 self drawTile: segment sprite on: aContext at: aPoint.
1191 aContext globalCompositeOperation: 'source-over'.
1194 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1195 aContext drawImage: skinImage
1196 sx: sourcePoint x
1197 sy: sourcePoint y
1198 sw: tileSize
1199 sh: tileSize
1200 dx: destinationPoint x
1201 dy: destinationPoint y
1202 dw: tileSize
1203 dh: tileSize
1206 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1207 self
1208 drawSkinImageOn: aContext
1209 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1210 destination: (targetPoint - 1) * tileSize
1213 drawTile: aSymbol on: aContext at: aPoint
1214 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1215 ! !
1217 !TronSkin methodsFor: 'initialization'!
1219 initialize
1220 super initialize.
1221 skinMap := Dictionary new
1222 at: #background put: 0@0;
1223 at: #backgroundBottomRight put: 49@34;
1224 at: #backgroundTile put: 0@35;
1225 at: #headNorth put: 1@35;
1226 at: #headEast put: 2@35;
1227 at: #headSouth put: 3@35;
1228 at: #headWest put: 4@35;
1229 at: #bodyNorth put: 5@35;
1230 at: #bodyEast put: 6@35;
1231 at: #bodySouth put: 7@35;
1232 at: #bodyWest put: 8@35;
1233 at: #bodySouthToEast put: 9@35;
1234 at: #bodyWestToNorth put: 9@35;
1235 at: #bodyNorthToEast put: 10@35;
1236 at: #bodyWestToSouth put: 10@35;
1237 at: #bodyNorthToWest put: 11@35;
1238 at: #bodyEastToSouth put: 11@35;
1239 at: #bodySouthToWest put: 12@35;
1240 at: #bodyEastToNorth put: 12@35;
1241 yourself.
1242 maskOffset := 12 @ 0.
1243 skinImage := document createElement: 'img'
1244 ! !
1246 !TronSkin methodsFor: 'loading'!
1248 load: url andDo: aBlock
1249 skinImage onload: [
1250 tileSize := skinImage width / 50.
1251 aBlock value ].
1252 skinImage src: url.
1253 ! !
1255 !Point methodsFor: '*Serpentron'!
1257 rotate90ccw
1258 ^ self y @ self x negated
1261 rotate90cw
1262 ^ self y negated @ self x
1263 ! !