view src/Serpentron.st @ 0:27280b550d56

Serpentron 1.0-beta.
author Mikhail Kryshen <mikhail@kryshen.net>
date Wed, 17 Feb 2016 23:55:43 +0300
parents
children 3bb2fada1594
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'
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 skin := TronSkin new.
15 field := TronField new
16 skin: skin;
17 onEndGame: [ self handleEndGame ].
18 self initializePlayers; initializeControllers.
19 (Silk fromElement: document)
20 on: #keydown bind: [ :event | field keyDown: event ].
21 ! !
23 !Serpentron methodsFor: 'private'!
25 handleEndGame
26 | color |
27 field isGameFinished
28 "Interrupted game, no winner."
29 ifFalse: [ ^ self startScreenVisible: true ].
30 color := field winningColorIfNone: [
31 ^ self status: 'No winner in this round.'; nextRound ].
32 self updateScore.
33 (score anySatisfy: [ :each | each >= pointsToWin ])
34 ifFalse: [
35 self status: (self winnerDOM: color) << ' won the round.'.
36 ^ self nextRound ].
37 self status: self scoreDOM.
38 window
39 setTimeout: [
40 self startScreenVisible: true.
41 self hideMessage.
42 self status: self scoreDOM << '. ' << (self winnerDOM: color) << ' won!!' ]
43 after: 2000.
44 window
45 setTimeout: [ self showMessage: (self winnerDOM: color) << ' won!!' ]
46 after: 1000.
47 !
49 hideMessage
50 (Silk at: '#message') element className: 'hidden'.
51 !
53 initializeControllers
54 controllerPrototypes := {
55 TronKeyboardController new
56 keyMap: #{
57 38 -> (0 @ -1).
58 39 -> (1 @ 0).
59 40 -> (0 @ 1).
60 37 -> (-1 @ 0)}
61 name: 'arrows'.
62 TronKeyboardController new
63 keyMap: #{
64 87 -> (0 @ -1).
65 68 -> (1 @ 0).
66 83 -> (0 @ 1).
67 65 -> (-1 @ 0)}
68 name: 'WASD'.
69 TronKeyboardController new
70 keyMap: #{
71 89 -> (0 @ -1).
72 74 -> (1 @ 0).
73 72 -> (0 @ 1).
74 71 -> (-1 @ 0)}
75 name: 'YGHJ'.
76 TronKeyboardController new
77 keyMap: #{
78 80 -> (0 @ -1).
79 222 -> (1 @ 0).
80 59 -> (0 @ 1).
81 76 -> (-1 @ 0)}
82 name: 'PL;'''.
83 TronComputerController1 new.
84 "TronRandomController new"
85 }.
86 (players at: 1) controller: (controllerPrototypes at: 1) copy.
87 (players at: 2) controller: (controllerPrototypes at: 5) copy.
88 (players at: 3) controller: (controllerPrototypes at: 5) copy.
89 (players at: 4) controller: (controllerPrototypes at: 5) copy.
90 !
92 initializePlayers
93 players := {
94 TronPlayer new
95 name: 'Player 1';
96 color: (playerColors at: 1).
97 TronPlayer new
98 name: 'Player 2';
99 color: (playerColors at: 2).
100 TronPlayer new
101 name: 'Player 3';
102 color: (playerColors at: 3);
103 enabled: false.
104 TronPlayer new
105 name: 'Player 4';
106 color: (playerColors at: 4);
107 enabled: false
108 }.
109 !
111 nextRound
112 window
113 setTimeout: [
114 self status: self scoreDOM.
115 field start: field players ]
116 after: 2000.
117 !
119 randomizePlayerColors
120 | enabledPlayers |
121 enabledPlayers := players select: #isEnabled.
122 enabledPlayers do: [ :each |
123 | color |
124 [ color := playerColors atRandom.
125 enabledPlayers allSatisfy: [ :p |
126 p = each | (p color ~= color) ]
127 ] whileFalse.
128 each color: color.
129 ].
130 !
132 randomizeTeams
133 | colorA colorB teamA |
134 players do: [ :each | each enabled: true ].
135 colorA := playerColors atRandom.
136 [ colorB := playerColors atRandom.
137 colorB = colorA ] whileTrue.
138 teamA := players copy.
139 players size // 2
140 timesRepeat: [ (teamA remove: teamA atRandom) color: colorB ].
141 teamA do: [ :each | each color: colorA ].
142 !
144 renderColorSelectorFor: aPlayer on: aSilk
145 | container buttons onChange |
146 container := aSilk SPAN: {#class -> 'color-selector'}.
147 buttons := Dictionary from:
148 (playerColors collect: [ :each |
149 | button |
150 button := container A.
151 button on: #click bind: [
152 aPlayer enabled: true; color: each.
153 self validatePlayerColor: aPlayer ].
154 button element style background: each.
155 each -> button ]).
156 buttons at: #disabled put: (container A
157 SPAN: '✗';
158 on: #click bind: [
159 aPlayer enabled: false.
160 self validatePlayerColor: aPlayer ]).
161 onChange := [
162 buttons keysAndValuesDo: [ :k :v |
163 (aPlayer isEnabled not & k = #disabled) |
164 (aPlayer isEnabled & k = aPlayer color)
165 ifTrue: [ v element className: 'selected-color-button' ]
166 ifFalse: [ v element className: 'color-button' ] ] ].
167 aPlayer
168 onEnabledChange: onChange;
169 onColorChange: onChange.
170 onChange value.
171 !
173 renderControllerSelectorFor: aPlayer on: aSilk
174 | select options onChange |
175 select := aSilk SELECT.
176 options := Dictionary new.
177 controllerPrototypes do: [ :each |
178 options at: each name put: (select OPTION: each name) element ].
179 select
180 on: #change
181 bind: [
182 aPlayer controller:
183 (controllerPrototypes
184 detect: [ :each | each name = select element value ])
185 copy ].
186 onChange := [
187 | value |
188 value := aPlayer controller name.
189 (options at: value) selected: 'selected'.
190 aPlayer controller class = TronKeyboardController
191 ifTrue: [
192 players do: [ :each |
193 (each ~= aPlayer and: [ each controller name = value ])
194 ifTrue: [ each controller: TronComputerController1 new ]]]].
195 aPlayer onControllerChange: onChange.
196 onChange value.
197 !
199 renderPlayer: aPlayer on: aSilk
200 | container |
201 container := aSilk DIV: { #class -> 'player' }.
202 self renderColorSelectorFor: aPlayer on: container.
203 (container INPUT
204 on: #change bind: [ :event |
205 aPlayer name: event target value ])
206 element value: aPlayer name.
207 self renderControllerSelectorFor: aPlayer on: container.
208 !
210 renderStartScreenOn: aSilk
211 | startScreen |
212 startScreen := aSilk DIV: {#id -> 'start-screen'}.
214 players do: [ :each | self renderPlayer: each on: startScreen ].
216 (startScreen BUTTON: 'Random colors')
217 on: #click bind: [ self randomizePlayerColors ].
218 (startScreen BUTTON: 'Random teams')
219 on: #click bind: [ self randomizeTeams ].
221 startScreen BR.
223 (startScreen BUTTON: 'Start')
224 on: #click bind: [ :event | self startGame; status: self scoreDOM ].
225 !
227 scoreDOM
228 | message |
229 message := Silk SPAN: 'Score: '.
230 score associations
231 do: [ :each |
232 (message SPAN: {#class -> 'player-color'. (each value * 10) rounded / 10})
233 element style color: each key ]
234 separatedBy: [ message << ':' ].
235 message << ('/', pointsToWin).
236 ^ message.
237 !
239 showMessage: anObject
240 ((Silk at: '#message') resetContents << anObject)
241 element className: 'visible'.
242 !
244 startGame
245 | enabledPlayers |
246 self startScreenVisible: false.
247 field start: (players select: #isEnabled).
248 pointsToWin := (30 / (field players size + 1)) ceiling.
249 score := field colors collect: [ :each | 0 ].
250 !
252 startScreenVisible: aBoolean
253 (Silk at: '#start-screen') element
254 className: (aBoolean ifTrue: [ 'visible' ] ifFalse: [ 'hidden' ])
255 !
257 status: anObject
258 '#status' asSilk resetContents << anObject
259 !
261 updateScore
262 | color teamSize alive points defeated |
263 color := field winningColorIfNone: [ ^ self ].
264 teamSize := field colors at: color.
265 defeated := field players size - teamSize.
266 alive := field liveColors at: color.
267 points := defeated * alive / teamSize * (teamSize + 1) / 2.
268 points := points / (field players size - 1).
269 score at: color put: (score at: color) + points.
270 !
272 validatePlayerColor: aPlayer
273 | enabledPlayers otherPlayers freeColor |
274 enabledPlayers := players select: #isEnabled.
275 ((enabledPlayers collect: #color) asSet size > 1)
276 ifTrue: [ ^ self ].
277 freeColor := playerColors detect: [ :each | each ~= enabledPlayers anyOne color ].
278 otherPlayers := players select: [ :each | each ~~ aPlayer ].
279 (otherPlayers
280 detect: [ :each | each isEnabled not ]
281 ifNone: [ otherPlayers first ])
282 enabled: true;
283 color: freeColor.
284 !
286 winnerDOM: color
287 | names team message |
288 team := field players select: [ :each | each color = color ].
289 names := ''.
290 (team collect: #name)
291 do: [ :each | names := names, each]
292 separatedBy: [ names := names, ' and ' ].
293 message := Silk SPAN.
294 (message SPAN: {#class -> 'player-color'. names})
295 element style color: color.
296 ^ message
297 ! !
299 !Serpentron methodsFor: 'rendering'!
301 augmentPage
302 '#serpentron' asSilk resetContents << 'Loading...'.
303 skin
304 load: 'resources/skin.png'
305 andDo: [ '#serpentron' asSilk resetContents << self ]
306 !
308 renderOnSilk: aSilk
309 | scale container width height |
310 scale := 1.
311 skin tileSize >= 20
312 ifTrue: [ scale := 0.5 ].
313 skin tileSize < 10
314 ifTrue: [ scale := 2].
316 width := field fieldSize x * skin tileSize * scale.
317 height := field fieldSize y * skin tileSize * scale.
319 aSilk element style
320 width: width asString , 'px'.
322 (aSilk DIV: {#id -> 'title'})
323 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
324 #target -> '_top'.
325 'Serpentron'});
326 SPAN: {#id -> 'status'. '1.0 beta'}.
328 container := aSilk DIV: {#id -> 'field'}.
329 container element style
330 width: width asString , 'px';
331 height: height asString , 'px'.
333 container << field.
334 self renderStartScreenOn: container.
336 (container DIV: {#id -> 'message'. #class -> 'hidden'})
337 element style
338 margin: '0 auto'.
340 aSilk << container.
341 ! !
343 Serpentron class instanceVariableNames: 'Instance'!
345 !Serpentron class methodsFor: 'starting'!
347 start
348 (Instance := self new) augmentPage
349 ! !
351 Object subclass: #TronCollider
352 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
353 package: 'Serpentron'!
354 !TronCollider commentStamp!
355 Particle system for collision effect.!
357 !TronCollider methodsFor: 'accessing'!
359 canvas: aCanvas
360 canvas := aCanvas.
361 context := canvas getContext: '2d'.
362 !
364 skin: aTronSkin
365 skin := aTronSkin
366 ! !
368 !TronCollider methodsFor: 'initialization'!
370 initialize
371 super initialize.
372 particlesPerCollision := 200.
373 particlePool := Queue new.
374 particlesPerCollision * 2 + 10 timesRepeat: [
375 particlePool nextPut: TronColliderParticle new ].
376 particles := OrderedCollection new
377 ! !
379 !TronCollider methodsFor: 'private'!
381 renderFrame: timestamp
382 | liveParticles delta |
383 delta := lastFrameTime
384 ifNil: [ 0 ]
385 ifNotNil: [ timestamp - lastFrameTime ].
386 lastFrameTime := timestamp.
387 "Transcript show: 'renderFrame: ';
388 show: timestamp;
389 show: '; ';
390 show: delta;
391 cr."
392 liveParticles := OrderedCollection new.
393 context clearRect: 0 y: 0 w: canvas width h: canvas height.
394 particles do: [ :each |
395 each
396 update: delta;
397 drawOn: context tileSize: skin tileSize.
398 each
399 ifAlive: [ liveParticles add: each ]
400 ifNotAlive: [ particlePool nextPut: each ] ].
401 particles := liveParticles.
402 animationRequest := particles
403 ifEmpty: [ nil ]
404 ifNotEmpty: [
405 window
406 requestAnimationFrame: [ :ts |
407 self renderFrame: ts ]
408 on: canvas ]
409 !
411 startAnimation
412 animationRequest ifNotNil: [ ^ self ].
413 lastFrameTime := nil.
414 animationRequest := window
415 requestAnimationFrame: [ :ts | self renderFrame: ts ]
416 on: canvas.
417 ! !
419 !TronCollider methodsFor: 'starting'!
421 animateCollisionFor: aPlayer
422 | particle |
423 particlesPerCollision timesRepeat: [
424 particles add:
425 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
426 resetPosition:
427 aPlayer location - 0.5 - (aPlayer direction / 2)
428 color: (Math random < 0.5
429 ifTrue: [ aPlayer color ]
430 ifFalse: [ '#ff3000' ])) ].
431 self startAnimation.
432 ! !
434 Object subclass: #TronColliderParticle
435 instanceVariableNames: 'color size velocity position alpha decay'
436 package: 'Serpentron'!
438 !TronColliderParticle methodsFor: 'accessing'!
440 ifAlive: aliveBlock ifNotAlive: notAliveBlock
441 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
442 !
444 resetPosition: positionPoint color: aString
445 position := positionPoint.
446 color := aString.
447 "Particles return to the pool as soon as they die.
448 Randomize on each reset or the pool will end up sotrted by particles' life time."
449 alpha := 1 - (Math random * 0.5).
450 decay := 0.997 - (Math random * 0.01)
451 ! !
453 !TronColliderParticle methodsFor: 'drawing'!
455 drawOn: aContext tileSize: tileSize
456 aContext
457 globalAlpha: alpha;
458 fillStyle: color;
459 fillRect: (position x - (size / 2)) * tileSize
460 y: (position y - (size / 2)) * tileSize
461 w: size * tileSize
462 h: size * tileSize.
463 ! !
465 !TronColliderParticle methodsFor: 'initialization'!
467 initialize
468 super initialize.
469 size := (Math random * 0.3 + 0.7).
470 velocity := (Math random - 0.5) @ (Math random - 0.5).
471 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
472 ! !
474 !TronColliderParticle methodsFor: 'updating'!
476 update: delta
477 position := position + (velocity * delta).
478 alpha := alpha * (decay raisedTo: delta).
479 ! !
481 Object subclass: #TronController
482 instanceVariableNames: 'field player'
483 package: 'Serpentron'!
484 !TronController commentStamp!
485 Abstract superclass for controlling input devices and algorithms.!
487 !TronController methodsFor: 'accessing'!
489 field: aTronField
490 field := aTronField
491 !
493 name
494 self subclassResponsibility.
495 !
497 nextDirection: aPoint
498 "Do not turn back."
499 player nextDirection = (aPoint * (-1 @ -1))
500 ifTrue: [ ^ self ].
501 player nextDirection: aPoint.
502 !
504 player: aTronPlayer
505 player := aTronPlayer
506 ! !
508 !TronController methodsFor: 'computing'!
510 compute
511 !
513 isAtDecisionPoint
514 | loc dir turn |
515 player isFirstMove ifTrue: [ ^ true ].
516 dir := player nextDirection.
517 loc := player location.
518 (field isFreeAt: loc + dir)
519 ifFalse: [ ^ true ].
520 turn := dir rotate90ccw.
521 ((field isFreeAt: loc + turn)
522 and: [ (field isFreeAt: loc + dir + turn) not ])
523 ifTrue: [ ^ true ].
524 turn := dir rotate90cw.
525 ((field isFreeAt: loc + turn)
526 and: [ (field isFreeAt: loc + dir + turn) not ])
527 ifTrue: [ ^ true ].
528 ^ false
529 ! !
531 !TronController methodsFor: 'event handling'!
533 keyDown: keyCode
534 ^ false
535 ! !
537 !TronController methodsFor: 'initialization'!
539 reset
540 ! !
542 TronController class instanceVariableNames: 'directions'!
544 TronController subclass: #TronComputerController1
545 instanceVariableNames: 'weight aggressiveness'
546 package: 'Serpentron'!
548 !TronComputerController1 methodsFor: 'accessing'!
550 name
551 ^ 'Computer'
552 ! !
554 !TronComputerController1 methodsFor: 'computing'!
556 compute
557 | best bestDirection |
558 "player isFirstMove
559 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
560 (self isAtDecisionPoint
561 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
562 ifFalse: [ ^ self ].
563 aggressiveness := 200 atRandom.
564 best := 0.
565 TronPlayer directions do: [ :each |
566 weight := 0.
567 self scan: each.
568 weight > best
569 ifTrue: [
570 best := weight.
571 bestDirection := each ]].
572 bestDirection ifNil: [ ^ self ].
573 self nextDirection: bestDirection.
574 ! !
576 !TronComputerController1 methodsFor: 'private'!
578 extend: directionPoint from: nwPoint to: sePoint
579 | point scanDir |
580 scanDir := directionPoint y abs @ directionPoint x abs.
581 point := Point
582 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
583 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
584 [ point <= sePoint ]
585 whileTrue: [
586 weight := weight + 1.
587 (field isFreeAt: point)
588 ifFalse: [
589 (self isEnemyHeadAt: point)
590 ifTrue: [ weight := weight + aggressiveness ]
591 ifFalse: [ ^ false ] ].
592 point := point + scanDir ].
593 ^ true
594 !
596 isEnemyHeadAt: aPoint
597 (field playerWithHeadAt: aPoint)
598 ifNotNil: [ :p | ^ p color ~= player color ].
599 ^ false
600 !
602 scan: directionPoint
603 | nw se nextNW nextSE directions scanDir |
604 nw := se := player location + directionPoint.
605 (field isFreeAt: nw) ifFalse: [ ^ self ].
606 directions := TronPlayer directions copy
607 remove: directionPoint * -1;
608 yourself.
609 [ scanDir := directions atRandom.
610 scanDir <= 0 asPoint
611 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
612 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
613 (self extend: scanDir from: nextNW to: nextSE)
614 ifTrue: [ nw := nextNW. se := nextSE ]
615 ifFalse: [ directions remove: scanDir ].
616 directions isEmpty not
617 ] whileTrue.
618 ! !
620 TronController subclass: #TronKeyboardController
621 instanceVariableNames: 'keyMap name'
622 package: 'Serpentron'!
624 !TronKeyboardController methodsFor: 'accessing'!
626 keyMap: aDictionary name: aString
627 keyMap := aDictionary.
628 name := 'Keyboard: ', aString.
629 !
631 name
632 ^ name
633 ! !
635 !TronKeyboardController methodsFor: 'event handling'!
637 keyDown: keyCode
638 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
639 ^ true
640 ! !
642 TronKeyboardController class instanceVariableNames: 'keyMaps'!
644 TronController subclass: #TronRandomController
645 instanceVariableNames: ''
646 package: 'Serpentron'!
648 !TronRandomController methodsFor: 'accessing'!
650 name
651 ^ 'Computer (random)'
652 ! !
654 !TronRandomController methodsFor: 'computing'!
656 compute
657 | dirs |
658 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
659 ifFalse: [ ^ self ].
660 dirs := TronPlayer directions
661 select: [ :each | field isFreeAt: player location + each ].
662 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
663 ! !
665 Object subclass: #TronField
666 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
667 package: 'Serpentron'!
668 !TronField commentStamp!
669 The game field. Provides game logic and rendering.!
671 !TronField methodsFor: 'accessing'!
673 at: aPoint
674 ^ (matrix at: aPoint y) at: aPoint x
675 !
677 at: aPoint ifAbsent: aBlock
678 ^ matrix
679 at: aPoint y
680 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
681 ifAbsent: aBlock
682 !
684 at: aPoint put: aSegment
685 (matrix at: aPoint y) at: aPoint x put: aSegment
686 !
688 colors
689 ^ colors
690 !
692 fieldSize
693 ^ size
694 !
696 isFreeAt: aPoint
697 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
698 !
700 isGameFinished
701 ^ liveColors size < 2
702 !
704 liveColors
705 ^ liveColors
706 !
708 playerWithHeadAt: aPoint
709 | s |
710 s := self at: aPoint ifAbsent: [ ^ nil ].
711 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
712 !
714 players
715 ^ players
716 !
718 skin
719 ^ skin
720 !
722 skin: aTronSkin
723 skin := aTronSkin
724 !
726 winningColorIfNone: aBlock
727 ^ (liveColors size = 1)
728 ifTrue: [ liveColors keys anyOne ]
729 ifFalse: aBlock.
730 ! !
732 !TronField methodsFor: 'event handling'!
734 keyDown: event
735 "Handle Esc key."
736 livePlayers isEmpty not & (event keyCode = 27)
737 ifTrue: [
738 event preventDefault.
739 self endGame.
740 ^ self ].
741 "Check all players rather than livePlayers
742 so that preventDefault is called for all used keys."
743 (players anySatisfy: [ :each | each keyDown: event keyCode ])
744 ifTrue: [ event preventDefault ].
745 timerId ifNil: [ self startIfAllReady ].
746 ! !
748 !TronField methodsFor: 'initialization'!
750 initialize
751 super initialize.
752 livePlayers := players := #().
753 size := 50 @ 35
754 ! !
756 !TronField methodsFor: 'observing'!
758 onEndGame: aBlock
759 onEndGame := aBlock
760 ! !
762 !TronField methodsFor: 'private'!
764 endGame
765 self stopTimer.
766 livePlayers := #().
767 onEndGame ifNotNil: #value.
768 !
770 killPlayer: aPlayer
771 collider animateCollisionFor: aPlayer.
772 livePlayers remove: aPlayer.
773 self updateLiveColors.
774 !
776 locatePlayers
777 players size = 2 ifTrue: [
778 (players at: 1) location: 16 @ 18.
779 (players at: 2) location: 35 @ 18.
780 ^ self ].
781 players size = 3 ifTrue: [
782 (players at: 1) location: 18 @ 13.
783 (players at: 2) location: 18 @ 23.
784 (players at: 3) location: 33 @ 18.
785 ^ self ].
786 players size = 4 ifTrue: [
787 (players at: 1) location: 16 @ 11.
788 (players at: 2) location: 35 @ 11.
789 (players at: 3) location: 16 @ 25.
790 (players at: 4) location: 35 @ 25.
791 ^ self ].
792 self error: 'Invalid number of players.'
793 !
795 renderCanvasOn: aSilk
796 ^ aSilk CANVAS: {
797 #width -> (size x * skin tileSize).
798 #height -> (size y * skin tileSize)}.
799 !
801 startIfAllReady
802 (livePlayers isEmpty not
803 and: [ livePlayers allSatisfy: #isReady ])
804 ifTrue: [ self startTimer ]
805 !
807 startTimer
808 timerId ifNotNil: [ self error: 'Timer already running.' ].
809 timerId := window setInterval: [ self update ] every: 75.
810 !
812 stopTimer
813 timerId ifNotNil: [
814 window clearInterval: timerId.
815 timerId := nil ]
816 !
818 update
819 | lpCopy |
820 lpCopy := livePlayers copy.
821 lpCopy do: [ :each |
822 | l |
823 l := each location.
824 self at: l put: each move.
825 skin drawField: self on: context at: l ].
826 lpCopy do: [ :each |
827 | l other |
828 l := each location.
829 (self isFreeAt: l)
830 ifTrue: [ self at: l put: each headSegment. ]
831 ifFalse: [
832 self killPlayer: each.
833 "Check for head-to-head collision."
834 other := self playerWithHeadAt: l.
835 (other isNil | (other = each))
836 ifFalse: [
837 self killPlayer: other.
838 self at: l put: nil. ]]].
839 livePlayers do: [ :each |
840 skin drawField: self on: context at: each location ].
841 self isGameFinished ifTrue: [ ^ self endGame ].
842 livePlayers do: #compute.
843 !
845 updateLiveColors
846 liveColors := Dictionary new.
847 livePlayers do: [ :each |
848 liveColors
849 at: each color
850 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
851 ifAbsent: [ liveColors at: each color put: 1 ]].
852 ! !
854 !TronField methodsFor: 'rendering'!
856 renderOnSilk: aSilk
857 | backgroundCanvas |
859 backgroundCanvas := (self renderCanvasOn: aSilk) element.
861 canvas := (self renderCanvasOn: aSilk) element.
862 context := canvas getContext: '2d'.
864 collider := TronCollider new
865 canvas: (self renderCanvasOn: aSilk) element;
866 skin: skin.
868 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
869 ! !
871 !TronField methodsFor: 'starting'!
873 start: anArrayOfPlayers
874 players := anArrayOfPlayers.
875 livePlayers := players copy.
876 self updateLiveColors.
877 colors := liveColors.
878 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
879 context clearRect: 0 y: 0 w: canvas width h: canvas height.
880 self locatePlayers.
881 players do: [ :each | each field: self; reset; compute ].
882 players do: [ :each |
883 self at: each location put: each headSegment.
884 skin drawField: self on: context at: each location ].
885 self startIfAllReady.
886 ! !
888 Object subclass: #TronPlayer
889 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
890 package: 'Serpentron'!
892 !TronPlayer methodsFor: 'accessing'!
894 color
895 ^ color
896 !
898 color: anObject
899 color := anObject.
900 onColorChange ifNotNil: #value.
901 !
903 controller
904 ^ controller
905 !
907 controller: aTronController
908 aTronController player: self.
909 controller := aTronController.
910 onControllerChange ifNotNil: #value.
911 !
913 direction
914 ^ direction
915 !
917 enabled: aBoolean
918 enabled := aBoolean.
919 onEnabledChange ifNotNil: #value.
920 !
922 field: aField
923 controller field: aField
924 !
926 headSegment
927 ^ segments at: direction
928 !
930 isEnabled
931 ^ enabled
932 !
934 isFirstMove
935 ^ moved not
936 !
938 isReady
939 ^ nextDirection notNil
940 !
942 location
943 ^ location
944 !
946 location: anObject
947 location := anObject
948 !
950 name
951 ^ name
952 !
954 name: anObject
955 name := anObject.
956 !
958 nextDirection
959 ^ nextDirection
960 !
962 nextDirection: aPoint
963 moved ifFalse: [ direction := aPoint ].
964 nextDirection := aPoint
965 ! !
967 !TronPlayer methodsFor: 'event handling'!
969 keyDown: keyCode
970 ^ controller keyDown: keyCode
971 ! !
973 !TronPlayer methodsFor: 'initialization'!
975 initialize
976 super initialize.
977 enabled := true.
978 self reset; initializeSegments.
979 !
981 reset
982 direction := 0 @ -1.
983 nextDirection := nil.
984 moved := false.
985 controller ifNotNil: [ controller reset ].
986 ! !
988 !TronPlayer methodsFor: 'observing'!
990 onColorChange: aBlock
991 onColorChange := aBlock.
992 !
994 onControllerChange: aBlock
995 onControllerChange := aBlock.
996 !
998 onEnabledChange: aBlock
999 onEnabledChange := aBlock.
1000 ! !
1002 !TronPlayer methodsFor: 'private'!
1004 initializeSegments
1005 segments := Dictionary new.
1006 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1007 segments
1008 at: to
1009 put: (TronHead new
1010 player: self;
1011 sprite: 'head', toName;
1012 direction: to);
1013 at: {to. to}
1014 put: (TronSegment new
1015 player: self;
1016 sprite: 'body', toName).
1017 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1018 (from x = to x) | (from y = to y) ifFalse: [
1019 segments
1020 at: {from. to}
1021 put: (TronSegment new
1022 player: self;
1023 sprite: 'body', fromName, 'To', toName) ]]].
1024 ! !
1026 !TronPlayer methodsFor: 'updating'!
1028 compute
1029 controller compute
1032 move
1033 | segment |
1034 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1035 direction := nextDirection.
1036 location := location + direction.
1037 moved := true.
1038 ^ segment
1039 ! !
1041 TronPlayer class instanceVariableNames: 'directions directionNames'!
1043 !TronPlayer class methodsFor: 'initialization'!
1045 directionNames
1046 ^ directionNames
1049 directions
1050 ^ directions
1053 initialize
1054 super initialize.
1055 directionNames := Dictionary new
1056 at: 0 @ -1 put: 'North';
1057 at: 1 @ 0 put: 'East';
1058 at: 0 @ 1 put: 'South';
1059 at: -1 @ 0 put: 'West';
1060 yourself.
1061 directions := directionNames keys.
1062 ! !
1064 Object subclass: #TronSegment
1065 instanceVariableNames: 'player sprite'
1066 package: 'Serpentron'!
1068 !TronSegment methodsFor: 'accessing'!
1070 color
1071 ^ player color
1074 isHead
1075 ^ false
1078 player
1079 ^ player
1082 player: anObject
1083 player := anObject
1086 sprite
1087 ^ sprite
1090 sprite: anObject
1091 sprite := anObject
1092 ! !
1094 TronSegment subclass: #TronHead
1095 instanceVariableNames: 'direction'
1096 package: 'Serpentron'!
1098 !TronHead methodsFor: 'accessing'!
1100 direction
1101 ^ direction
1104 direction: anObject
1105 direction := anObject
1108 isHead
1109 ^ true
1110 ! !
1112 Object subclass: #TronSkin
1113 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1114 package: 'Serpentron'!
1116 !TronSkin methodsFor: 'accessing'!
1118 tileSize
1119 ^ tileSize
1120 ! !
1122 !TronSkin methodsFor: 'drawing'!
1124 drawBackgroundOn: aContext at: aPoint
1125 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1126 drawTile: #backgroundTile on: aContext at: aPoint
1129 drawBackgroundOn: aContext from: nwPoint to: sePoint
1130 nwPoint y to: sePoint y do: [ :row |
1131 nwPoint x to: sePoint x do: [ :col |
1132 self drawBackgroundOn: aContext at: col @ row ]]
1135 drawField: aField on: aContext at: aPoint
1136 | x y segment |
1138 x := (aPoint - 1) x * tileSize.
1139 y := (aPoint - 1) y * tileSize.
1141 aContext
1142 clearRect: x and: y and: tileSize and: tileSize.
1144 segment := (aField at: aPoint) ifNil: [ ^ self ].
1146 self
1147 drawTile: segment sprite
1148 offset: maskOffset
1149 on: aContext
1150 at: aPoint.
1151 aContext
1152 globalCompositeOperation: 'source-atop';
1153 fillStyle: segment color;
1154 fillRect: x and: y and: tileSize and: tileSize.
1155 self drawTile: segment sprite on: aContext at: aPoint.
1157 aContext globalCompositeOperation: 'source-over'.
1160 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1161 aContext drawImage: skinImage
1162 sx: sourcePoint x
1163 sy: sourcePoint y
1164 sw: tileSize
1165 sh: tileSize
1166 dx: destinationPoint x
1167 dy: destinationPoint y
1168 dw: tileSize
1169 dh: tileSize
1172 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1173 self
1174 drawSkinImageOn: aContext
1175 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1176 destination: (targetPoint - 1) * tileSize
1179 drawTile: aSymbol on: aContext at: aPoint
1180 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1181 ! !
1183 !TronSkin methodsFor: 'initialization'!
1185 initialize
1186 super initialize.
1187 skinMap := Dictionary new
1188 at: #background put: 0@0;
1189 at: #backgroundBottomRight put: 49@34;
1190 at: #backgroundTile put: 0@35;
1191 at: #headNorth put: 1@35;
1192 at: #headEast put: 2@35;
1193 at: #headSouth put: 3@35;
1194 at: #headWest put: 4@35;
1195 at: #bodyNorth put: 5@35;
1196 at: #bodyEast put: 6@35;
1197 at: #bodySouth put: 7@35;
1198 at: #bodyWest put: 8@35;
1199 at: #bodySouthToEast put: 9@35;
1200 at: #bodyWestToNorth put: 9@35;
1201 at: #bodyNorthToEast put: 10@35;
1202 at: #bodyWestToSouth put: 10@35;
1203 at: #bodyNorthToWest put: 11@35;
1204 at: #bodyEastToSouth put: 11@35;
1205 at: #bodySouthToWest put: 12@35;
1206 at: #bodyEastToNorth put: 12@35;
1207 yourself.
1208 maskOffset := 12 @ 0.
1209 skinImage := document createElement: 'img'
1210 ! !
1212 !TronSkin methodsFor: 'loading'!
1214 load: url andDo: aBlock
1215 skinImage onload: [
1216 tileSize := skinImage width / 50.
1217 aBlock value ].
1218 skinImage src: url.
1219 ! !
1221 !Point methodsFor: '*Serpentron'!
1223 rotate90ccw
1224 ^ self y @ self x negated
1227 rotate90cw
1228 ^ self y negated @ self x
1229 ! !