view src/Serpentron.st @ 10:5bb39198a9be

Always wait before starting next round.
author Mikhail Kryshen <mikhail@kryshen.net>
date Sat, 02 Apr 2016 12:24:52 +0300
parents 8c7e910cb328
children a6bde9cde187
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
37 status: (self winnerDOM: color) << ' won the round.';
38 nextRound.
39 ^ self ].
40 self status: self scoreDOM.
41 timeoutId := window
42 setTimeout: [ self showGameWinner: color ]
43 after: 1000.
44 !
46 hideMessage
47 (Silk at: '#message') element className: 'hidden'.
48 !
50 initializeControllers
51 controllerPrototypes := {
52 TronKeyboardController new
53 keyMap: #{
54 38 -> (0 @ -1).
55 39 -> (1 @ 0).
56 40 -> (0 @ 1).
57 37 -> (-1 @ 0)}
58 name: 'arrows'.
59 TronKeyboardController new
60 keyMap: #{
61 87 -> (0 @ -1).
62 68 -> (1 @ 0).
63 83 -> (0 @ 1).
64 65 -> (-1 @ 0)}
65 name: 'WASD'.
66 TronKeyboardController new
67 keyMap: #{
68 89 -> (0 @ -1).
69 74 -> (1 @ 0).
70 72 -> (0 @ 1).
71 71 -> (-1 @ 0)}
72 name: 'YGHJ'.
73 TronKeyboardController new
74 keyMap: #{
75 80 -> (0 @ -1).
76 222 -> (1 @ 0).
77 59 -> (0 @ 1).
78 76 -> (-1 @ 0)}
79 name: 'PL;'''.
80 TronComputerController1 new.
81 "TronRandomController new"
82 }.
83 (players at: 1) controller: (controllerPrototypes at: 5) copy.
84 (players at: 2) controller: (controllerPrototypes at: 1) copy.
85 (players at: 3) controller: (controllerPrototypes at: 5) copy.
86 (players at: 4) controller: (controllerPrototypes at: 5) copy.
87 !
89 initializePlayers
90 players := {
91 TronPlayer new
92 name: 'Player 1';
93 color: (playerColors at: 1).
94 TronPlayer new
95 name: 'Player 2';
96 color: (playerColors at: 2).
97 TronPlayer new
98 name: 'Player 3';
99 color: (playerColors at: 3);
100 enabled: false.
101 TronPlayer new
102 name: 'Player 4';
103 color: (playerColors at: 4);
104 enabled: false
105 }.
106 !
108 keyDown: event
109 startScreenVisible ifTrue: [ ^ self ].
110 "Handle Esc key."
111 (event keyCode = 27)
112 ifTrue: [
113 event preventDefault.
114 field stopTimer.
115 window clearTimeout: timeoutId.
116 self hideMessage; startScreenVisible: true.
117 ^ self ].
118 field keyDown: event.
119 !
121 nextRound
122 timeoutId := window
123 setTimeout: [
124 self status: self scoreDOM.
125 field start: field players ]
126 after: 2000.
127 !
129 randomizePlayerColors
130 | enabledPlayers |
131 enabledPlayers := players select: #isEnabled.
132 enabledPlayers do: [ :each |
133 | color |
134 [ color := playerColors atRandom.
135 enabledPlayers allSatisfy: [ :p |
136 p = each | (p color ~= color) ]
137 ] whileFalse.
138 each color: color.
139 ].
140 !
142 randomizeTeams
143 | colorA colorB teamA |
144 players do: [ :each | each enabled: true ].
145 colorA := playerColors atRandom.
146 [ colorB := playerColors atRandom.
147 colorB = colorA ] whileTrue.
148 teamA := players copy.
149 players size // 2
150 timesRepeat: [ (teamA remove: teamA atRandom) color: colorB ].
151 teamA do: [ :each | each color: colorA ].
152 !
154 renderColorSelectorFor: aPlayer on: aSilk
155 | container buttons onChange |
156 container := aSilk SPAN: {#class -> 'color-selector'}.
157 buttons := Dictionary from:
158 (playerColors collect: [ :each |
159 | button |
160 button := container A.
161 button on: #click bind: [
162 aPlayer enabled: true; color: each.
163 self validatePlayerColor: aPlayer ].
164 button element style background: each.
165 each -> button ]).
166 buttons at: #disabled put: (container A
167 SPAN: '✗';
168 on: #click bind: [
169 aPlayer enabled: false.
170 self validatePlayerColor: aPlayer ]).
171 onChange := [
172 buttons keysAndValuesDo: [ :k :v |
173 (aPlayer isEnabled not & k = #disabled) |
174 (aPlayer isEnabled & k = aPlayer color)
175 ifTrue: [ v element className: 'selected-color-button' ]
176 ifFalse: [ v element className: 'color-button' ] ] ].
177 aPlayer
178 onEnabledChange: onChange;
179 onColorChange: onChange.
180 onChange value.
181 !
183 renderControllerSelectorFor: aPlayer on: aSilk
184 | select options onChange |
185 select := aSilk SELECT.
186 options := Dictionary new.
187 controllerPrototypes do: [ :each |
188 options at: each name put: (select OPTION: each name) element ].
189 select
190 on: #change
191 bind: [
192 aPlayer controller:
193 (controllerPrototypes
194 detect: [ :each | each name = select element value ])
195 copy ].
196 onChange := [
197 | value |
198 value := aPlayer controller name.
199 (options at: value) selected: 'selected'.
200 aPlayer controller class = TronKeyboardController
201 ifTrue: [
202 players do: [ :each |
203 (each ~= aPlayer and: [ each controller name = value ])
204 ifTrue: [ each controller: TronComputerController1 new ]]]].
205 aPlayer onControllerChange: onChange.
206 onChange value.
207 !
209 renderPlayer: aPlayer on: aSilk
210 | container |
211 container := aSilk DIV: { #class -> 'player' }.
212 self renderColorSelectorFor: aPlayer on: container.
213 (container INPUT
214 on: #change bind: [ :event |
215 aPlayer name: event target value ])
216 element value: aPlayer name.
217 self renderControllerSelectorFor: aPlayer on: container.
218 !
220 renderStartScreenOn: aSilk
221 | startScreen |
222 startScreen := aSilk DIV: {#id -> 'start-screen'}.
224 players do: [ :each | self renderPlayer: each on: startScreen ].
226 (startScreen BUTTON: 'Random colors')
227 on: #click bind: [ self randomizePlayerColors ].
228 (startScreen BUTTON: 'Random teams')
229 on: #click bind: [ self randomizeTeams ].
231 startScreen BR.
233 (startScreen BUTTON: 'Start')
234 on: #click bind: [ :event | self startGame; status: self scoreDOM ].
235 !
237 scoreDOM
238 | message |
239 message := Silk SPAN: 'Score: '.
240 score associations
241 do: [ :each |
242 (message SPAN: {#class -> 'player-color'. (each value * 10) rounded / 10})
243 element style color: each key ]
244 separatedBy: [ message << ':' ].
245 message << ('/', pointsToWin).
246 ^ message.
247 !
249 showGameWinner: color
250 self showMessage: (self winnerDOM: color) << ' won the game!!'.
251 timeoutId := window
252 setTimeout: [
253 self
254 startScreenVisible: true;
255 hideMessage;
256 status: self scoreDOM << '. ' << (self winnerDOM: color) << ' won the game!!' ]
257 after: 1000.
258 !
260 showMessage: anObject
261 ((Silk at: '#message') resetContents << anObject)
262 element className: 'visible'.
263 !
265 startGame
266 | enabledPlayers |
267 self startScreenVisible: false.
268 field start: (players select: #isEnabled).
269 pointsToWin := (30 / (field players size + 1)) ceiling.
270 score := field colors collect: [ :each | 0 ].
271 !
273 startScreenVisible: aBoolean
274 startScreenVisible := aBoolean.
275 (Silk at: '#start-screen') element
276 className: (aBoolean ifTrue: [ 'visible' ] ifFalse: [ 'hidden' ]).
277 !
279 status: anObject
280 '#status' asSilk resetContents << anObject
281 !
283 updateScore
284 | color teamSize alive points defeated |
285 color := field winningColorIfNone: [ ^ self ].
286 teamSize := field colors at: color.
287 defeated := field players size - teamSize.
288 alive := field liveColors at: color.
289 points := defeated * alive / teamSize * (teamSize + 1) / 2.
290 points := points / (field players size - 1).
291 score at: color put: (score at: color) + points.
292 !
294 updateSize
295 | w h fw fh ratio |
296 w := window innerWidth.
297 h := window innerHeight - ('#title' asSilk element offsetHeight).
298 ratio := field fieldSize x / field fieldSize y.
299 (w / ratio <= h)
300 ifTrue: [ fw := w. fh := w / ratio ]
301 ifFalse: [ fh := h. fw := h * ratio ].
302 '#serpentron' asSilk element style
303 width: fw rounded asString, 'px';
304 marginLeft: ((w - fw) / 2) rounded asString, 'px';
305 marginTop: ((h - fh) / 2) rounded asString, 'px'.
306 '#field' asSilk element style
307 width: fw rounded asString, 'px';
308 height: fh rounded asString, 'px'.
309 !
311 validatePlayerColor: aPlayer
312 | enabledPlayers otherPlayers freeColor |
313 enabledPlayers := players select: #isEnabled.
314 ((enabledPlayers collect: #color) asSet size > 1)
315 ifTrue: [ ^ self ].
316 freeColor := playerColors detect: [ :each | each ~= enabledPlayers anyOne color ].
317 otherPlayers := players select: [ :each | each ~~ aPlayer ].
318 (otherPlayers
319 detect: [ :each | each isEnabled not ]
320 ifNone: [ otherPlayers first ])
321 enabled: true;
322 color: freeColor.
323 !
325 winnerDOM: color
326 | names team message |
327 team := field players select: [ :each | each color = color ].
328 names := ''.
329 (team collect: #name)
330 do: [ :each | names := names, each]
331 separatedBy: [ names := names, ' and ' ].
332 message := Silk SPAN.
333 (message SPAN: {#class -> 'player-color'. names})
334 element style color: color.
335 ^ message
336 ! !
338 !Serpentron methodsFor: 'rendering'!
340 augmentPage
341 Serpentron isCompatibleBrowser ifFalse: [
342 '#serpentron' asSilk resetContents
343 << 'Your browser is not supported.'
344 << Silk BR
345 << 'Please use a modern browser to run the game.'.
346 ^ self ].
347 '#serpentron' asSilk resetContents << 'Loading...'.
348 skin
349 load: 'resources/skin.png'
350 andDo: [ '#serpentron' asSilk resetContents << self ]
351 !
353 renderOnSilk: aSilk
354 | scale title container width height |
355 (title := aSilk DIV: {#id -> 'title'})
356 H1: (Silk A: { #href -> 'http://www.games1729.com/serpentron/'.
357 #target -> '_top'.
358 'Serpentron'});
359 SPAN: {#id -> 'status'. '1.0-beta2'}.
361 (title BUTTON: {#id -> 'fullscreen-button'.
362 #title -> 'Toggle fullscreen'.
363 '▣'})
364 on: #click bind: [ Serpentron toggleFullscreen ].
366 container := aSilk DIV: {#id -> 'field'}.
368 self updateSize.
369 window onresize: [ self updateSize ].
371 container << field.
372 self renderStartScreenOn: container.
374 (container DIV: {#id -> 'message'. #class -> 'hidden'})
375 element style
376 margin: '0 auto'.
377 ! !
379 Serpentron class instanceVariableNames: 'Instance'!
381 !Serpentron class methodsFor: 'compatibility'!
383 isCompatibleBrowser
384 "No reason to polyfill requestAnimationFrame
385 or use vendor prefixes as browsers that do not have it
386 will likely have other incompatibilities."
387 < return window.requestAnimationFrame && true || false >
388 !
390 toggleFullscreen
391 "Sample code from https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API"
392 <
393 if (!!document.fullscreenElement &&
394 !!document.mozFullScreenElement && !!document.webkitFullscreenElement && !!document.msFullscreenElement ) {
395 if (document.documentElement.requestFullscreen) {
396 document.documentElement.requestFullscreen();
397 } else if (document.documentElement.msRequestFullscreen) {
398 document.documentElement.msRequestFullscreen();
399 } else if (document.documentElement.mozRequestFullScreen) {
400 document.documentElement.mozRequestFullScreen();
401 } else if (document.documentElement.webkitRequestFullscreen) {
402 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
403 }
404 } else {
405 if (document.exitFullscreen) {
406 document.exitFullscreen();
407 } else if (document.msExitFullscreen) {
408 document.msExitFullscreen();
409 } else if (document.mozCancelFullScreen) {
410 document.mozCancelFullScreen();
411 } else if (document.webkitExitFullscreen) {
412 document.webkitExitFullscreen();
413 }
414 }
415 >
416 ! !
418 !Serpentron class methodsFor: 'starting'!
420 start
421 (Instance := self new) augmentPage
422 ! !
424 Object subclass: #TronCollider
425 instanceVariableNames: 'canvas context skin animationRequest lastFrameTime particles particlePool particlesPerCollision'
426 package: 'Serpentron'!
427 !TronCollider commentStamp!
428 Particle system for collision effect.!
430 !TronCollider methodsFor: 'accessing'!
432 canvas: aCanvas
433 canvas := aCanvas.
434 context := canvas getContext: '2d'.
435 !
437 skin: aTronSkin
438 skin := aTronSkin
439 ! !
441 !TronCollider methodsFor: 'initialization'!
443 initialize
444 super initialize.
445 particlesPerCollision := 200.
446 particlePool := Queue new.
447 particlesPerCollision * 2 + 10 timesRepeat: [
448 particlePool nextPut: TronColliderParticle new ].
449 particles := OrderedCollection new
450 ! !
452 !TronCollider methodsFor: 'private'!
454 renderFrame: timestamp
455 | liveParticles delta |
456 delta := lastFrameTime
457 ifNil: [ 0 ]
458 ifNotNil: [ timestamp - lastFrameTime ].
459 lastFrameTime := timestamp.
460 "Transcript show: 'renderFrame: ';
461 show: timestamp;
462 show: '; ';
463 show: delta;
464 cr."
465 liveParticles := OrderedCollection new.
466 context clearRect: 0 y: 0 w: canvas width h: canvas height.
467 particles do: [ :each |
468 each
469 update: delta;
470 drawOn: context tileSize: skin tileSize.
471 each
472 ifAlive: [ liveParticles add: each ]
473 ifNotAlive: [ particlePool nextPut: each ] ].
474 particles := liveParticles.
475 animationRequest := particles
476 ifEmpty: [ nil ]
477 ifNotEmpty: [
478 window
479 requestAnimationFrame: [ :ts |
480 self renderFrame: ts ]
481 on: canvas ]
482 !
484 startAnimation
485 animationRequest ifNotNil: [ ^ self ].
486 lastFrameTime := nil.
487 animationRequest := window
488 requestAnimationFrame: [ :ts | self renderFrame: ts ]
489 on: canvas.
490 ! !
492 !TronCollider methodsFor: 'starting'!
494 animateCollisionFor: aPlayer
495 | particle |
496 particlesPerCollision timesRepeat: [
497 particles add:
498 ((particlePool nextIfAbsent: [ TronColliderParticle new ])
499 resetPosition:
500 aPlayer location - 0.5 - (aPlayer direction / 2)
501 color: (Math random < 0.5
502 ifTrue: [ aPlayer color ]
503 ifFalse: [ '#ff3000' ])) ].
504 self startAnimation.
505 ! !
507 Object subclass: #TronColliderParticle
508 instanceVariableNames: 'color size velocity position alpha decay'
509 package: 'Serpentron'!
511 !TronColliderParticle methodsFor: 'accessing'!
513 ifAlive: aliveBlock ifNotAlive: notAliveBlock
514 ^ alpha > 0.01 ifTrue: aliveBlock ifFalse:notAliveBlock
515 !
517 resetPosition: positionPoint color: aString
518 position := positionPoint.
519 color := aString.
520 "Particles return to the pool as soon as they die.
521 Randomize on each reset or the pool will end up sotrted by particles' life time."
522 alpha := 1 - (Math random * 0.5).
523 decay := 0.997 - (Math random * 0.01)
524 ! !
526 !TronColliderParticle methodsFor: 'drawing'!
528 drawOn: aContext tileSize: tileSize
529 aContext
530 globalAlpha: alpha;
531 fillStyle: color;
532 fillRect: (position x - (size / 2)) * tileSize
533 y: (position y - (size / 2)) * tileSize
534 w: size * tileSize
535 h: size * tileSize.
536 ! !
538 !TronColliderParticle methodsFor: 'initialization'!
540 initialize
541 super initialize.
542 size := (Math random * 0.3 + 0.7).
543 velocity := (Math random - 0.5) @ (Math random - 0.5).
544 velocity := velocity / (velocity dist: 0 @ 0) * (Math random raisedTo: 4 + 0.1) * 0.06
545 ! !
547 !TronColliderParticle methodsFor: 'updating'!
549 update: delta
550 position := position + (velocity * delta).
551 alpha := alpha * (decay raisedTo: delta).
552 ! !
554 Object subclass: #TronController
555 instanceVariableNames: 'field player'
556 package: 'Serpentron'!
557 !TronController commentStamp!
558 Abstract superclass for controlling input devices and algorithms.!
560 !TronController methodsFor: 'accessing'!
562 field: aTronField
563 field := aTronField
564 !
566 name
567 self subclassResponsibility.
568 !
570 nextDirection: aPoint
571 "Do not turn back."
572 player nextDirection = (aPoint * (-1 @ -1))
573 ifTrue: [ ^ self ].
574 player nextDirection: aPoint.
575 !
577 player: aTronPlayer
578 player := aTronPlayer
579 ! !
581 !TronController methodsFor: 'computing'!
583 compute
584 !
586 isAtDecisionPoint
587 | loc dir turn |
588 player isFirstMove ifTrue: [ ^ true ].
589 dir := player nextDirection.
590 loc := player location.
591 (field isFreeAt: loc + dir)
592 ifFalse: [ ^ true ].
593 turn := dir rotate90ccw.
594 ((field isFreeAt: loc + turn)
595 and: [ (field isFreeAt: loc + dir + turn) not ])
596 ifTrue: [ ^ true ].
597 turn := dir rotate90cw.
598 ((field isFreeAt: loc + turn)
599 and: [ (field isFreeAt: loc + dir + turn) not ])
600 ifTrue: [ ^ true ].
601 ^ false
602 ! !
604 !TronController methodsFor: 'event handling'!
606 keyDown: keyCode
607 ^ false
608 ! !
610 !TronController methodsFor: 'initialization'!
612 reset
613 ! !
615 TronController class instanceVariableNames: 'directions'!
617 TronController subclass: #TronComputerController1
618 instanceVariableNames: 'weight aggressiveness'
619 package: 'Serpentron'!
621 !TronComputerController1 methodsFor: 'accessing'!
623 name
624 ^ 'Computer'
625 ! !
627 !TronComputerController1 methodsFor: 'computing'!
629 compute
630 | best bestDirection |
631 "player isFirstMove
632 ifTrue: [ self nextDirection: TronPlayer directions atRandom ]."
633 (self isAtDecisionPoint
634 or: [ (field isFreeAt: player location + (player nextDirection * 2)) not ])
635 ifFalse: [ ^ self ].
636 aggressiveness := 200 atRandom.
637 best := 0.
638 TronPlayer directions do: [ :each |
639 weight := 0.
640 self scan: each.
641 weight > best
642 ifTrue: [
643 best := weight.
644 bestDirection := each ]].
645 bestDirection ifNil: [ ^ self ].
646 self nextDirection: bestDirection.
647 ! !
649 !TronComputerController1 methodsFor: 'private'!
651 extend: directionPoint from: nwPoint to: sePoint
652 | point scanDir |
653 scanDir := directionPoint y abs @ directionPoint x abs.
654 point := Point
655 x: (directionPoint x <= 0 ifTrue: [ nwPoint x ] ifFalse: [ sePoint x ])
656 y: (directionPoint y <= 0 ifTrue: [ nwPoint y ] ifFalse: [ sePoint y ]).
657 [ point <= sePoint ]
658 whileTrue: [
659 weight := weight + 1.
660 (field isFreeAt: point)
661 ifFalse: [
662 (self isEnemyHeadAt: point)
663 ifTrue: [ weight := weight + aggressiveness ]
664 ifFalse: [ ^ false ] ].
665 point := point + scanDir ].
666 ^ true
667 !
669 isEnemyHeadAt: aPoint
670 (field playerWithHeadAt: aPoint)
671 ifNotNil: [ :p | ^ p color ~= player color ].
672 ^ false
673 !
675 scan: directionPoint
676 | nw se nextNW nextSE directions scanDir |
677 nw := se := player location + directionPoint.
678 (field isFreeAt: nw) ifFalse: [ ^ self ].
679 directions := TronPlayer directions copy
680 remove: directionPoint * -1;
681 yourself.
682 [ scanDir := directions atRandom.
683 scanDir <= 0 asPoint
684 ifTrue: [ nextNW := nw + scanDir. nextSE := se ]
685 ifFalse: [ nextNW := nw. nextSE := se + scanDir ].
686 (self extend: scanDir from: nextNW to: nextSE)
687 ifTrue: [ nw := nextNW. se := nextSE ]
688 ifFalse: [ directions remove: scanDir ].
689 directions isEmpty not
690 ] whileTrue.
691 ! !
693 TronController subclass: #TronKeyboardController
694 instanceVariableNames: 'keyMap name'
695 package: 'Serpentron'!
697 !TronKeyboardController methodsFor: 'accessing'!
699 keyMap: aDictionary name: aString
700 keyMap := aDictionary.
701 name := 'Keyboard: ', aString.
702 !
704 name
705 ^ name
706 ! !
708 !TronKeyboardController methodsFor: 'event handling'!
710 keyDown: keyCode
711 self nextDirection: (keyMap at: keyCode ifAbsent: [ ^ false ]).
712 ^ true
713 ! !
715 TronKeyboardController class instanceVariableNames: 'keyMaps'!
717 TronController subclass: #TronRandomController
718 instanceVariableNames: ''
719 package: 'Serpentron'!
721 !TronRandomController methodsFor: 'accessing'!
723 name
724 ^ 'Computer (random)'
725 ! !
727 !TronRandomController methodsFor: 'computing'!
729 compute
730 | dirs |
731 (self isAtDecisionPoint or: [ 50 atRandom = 1 ])
732 ifFalse: [ ^ self ].
733 dirs := TronPlayer directions
734 select: [ :each | field isFreeAt: player location + each ].
735 dirs ifNotEmpty: [ self nextDirection: dirs atRandom ]
736 ! !
738 Object subclass: #TronField
739 instanceVariableNames: 'skin size matrix players livePlayers colors liveColors canvas context collider timerId onEndGame'
740 package: 'Serpentron'!
741 !TronField commentStamp!
742 The game field. Provides game logic and rendering.!
744 !TronField methodsFor: 'accessing'!
746 at: aPoint
747 ^ (matrix at: aPoint y) at: aPoint x
748 !
750 at: aPoint ifAbsent: aBlock
751 ^ matrix
752 at: aPoint y
753 ifPresent: [ :value | value at: aPoint x ifAbsent: aBlock ]
754 ifAbsent: aBlock
755 !
757 at: aPoint put: aSegment
758 (matrix at: aPoint y) at: aPoint x put: aSegment
759 !
761 colors
762 ^ colors
763 !
765 fieldSize
766 ^ size
767 !
769 isFreeAt: aPoint
770 ^ (self at: aPoint ifAbsent: [ ^ false ]) isNil
771 !
773 isGameFinished
774 ^ liveColors size < 2
775 !
777 liveColors
778 ^ liveColors
779 !
781 playerWithHeadAt: aPoint
782 | s |
783 s := self at: aPoint ifAbsent: [ ^ nil ].
784 s isHead ifTrue: [ ^ s player ] ifFalse: [ ^ nil ]
785 !
787 players
788 ^ players
789 !
791 skin
792 ^ skin
793 !
795 skin: aTronSkin
796 skin := aTronSkin
797 !
799 winningColorIfNone: aBlock
800 ^ (liveColors size = 1)
801 ifTrue: [ liveColors keys anyOne ]
802 ifFalse: aBlock.
803 ! !
805 !TronField methodsFor: 'event handling'!
807 keyDown: event
808 "Check all players rather than livePlayers
809 so that preventDefault is called for all used keys."
810 (players anySatisfy: [ :each | each keyDown: event keyCode ])
811 ifTrue: [ event preventDefault ].
812 timerId ifNil: [ self startIfAllReady ].
813 ! !
815 !TronField methodsFor: 'initialization'!
817 initialize
818 super initialize.
819 livePlayers := players := #().
820 size := 50 @ 35
821 ! !
823 !TronField methodsFor: 'observing'!
825 onEndGame: aBlock
826 onEndGame := aBlock
827 ! !
829 !TronField methodsFor: 'private'!
831 endGame
832 self stopTimer.
833 livePlayers := #().
834 onEndGame ifNotNil: #value.
835 !
837 killPlayer: aPlayer
838 collider animateCollisionFor: aPlayer.
839 livePlayers remove: aPlayer.
840 self updateLiveColors.
841 !
843 locatePlayers
844 players size = 2 ifTrue: [
845 (players at: 1) location: 16 @ 18.
846 (players at: 2) location: 35 @ 18.
847 ^ self ].
848 players size = 3 ifTrue: [
849 (players at: 1) location: 18 @ 13.
850 (players at: 2) location: 18 @ 23.
851 (players at: 3) location: 33 @ 18.
852 ^ self ].
853 players size = 4 ifTrue: [
854 (players at: 1) location: 16 @ 11.
855 (players at: 2) location: 35 @ 11.
856 (players at: 3) location: 16 @ 25.
857 (players at: 4) location: 35 @ 25.
858 ^ self ].
859 self error: 'Invalid number of players.'
860 !
862 renderCanvasOn: aSilk
863 ^ aSilk CANVAS: {
864 #width -> (size x * skin tileSize).
865 #height -> (size y * skin tileSize)}.
866 !
868 startIfAllReady
869 (livePlayers isEmpty not
870 and: [ livePlayers allSatisfy: #isReady ])
871 ifTrue: [ self startTimer ]
872 !
874 startTimer
875 timerId ifNotNil: [ self error: 'Timer already running.' ].
876 timerId := window setInterval: [ self update ] every: 75.
877 !
879 update
880 | lpCopy |
881 lpCopy := livePlayers copy.
882 lpCopy do: [ :each |
883 | l |
884 l := each location.
885 self at: l put: each move.
886 skin drawField: self on: context at: l ].
887 lpCopy do: [ :each |
888 | l other |
889 l := each location.
890 (self isFreeAt: l)
891 ifTrue: [ self at: l put: each headSegment. ]
892 ifFalse: [
893 self killPlayer: each.
894 "Check for head-to-head collision."
895 other := self playerWithHeadAt: l.
896 (other isNil | (other = each))
897 ifFalse: [
898 self killPlayer: other.
899 self at: l put: nil. ]]].
900 livePlayers do: [ :each |
901 skin drawField: self on: context at: each location ].
902 self isGameFinished ifTrue: [ ^ self endGame ].
903 livePlayers do: #compute.
904 !
906 updateLiveColors
907 liveColors := Dictionary new.
908 livePlayers do: [ :each |
909 liveColors
910 at: each color
911 ifPresent: [ :v | liveColors at: each color put: v + 1 ]
912 ifAbsent: [ liveColors at: each color put: 1 ]].
913 ! !
915 !TronField methodsFor: 'rendering'!
917 renderOnSilk: aSilk
918 | backgroundCanvas |
920 backgroundCanvas := (self renderCanvasOn: aSilk) element.
922 canvas := (self renderCanvasOn: aSilk) element.
923 context := canvas getContext: '2d'.
925 collider := TronCollider new
926 canvas: (self renderCanvasOn: aSilk) element;
927 skin: skin.
929 skin drawBackgroundOn: (backgroundCanvas getContext: '2d') from: 1 @ 1 to: size
930 ! !
932 !TronField methodsFor: 'starting'!
934 start: anArrayOfPlayers
935 players := anArrayOfPlayers.
936 livePlayers := players copy.
937 self updateLiveColors.
938 colors := liveColors.
939 matrix := (1 to: size y) collect: [ :i | Array new: size x ].
940 context clearRect: 0 y: 0 w: canvas width h: canvas height.
941 self locatePlayers.
942 players do: [ :each | each field: self; reset; compute ].
943 players do: [ :each |
944 self at: each location put: each headSegment.
945 skin drawField: self on: context at: each location ].
946 self startIfAllReady.
947 ! !
949 !TronField methodsFor: 'stopping'!
951 stopTimer
952 timerId ifNotNil: [
953 window clearInterval: timerId.
954 timerId := nil ]
955 ! !
957 Object subclass: #TronPlayer
958 instanceVariableNames: 'enabled color name controller segments location moved direction nextDirection onColorChange onEnabledChange onControllerChange'
959 package: 'Serpentron'!
961 !TronPlayer methodsFor: 'accessing'!
963 color
964 ^ color
965 !
967 color: anObject
968 color := anObject.
969 onColorChange ifNotNil: #value.
970 !
972 controller
973 ^ controller
974 !
976 controller: aTronController
977 aTronController player: self.
978 controller := aTronController.
979 onControllerChange ifNotNil: #value.
980 !
982 direction
983 ^ direction
984 !
986 enabled: aBoolean
987 enabled := aBoolean.
988 onEnabledChange ifNotNil: #value.
989 !
991 field: aField
992 controller field: aField
993 !
995 headSegment
996 ^ segments at: direction
997 !
999 isEnabled
1000 ^ enabled
1003 isFirstMove
1004 ^ moved not
1007 isReady
1008 ^ nextDirection notNil
1011 location
1012 ^ location
1015 location: anObject
1016 location := anObject
1019 name
1020 ^ name
1023 name: anObject
1024 name := anObject.
1027 nextDirection
1028 ^ nextDirection
1031 nextDirection: aPoint
1032 moved ifFalse: [ direction := aPoint ].
1033 nextDirection := aPoint
1034 ! !
1036 !TronPlayer methodsFor: 'event handling'!
1038 keyDown: keyCode
1039 ^ controller keyDown: keyCode
1040 ! !
1042 !TronPlayer methodsFor: 'initialization'!
1044 initialize
1045 super initialize.
1046 enabled := true.
1047 self reset; initializeSegments.
1050 reset
1051 direction := 0 @ -1.
1052 nextDirection := nil.
1053 moved := false.
1054 controller ifNotNil: [ controller reset ].
1055 ! !
1057 !TronPlayer methodsFor: 'observing'!
1059 onColorChange: aBlock
1060 onColorChange := aBlock.
1063 onControllerChange: aBlock
1064 onControllerChange := aBlock.
1067 onEnabledChange: aBlock
1068 onEnabledChange := aBlock.
1069 ! !
1071 !TronPlayer methodsFor: 'private'!
1073 initializeSegments
1074 segments := Dictionary new.
1075 TronPlayer directionNames keysAndValuesDo: [ :to :toName |
1076 segments
1077 at: to
1078 put: (TronHead new
1079 player: self;
1080 sprite: 'head', toName;
1081 direction: to);
1082 at: {to. to}
1083 put: (TronSegment new
1084 player: self;
1085 sprite: 'body', toName).
1086 TronPlayer directionNames keysAndValuesDo: [ :from :fromName |
1087 (from x = to x) | (from y = to y) ifFalse: [
1088 segments
1089 at: {from. to}
1090 put: (TronSegment new
1091 player: self;
1092 sprite: 'body', fromName, 'To', toName) ]]].
1093 ! !
1095 !TronPlayer methodsFor: 'updating'!
1097 compute
1098 controller compute
1101 move
1102 | segment |
1103 segment := segments at: {direction. nextDirection} ifAbsent: [ nil ].
1104 direction := nextDirection.
1105 location := location + direction.
1106 moved := true.
1107 ^ segment
1108 ! !
1110 TronPlayer class instanceVariableNames: 'directions directionNames'!
1112 !TronPlayer class methodsFor: 'initialization'!
1114 directionNames
1115 ^ directionNames
1118 directions
1119 ^ directions
1122 initialize
1123 super initialize.
1124 directionNames := Dictionary new
1125 at: 0 @ -1 put: 'North';
1126 at: 1 @ 0 put: 'East';
1127 at: 0 @ 1 put: 'South';
1128 at: -1 @ 0 put: 'West';
1129 yourself.
1130 directions := directionNames keys.
1131 ! !
1133 Object subclass: #TronSegment
1134 instanceVariableNames: 'player sprite'
1135 package: 'Serpentron'!
1137 !TronSegment methodsFor: 'accessing'!
1139 color
1140 ^ player color
1143 isHead
1144 ^ false
1147 player
1148 ^ player
1151 player: anObject
1152 player := anObject
1155 sprite
1156 ^ sprite
1159 sprite: anObject
1160 sprite := anObject
1161 ! !
1163 TronSegment subclass: #TronHead
1164 instanceVariableNames: 'direction'
1165 package: 'Serpentron'!
1167 !TronHead methodsFor: 'accessing'!
1169 direction
1170 ^ direction
1173 direction: anObject
1174 direction := anObject
1177 isHead
1178 ^ true
1179 ! !
1181 Object subclass: #TronSkin
1182 instanceVariableNames: 'skinMap skinImage tileSize maskOffset'
1183 package: 'Serpentron'!
1185 !TronSkin methodsFor: 'accessing'!
1187 tileSize
1188 ^ tileSize
1189 ! !
1191 !TronSkin methodsFor: 'drawing'!
1193 drawBackgroundOn: aContext at: aPoint
1194 self drawTile: #background offset: aPoint - 1 on: aContext at: aPoint;
1195 drawTile: #backgroundTile on: aContext at: aPoint
1198 drawBackgroundOn: aContext from: nwPoint to: sePoint
1199 nwPoint y to: sePoint y do: [ :row |
1200 nwPoint x to: sePoint x do: [ :col |
1201 self drawBackgroundOn: aContext at: col @ row ]]
1204 drawField: aField on: aContext at: aPoint
1205 | x y segment |
1207 x := (aPoint - 1) x * tileSize.
1208 y := (aPoint - 1) y * tileSize.
1210 aContext
1211 clearRect: x and: y and: tileSize and: tileSize.
1213 segment := (aField at: aPoint) ifNil: [ ^ self ].
1215 self
1216 drawTile: segment sprite
1217 offset: maskOffset
1218 on: aContext
1219 at: aPoint.
1220 aContext
1221 globalCompositeOperation: 'source-atop';
1222 fillStyle: segment color;
1223 fillRect: x and: y and: tileSize and: tileSize.
1224 self drawTile: segment sprite on: aContext at: aPoint.
1226 aContext globalCompositeOperation: 'source-over'.
1229 drawSkinImageOn: aContext source: sourcePoint destination: destinationPoint
1230 aContext drawImage: skinImage
1231 sx: sourcePoint x
1232 sy: sourcePoint y
1233 sw: tileSize
1234 sh: tileSize
1235 dx: destinationPoint x
1236 dy: destinationPoint y
1237 dw: tileSize
1238 dh: tileSize
1241 drawTile: aSymbol offset: offsetPoint on: aContext at: targetPoint
1242 self
1243 drawSkinImageOn: aContext
1244 source: ((skinMap at: aSymbol) + offsetPoint) * tileSize
1245 destination: (targetPoint - 1) * tileSize
1248 drawTile: aSymbol on: aContext at: aPoint
1249 self drawTile: aSymbol offset: 0@0 on: aContext at: aPoint
1250 ! !
1252 !TronSkin methodsFor: 'initialization'!
1254 initialize
1255 super initialize.
1256 skinMap := Dictionary new
1257 at: #background put: 0@0;
1258 at: #backgroundBottomRight put: 49@34;
1259 at: #backgroundTile put: 0@35;
1260 at: #headNorth put: 1@35;
1261 at: #headEast put: 2@35;
1262 at: #headSouth put: 3@35;
1263 at: #headWest put: 4@35;
1264 at: #bodyNorth put: 5@35;
1265 at: #bodyEast put: 6@35;
1266 at: #bodySouth put: 7@35;
1267 at: #bodyWest put: 8@35;
1268 at: #bodySouthToEast put: 9@35;
1269 at: #bodyWestToNorth put: 9@35;
1270 at: #bodyNorthToEast put: 10@35;
1271 at: #bodyWestToSouth put: 10@35;
1272 at: #bodyNorthToWest put: 11@35;
1273 at: #bodyEastToSouth put: 11@35;
1274 at: #bodySouthToWest put: 12@35;
1275 at: #bodyEastToNorth put: 12@35;
1276 yourself.
1277 maskOffset := 12 @ 0.
1278 skinImage := document createElement: 'img'
1279 ! !
1281 !TronSkin methodsFor: 'loading'!
1283 load: url andDo: aBlock
1284 skinImage onload: [
1285 tileSize := skinImage width / 50.
1286 aBlock value ].
1287 skinImage src: url.
1288 ! !
1290 !Point methodsFor: '*Serpentron'!
1292 rotate90ccw
1293 ^ self y @ self x negated
1296 rotate90cw
1297 ^ self y negated @ self x
1298 ! !