view src/Serpentron.st @ 3:9ee21f993e05

Changed the end game message.
author Mikhail Kryshen <mikhail@kryshen.net>
date Thu, 18 Feb 2016 01:28:43 +0300
parents 3bb2fada1594
children 1a5e50fbba08
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 := #('#377eb8' '#e41a1c' '#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: 1) copy.
85 (players at: 2) controller: (controllerPrototypes at: 5) 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' asSilk resetContents << 'Loading...'.
323 skin
324 load: 'resources/skin.png'
325 andDo: [ '#serpentron' asSilk resetContents << self ]
326 !
328 renderOnSilk: aSilk
329 | scale container width height |
330 scale := 1.
331 skin tileSize >= 20
332 ifTrue: [ scale := 0.5 ].
333 skin tileSize < 10
334 ifTrue: [ scale := 2].
336 width := field fieldSize x * skin tileSize * scale.
337 height := field fieldSize y * skin tileSize * scale.
339 aSilk element style
340 width: width asString , 'px'.
342 (aSilk DIV: {#id -> 'title'})
343 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
344 #target -> '_top'.
345 'Serpentron'});
346 SPAN: {#id -> 'status'. '1.0 beta'}.
348 container := aSilk DIV: {#id -> 'field'}.
349 container element style
350 width: width asString , 'px';
351 height: height asString , 'px'.
353 container << field.
354 self renderStartScreenOn: container.
356 (container DIV: {#id -> 'message'. #class -> 'hidden'})
357 element style
358 margin: '0 auto'.
360 aSilk << container.
361 ! !
363 Serpentron class instanceVariableNames: 'Instance'!
365 !Serpentron class methodsFor: 'starting'!
367 start
368 (Instance := self new) augmentPage
369 ! !
371 Object subclass: #TronCollider
372 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
373 package: 'Serpentron'!
374 !TronCollider commentStamp!
375 Particle system for collision effect.!
377 !TronCollider methodsFor: 'accessing'!
379 canvas: aCanvas
380 canvas := aCanvas.
381 context := canvas getContext: '2d'.
382 !
384 skin: aTronSkin
385 skin := aTronSkin
386 ! !
388 !TronCollider methodsFor: 'initialization'!
390 initialize
391 super initialize.
392 particlesPerCollision := 200.
393 particlePool := Queue new.
394 particlesPerCollision * 2 + 10 timesRepeat: [
395 particlePool nextPut: TronColliderParticle new ].
396 particles := OrderedCollection new
397 ! !
399 !TronCollider methodsFor: 'private'!
401 renderFrame: timestamp
402 | liveParticles delta |
403 delta := lastFrameTime
404 ifNil: [ 0 ]
405 ifNotNil: [ timestamp - lastFrameTime ].
406 lastFrameTime := timestamp.
407 "Transcript show: 'renderFrame: ';
408 show: timestamp;
409 show: '; ';
410 show: delta;
411 cr."
412 liveParticles := OrderedCollection new.
413 context clearRect: 0 y: 0 w: canvas width h: canvas height.
414 particles do: [ :each |
415 each
416 update: delta;
417 drawOn: context tileSize: skin tileSize.
418 each
419 ifAlive: [ liveParticles add: each ]
420 ifNotAlive: [ particlePool nextPut: each ] ].
421 particles := liveParticles.
422 animationRequest := particles
423 ifEmpty: [ nil ]
424 ifNotEmpty: [
425 window
426 requestAnimationFrame: [ :ts |
427 self renderFrame: ts ]
428 on: canvas ]
429 !
431 startAnimation
432 animationRequest ifNotNil: [ ^ self ].
433 lastFrameTime := nil.
434 animationRequest := window
435 requestAnimationFrame: [ :ts | self renderFrame: ts ]
436 on: canvas.
437 ! !
439 !TronCollider methodsFor: 'starting'!
441 animateCollisionFor: aPlayer
442 | particle |
443 particlesPerCollision timesRepeat: [
444 particles add:
445 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
446 resetPosition:
447 aPlayer location - 0.5 - (aPlayer direction / 2)
448 color: (Math random < 0.5
449 ifTrue: [ aPlayer color ]
450 ifFalse: [ '#ff3000' ])) ].
451 self startAnimation.
452 ! !
454 Object subclass: #TronColliderParticle
455 instanceVariableNames: 'color size velocity position alpha decay'
456 package: 'Serpentron'!
458 !TronColliderParticle methodsFor: 'accessing'!
460 ifAlive: aliveBlock ifNotAlive: notAliveBlock
461 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
462 !
464 resetPosition: positionPoint color: aString
465 position := positionPoint.
466 color := aString.
467 "Particles return to the pool as soon as they die.
468 Randomize on each reset or the pool will end up sotrted by particles' life time."
469 alpha := 1 - (Math random * 0.5).
470 decay := 0.997 - (Math random * 0.01)
471 ! !
473 !TronColliderParticle methodsFor: 'drawing'!
475 drawOn: aContext tileSize: tileSize
476 aContext
477 globalAlpha: alpha;
478 fillStyle: color;
479 fillRect: (position x - (size / 2)) * tileSize
480 y: (position y - (size / 2)) * tileSize
481 w: size * tileSize
482 h: size * tileSize.
483 ! !
485 !TronColliderParticle methodsFor: 'initialization'!
487 initialize
488 super initialize.
489 size := (Math random * 0.3 + 0.7).
490 velocity := (Math random - 0.5) @ (Math random - 0.5).
491 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
492 ! !
494 !TronColliderParticle methodsFor: 'updating'!
496 update: delta
497 position := position + (velocity * delta).
498 alpha := alpha * (decay raisedTo: delta).
499 ! !
501 Object subclass: #TronController
502 instanceVariableNames: 'field player'
503 package: 'Serpentron'!
504 !TronController commentStamp!
505 Abstract superclass for controlling input devices and algorithms.!
507 !TronController methodsFor: 'accessing'!
509 field: aTronField
510 field := aTronField
511 !
513 name
514 self subclassResponsibility.
515 !
517 nextDirection: aPoint
518 "Do not turn back."
519 player nextDirection = (aPoint * (-1 @ -1))
520 ifTrue: [ ^ self ].
521 player nextDirection: aPoint.
522 !
524 player: aTronPlayer
525 player := aTronPlayer
526 ! !
528 !TronController methodsFor: 'computing'!
530 compute
531 !
533 isAtDecisionPoint
534 | loc dir turn |
535 player isFirstMove ifTrue: [ ^ true ].
536 dir := player nextDirection.
537 loc := player location.
538 (field isFreeAt: loc + dir)
539 ifFalse: [ ^ true ].
540 turn := dir rotate90ccw.
541 ((field isFreeAt: loc + turn)
542 and: [ (field isFreeAt: loc + dir + turn) not ])
543 ifTrue: [ ^ true ].
544 turn := dir rotate90cw.
545 ((field isFreeAt: loc + turn)
546 and: [ (field isFreeAt: loc + dir + turn) not ])
547 ifTrue: [ ^ true ].
548 ^ false
549 ! !
551 !TronController methodsFor: 'event handling'!
553 keyDown: keyCode
554 ^ false
555 ! !
557 !TronController methodsFor: 'initialization'!
559 reset
560 ! !
562 TronController class instanceVariableNames: 'directions'!
564 TronController subclass: #TronComputerController1
565 instanceVariableNames: 'weight aggressiveness'
566 package: 'Serpentron'!
568 !TronComputerController1 methodsFor: 'accessing'!
570 name
571 ^ 'Computer'
572 ! !
574 !TronComputerController1 methodsFor: 'computing'!
576 compute
577 | best bestDirection |
578 "player isFirstMove
579 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
580 (self isAtDecisionPoint
581 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
582 ifFalse: [ ^ self ].
583 aggressiveness := 200 atRandom.
584 best := 0.
585 TronPlayer directions do: [ :each |
586 weight := 0.
587 self scan: each.
588 weight > best
589 ifTrue: [
590 best := weight.
591 bestDirection := each ]].
592 bestDirection ifNil: [ ^ self ].
593 self nextDirection: bestDirection.
594 ! !
596 !TronComputerController1 methodsFor: 'private'!
598 extend: directionPoint from: nwPoint to: sePoint
599 | point scanDir |
600 scanDir := directionPoint y abs @ directionPoint x abs.
601 point := Point
602 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
603 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
604 [ point <= sePoint ]
605 whileTrue: [
606 weight := weight + 1.
607 (field isFreeAt: point)
608 ifFalse: [
609 (self isEnemyHeadAt: point)
610 ifTrue: [ weight := weight + aggressiveness ]
611 ifFalse: [ ^ false ] ].
612 point := point + scanDir ].
613 ^ true
614 !
616 isEnemyHeadAt: aPoint
617 (field playerWithHeadAt: aPoint)
618 ifNotNil: [ :p | ^ p color ~= player color ].
619 ^ false
620 !
622 scan: directionPoint
623 | nw se nextNW nextSE directions scanDir |
624 nw := se := player location + directionPoint.
625 (field isFreeAt: nw) ifFalse: [ ^ self ].
626 directions := TronPlayer directions copy
627 remove: directionPoint * -1;
628 yourself.
629 [ scanDir := directions atRandom.
630 scanDir <= 0 asPoint
631 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
632 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
633 (self extend: scanDir from: nextNW to: nextSE)
634 ifTrue: [ nw := nextNW. se := nextSE ]
635 ifFalse: [ directions remove: scanDir ].
636 directions isEmpty not
637 ] whileTrue.
638 ! !
640 TronController subclass: #TronKeyboardController
641 instanceVariableNames: 'keyMap name'
642 package: 'Serpentron'!
644 !TronKeyboardController methodsFor: 'accessing'!
646 keyMap: aDictionary name: aString
647 keyMap := aDictionary.
648 name := 'Keyboard: ', aString.
649 !
651 name
652 ^ name
653 ! !
655 !TronKeyboardController methodsFor: 'event handling'!
657 keyDown: keyCode
658 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
659 ^ true
660 ! !
662 TronKeyboardController class instanceVariableNames: 'keyMaps'!
664 TronController subclass: #TronRandomController
665 instanceVariableNames: ''
666 package: 'Serpentron'!
668 !TronRandomController methodsFor: 'accessing'!
670 name
671 ^ 'Computer (random)'
672 ! !
674 !TronRandomController methodsFor: 'computing'!
676 compute
677 | dirs |
678 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
679 ifFalse: [ ^ self ].
680 dirs := TronPlayer directions
681 select: [ :each | field isFreeAt: player location + each ].
682 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
683 ! !
685 Object subclass: #TronField
686 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
687 package: 'Serpentron'!
688 !TronField commentStamp!
689 The game field. Provides game logic and rendering.!
691 !TronField methodsFor: 'accessing'!
693 at: aPoint
694 ^ (matrix at: aPoint y) at: aPoint x
695 !
697 at: aPoint ifAbsent: aBlock
698 ^ matrix
699 at: aPoint y
700 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
701 ifAbsent: aBlock
702 !
704 at: aPoint put: aSegment
705 (matrix at: aPoint y) at: aPoint x put: aSegment
706 !
708 colors
709 ^ colors
710 !
712 fieldSize
713 ^ size
714 !
716 isFreeAt: aPoint
717 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
718 !
720 isGameFinished
721 ^ liveColors size < 2
722 !
724 liveColors
725 ^ liveColors
726 !
728 playerWithHeadAt: aPoint
729 | s |
730 s := self at: aPoint ifAbsent: [ ^ nil ].
731 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
732 !
734 players
735 ^ players
736 !
738 skin
739 ^ skin
740 !
742 skin: aTronSkin
743 skin := aTronSkin
744 !
746 winningColorIfNone: aBlock
747 ^ (liveColors size = 1)
748 ifTrue: [ liveColors keys anyOne ]
749 ifFalse: aBlock.
750 ! !
752 !TronField methodsFor: 'event handling'!
754 keyDown: event
755 "Check all players rather than livePlayers
756 so that preventDefault is called for all used keys."
757 (players anySatisfy: [ :each | each keyDown: event keyCode ])
758 ifTrue: [ event preventDefault ].
759 timerId ifNil: [ self startIfAllReady ].
760 ! !
762 !TronField methodsFor: 'initialization'!
764 initialize
765 super initialize.
766 livePlayers := players := #().
767 size := 50 @ 35
768 ! !
770 !TronField methodsFor: 'observing'!
772 onEndGame: aBlock
773 onEndGame := aBlock
774 ! !
776 !TronField methodsFor: 'private'!
778 endGame
779 self stopTimer.
780 livePlayers := #().
781 onEndGame ifNotNil: #value.
782 !
784 killPlayer: aPlayer
785 collider animateCollisionFor: aPlayer.
786 livePlayers remove: aPlayer.
787 self updateLiveColors.
788 !
790 locatePlayers
791 players size = 2 ifTrue: [
792 (players at: 1) location: 16 @ 18.
793 (players at: 2) location: 35 @ 18.
794 ^ self ].
795 players size = 3 ifTrue: [
796 (players at: 1) location: 18 @ 13.
797 (players at: 2) location: 18 @ 23.
798 (players at: 3) location: 33 @ 18.
799 ^ self ].
800 players size = 4 ifTrue: [
801 (players at: 1) location: 16 @ 11.
802 (players at: 2) location: 35 @ 11.
803 (players at: 3) location: 16 @ 25.
804 (players at: 4) location: 35 @ 25.
805 ^ self ].
806 self error: 'Invalid number of players.'
807 !
809 renderCanvasOn: aSilk
810 ^ aSilk CANVAS: {
811 #width -> (size x * skin tileSize).
812 #height -> (size y * skin tileSize)}.
813 !
815 startIfAllReady
816 (livePlayers isEmpty not
817 and: [ livePlayers allSatisfy: #isReady ])
818 ifTrue: [ self startTimer ]
819 !
821 startTimer
822 timerId ifNotNil: [ self error: 'Timer already running.' ].
823 timerId := window setInterval: [ self update ] every: 75.
824 !
826 update
827 | lpCopy |
828 lpCopy := livePlayers copy.
829 lpCopy do: [ :each |
830 | l |
831 l := each location.
832 self at: l put: each move.
833 skin drawField: self on: context at: l ].
834 lpCopy do: [ :each |
835 | l other |
836 l := each location.
837 (self isFreeAt: l)
838 ifTrue: [ self at: l put: each headSegment. ]
839 ifFalse: [
840 self killPlayer: each.
841 "Check for head-to-head collision."
842 other := self playerWithHeadAt: l.
843 (other isNil | (other = each))
844 ifFalse: [
845 self killPlayer: other.
846 self at: l put: nil. ]]].
847 livePlayers do: [ :each |
848 skin drawField: self on: context at: each location ].
849 self isGameFinished ifTrue: [ ^ self endGame ].
850 livePlayers do: #compute.
851 !
853 updateLiveColors
854 liveColors := Dictionary new.
855 livePlayers do: [ :each |
856 liveColors
857 at: each color
858 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
859 ifAbsent: [ liveColors at: each color put: 1 ]].
860 ! !
862 !TronField methodsFor: 'rendering'!
864 renderOnSilk: aSilk
865 | backgroundCanvas |
867 backgroundCanvas := (self renderCanvasOn: aSilk) element.
869 canvas := (self renderCanvasOn: aSilk) element.
870 context := canvas getContext: '2d'.
872 collider := TronCollider new
873 canvas: (self renderCanvasOn: aSilk) element;
874 skin: skin.
876 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
877 ! !
879 !TronField methodsFor: 'starting'!
881 start: anArrayOfPlayers
882 players := anArrayOfPlayers.
883 livePlayers := players copy.
884 self updateLiveColors.
885 colors := liveColors.
886 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
887 context clearRect: 0 y: 0 w: canvas width h: canvas height.
888 self locatePlayers.
889 players do: [ :each | each field: self; reset; compute ].
890 players do: [ :each |
891 self at: each location put: each headSegment.
892 skin drawField: self on: context at: each location ].
893 self startIfAllReady.
894 ! !
896 !TronField methodsFor: 'stopping'!
898 stopTimer
899 timerId ifNotNil: [
900 window clearInterval: timerId.
901 timerId := nil ]
902 ! !
904 Object subclass: #TronPlayer
905 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
906 package: 'Serpentron'!
908 !TronPlayer methodsFor: 'accessing'!
910 color
911 ^ color
912 !
914 color: anObject
915 color := anObject.
916 onColorChange ifNotNil: #value.
917 !
919 controller
920 ^ controller
921 !
923 controller: aTronController
924 aTronController player: self.
925 controller := aTronController.
926 onControllerChange ifNotNil: #value.
927 !
929 direction
930 ^ direction
931 !
933 enabled: aBoolean
934 enabled := aBoolean.
935 onEnabledChange ifNotNil: #value.
936 !
938 field: aField
939 controller field: aField
940 !
942 headSegment
943 ^ segments at: direction
944 !
946 isEnabled
947 ^ enabled
948 !
950 isFirstMove
951 ^ moved not
952 !
954 isReady
955 ^ nextDirection notNil
956 !
958 location
959 ^ location
960 !
962 location: anObject
963 location := anObject
964 !
966 name
967 ^ name
968 !
970 name: anObject
971 name := anObject.
972 !
974 nextDirection
975 ^ nextDirection
976 !
978 nextDirection: aPoint
979 moved ifFalse: [ direction := aPoint ].
980 nextDirection := aPoint
981 ! !
983 !TronPlayer methodsFor: 'event handling'!
985 keyDown: keyCode
986 ^ controller keyDown: keyCode
987 ! !
989 !TronPlayer methodsFor: 'initialization'!
991 initialize
992 super initialize.
993 enabled := true.
994 self reset; initializeSegments.
995 !
997 reset
998 direction := 0 @ -1.
999 nextDirection := nil.
1000 moved := false.
1001 controller ifNotNil: [ controller reset ].
1002 ! !
1004 !TronPlayer methodsFor: 'observing'!
1006 onColorChange: aBlock
1007 onColorChange := aBlock.
1010 onControllerChange: aBlock
1011 onControllerChange := aBlock.
1014 onEnabledChange: aBlock
1015 onEnabledChange := aBlock.
1016 ! !
1018 !TronPlayer methodsFor: 'private'!
1020 initializeSegments
1021 segments := Dictionary new.
1022 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1023 segments
1024 at: to
1025 put: (TronHead new
1026 player: self;
1027 sprite: 'head', toName;
1028 direction: to);
1029 at: {to. to}
1030 put: (TronSegment new
1031 player: self;
1032 sprite: 'body', toName).
1033 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1034 (from x = to x) | (from y = to y) ifFalse: [
1035 segments
1036 at: {from. to}
1037 put: (TronSegment new
1038 player: self;
1039 sprite: 'body', fromName, 'To', toName) ]]].
1040 ! !
1042 !TronPlayer methodsFor: 'updating'!
1044 compute
1045 controller compute
1048 move
1049 | segment |
1050 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1051 direction := nextDirection.
1052 location := location + direction.
1053 moved := true.
1054 ^ segment
1055 ! !
1057 TronPlayer class instanceVariableNames: 'directions directionNames'!
1059 !TronPlayer class methodsFor: 'initialization'!
1061 directionNames
1062 ^ directionNames
1065 directions
1066 ^ directions
1069 initialize
1070 super initialize.
1071 directionNames := Dictionary new
1072 at: 0 @ -1 put: 'North';
1073 at: 1 @ 0 put: 'East';
1074 at: 0 @ 1 put: 'South';
1075 at: -1 @ 0 put: 'West';
1076 yourself.
1077 directions := directionNames keys.
1078 ! !
1080 Object subclass: #TronSegment
1081 instanceVariableNames: 'player sprite'
1082 package: 'Serpentron'!
1084 !TronSegment methodsFor: 'accessing'!
1086 color
1087 ^ player color
1090 isHead
1091 ^ false
1094 player
1095 ^ player
1098 player: anObject
1099 player := anObject
1102 sprite
1103 ^ sprite
1106 sprite: anObject
1107 sprite := anObject
1108 ! !
1110 TronSegment subclass: #TronHead
1111 instanceVariableNames: 'direction'
1112 package: 'Serpentron'!
1114 !TronHead methodsFor: 'accessing'!
1116 direction
1117 ^ direction
1120 direction: anObject
1121 direction := anObject
1124 isHead
1125 ^ true
1126 ! !
1128 Object subclass: #TronSkin
1129 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1130 package: 'Serpentron'!
1132 !TronSkin methodsFor: 'accessing'!
1134 tileSize
1135 ^ tileSize
1136 ! !
1138 !TronSkin methodsFor: 'drawing'!
1140 drawBackgroundOn: aContext at: aPoint
1141 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1142 drawTile: #backgroundTile on: aContext at: aPoint
1145 drawBackgroundOn: aContext from: nwPoint to: sePoint
1146 nwPoint y to: sePoint y do: [ :row |
1147 nwPoint x to: sePoint x do: [ :col |
1148 self drawBackgroundOn: aContext at: col @ row ]]
1151 drawField: aField on: aContext at: aPoint
1152 | x y segment |
1154 x := (aPoint - 1) x * tileSize.
1155 y := (aPoint - 1) y * tileSize.
1157 aContext
1158 clearRect: x and: y and: tileSize and: tileSize.
1160 segment := (aField at: aPoint) ifNil: [ ^ self ].
1162 self
1163 drawTile: segment sprite
1164 offset: maskOffset
1165 on: aContext
1166 at: aPoint.
1167 aContext
1168 globalCompositeOperation: 'source-atop';
1169 fillStyle: segment color;
1170 fillRect: x and: y and: tileSize and: tileSize.
1171 self drawTile: segment sprite on: aContext at: aPoint.
1173 aContext globalCompositeOperation: 'source-over'.
1176 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1177 aContext drawImage: skinImage
1178 sx: sourcePoint x
1179 sy: sourcePoint y
1180 sw: tileSize
1181 sh: tileSize
1182 dx: destinationPoint x
1183 dy: destinationPoint y
1184 dw: tileSize
1185 dh: tileSize
1188 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1189 self
1190 drawSkinImageOn: aContext
1191 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1192 destination: (targetPoint - 1) * tileSize
1195 drawTile: aSymbol on: aContext at: aPoint
1196 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1197 ! !
1199 !TronSkin methodsFor: 'initialization'!
1201 initialize
1202 super initialize.
1203 skinMap := Dictionary new
1204 at: #background put: 0@0;
1205 at: #backgroundBottomRight put: 49@34;
1206 at: #backgroundTile put: 0@35;
1207 at: #headNorth put: 1@35;
1208 at: #headEast put: 2@35;
1209 at: #headSouth put: 3@35;
1210 at: #headWest put: 4@35;
1211 at: #bodyNorth put: 5@35;
1212 at: #bodyEast put: 6@35;
1213 at: #bodySouth put: 7@35;
1214 at: #bodyWest put: 8@35;
1215 at: #bodySouthToEast put: 9@35;
1216 at: #bodyWestToNorth put: 9@35;
1217 at: #bodyNorthToEast put: 10@35;
1218 at: #bodyWestToSouth put: 10@35;
1219 at: #bodyNorthToWest put: 11@35;
1220 at: #bodyEastToSouth put: 11@35;
1221 at: #bodySouthToWest put: 12@35;
1222 at: #bodyEastToNorth put: 12@35;
1223 yourself.
1224 maskOffset := 12 @ 0.
1225 skinImage := document createElement: 'img'
1226 ! !
1228 !TronSkin methodsFor: 'loading'!
1230 load: url andDo: aBlock
1231 skinImage onload: [
1232 tileSize := skinImage width / 50.
1233 aBlock value ].
1234 skinImage src: url.
1235 ! !
1237 !Point methodsFor: '*Serpentron'!
1239 rotate90ccw
1240 ^ self y @ self x negated
1243 rotate90cw
1244 ^ self y negated @ self x
1245 ! !