Skip to content

Commit 1985a11

Browse files
committed
Fix user input getting dropped by server lag compensation when another client submits older input
1 parent b316d7a commit 1985a11

File tree

7 files changed

+115
-37
lines changed

7 files changed

+115
-37
lines changed

public/javascripts/application.js

+21-9
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,24 @@ $.widget("ui.masterCanvas", $.extend({}, $.ui.abstractCellCanvas.prototype, {
104104
},
105105

106106
_newWorldFromServerSinceLastPaint: function() {
107-
return this._lastServerTick != this._lastPaintedServerTick;
107+
return this.lastServerTick() != this._lastPaintedServerTick;
108108
},
109109

110-
_setLastServerTick: function(tick) {
111-
this._lastServerTick = tick;
112-
$('.lastServerTick').html(tick);
110+
lastServerTick: function() {
111+
if (this._lastServerResponse) {
112+
return this._lastServerResponse.tick;
113+
} else {
114+
return null;
115+
}
116+
},
117+
118+
currentTick: function() {
119+
return this.lastServerTick() + this._predictedTicks;
120+
},
121+
122+
_setLastServerResponse: function(response) {
123+
this._lastServerResponse = response;
124+
$('.lastServerTick').html(response.tick);
113125
},
114126

115127
_setPredictedTicks: function(ticks) {
@@ -148,8 +160,8 @@ $.widget("ui.masterCanvas", $.extend({}, $.ui.abstractCellCanvas.prototype, {
148160

149161
this.stopRunningOnInputPrediction();
150162

151-
if (!this._lastServerTick || (this._lastServerTick < response.tick)) {
152-
this._setLastServerTick(response.tick);
163+
if (!this.lastServerTick() || (this.lastServerTick() < response.tick)) {
164+
this._setLastServerResponse(response);
153165
this._setPredictedTicks(0);
154166
this.updateCells(response.cells);
155167

@@ -170,7 +182,7 @@ $.widget("ui.masterCanvas", $.extend({}, $.ui.abstractCellCanvas.prototype, {
170182

171183
adjustDimensionsToMatchWidth: function() {
172184
if (this.element.width() != this._widthDimensionsWereBasedOn) {
173-
var impl = function() {
185+
function impl() {
174186
var currentWidth = this.element.width();
175187

176188
this.cellSize = currentWidth / this.width;
@@ -198,7 +210,7 @@ $.widget("ui.masterCanvas", $.extend({}, $.ui.abstractCellCanvas.prototype, {
198210

199211
_repaintWorld:function() {
200212
this.adjustDimensionsToMatchWidth();
201-
this._lastPaintedServerTick = this._lastServerTick;
213+
this._lastPaintedServerTick = this.lastServerTick();
202214

203215
for (var x = 0; x < this.width; x++) {
204216
for (var y = 0; y < this.height; y++) {
@@ -293,7 +305,7 @@ var polygod = {
293305
$.ajax({
294306
type: 'POST',
295307
url: 'world',
296-
data: JSON.stringify({cells: cells, tick: cellCanvas._lastServerTick + cellCanvas._predictedTicks}),
308+
data: JSON.stringify({cells: cells, tick: cellCanvas.currentTick()}),
297309
dataType: 'json',
298310
cache: false,
299311
success: function(data) { cellCanvas.runWithInputPredictionUntilTick(data.nextServerTickWithUpdate) },

src/Timeline.hs

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interfereAt inputTick f timeline = atomically $ do
6363
slice <- readTVar (tlTVar timeline)
6464
case addUserInput inputTick f slice of
6565
Just slice' -> do writeTVar (tlTVar timeline) slice'
66+
let !w' = world slice' -- compute world strictly to force exception in case user function is bad
6667
return $ Just (tick slice' + 1)
6768
_ -> return Nothing
6869

src/Timeline/Slice.hs

+54-19
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,42 @@ import Life
1515
type Tick = Word16
1616
type HistorySize = Word8
1717
type Projector a = World -> Tick -> a
18-
data Slice a = Slice { projection :: a, world :: !World, tick :: !Tick, slHistory :: [(Tick, World)], slUserWorlds :: [World] }
18+
type UserInputs = [World -> World]
19+
20+
data Slice a = Slice { projection :: a, slHistory :: HistoricalRecord }
21+
data HistoricalRecord = Evolution { hrTick::Tick, hrInput::UserInputs, hrPrevious::HistoricalRecord }
22+
| BaseState { hrTick::Tick, hrInput::UserInputs, baseWorld::World }
1923

2024
instance Show (Slice a) where
2125
show slice = "World size " ++ (show $ size (world slice)) ++
2226
" at tick " ++ (show $ tick slice) ++
23-
" with " ++ (show $ length (slHistory slice)) ++ " historical records"
27+
" with " ++ (show $ hrLength (slHistory slice)) ++ " historical records"
2428

2529
maxCloseTickDifference :: Word16
2630
maxCloseTickDifference = 11
2731

32+
tick :: Slice a -> Tick
33+
tick = hrTick . slHistory
34+
35+
world :: Slice a -> World
36+
world = hrWorld . slHistory
37+
38+
hrIsBase :: HistoricalRecord -> Bool
39+
hrIsBase (BaseState _ _ _) = True
40+
hrIsBase _ = False
41+
42+
hrLength :: HistoricalRecord -> Int
43+
hrLength hr | hrIsBase hr = 1
44+
| otherwise = 1 + hrLength (hrPrevious hr)
45+
46+
hrWorld :: HistoricalRecord -> World
47+
hrWorld hr | null (hrInput hr) = hrWorld' hr
48+
| otherwise = foldl1' merge worldsWithUserInput
49+
50+
where hrWorld' hr' | hrIsBase hr' = baseWorld hr'
51+
| otherwise = evolve . hrWorld . hrPrevious $ hr'
52+
worldsWithUserInput = map ($ hrWorld' hr) (hrInput hr)
53+
2854
farApart :: Tick -> Tick -> Bool
2955
farApart tick1 tick2 = tick2 - tick1 > maxCloseTickDifference && tick1 - tick2 > maxCloseTickDifference
3056

@@ -33,27 +59,36 @@ t1 `after` t2 | farApart t1 t2 = True
3359
| otherwise = (t1 - t2) < (t2 - t1)
3460

3561
newSlice :: Projector a -> World -> Tick -> Slice a
36-
newSlice f w t = Slice { world = w, tick = t, projection = f w t, slHistory = [(t,w)], slUserWorlds = [] }
62+
newSlice f w t = Slice { projection = f w t
63+
, slHistory = BaseState { hrTick = t
64+
, baseWorld = w
65+
, hrInput=[]
66+
}
67+
}
3768

3869
nextSlice :: Projector b -> Slice a -> Slice b
39-
nextSlice f s = let tick' = (tick s) + 1
40-
world' | (not . null) (slUserWorlds s) = evolve (foldl1' merge $ slUserWorlds s)
41-
| otherwise = evolve (world s)
42-
in Slice { tick = tick'
43-
, projection = f world' tick'
44-
, world = world'
45-
, slHistory = (tick', world') : (slHistory s)
46-
, slUserWorlds = []
47-
}
70+
nextSlice f s = let s' = Slice { projection = f (world s') (tick s')
71+
, slHistory = Evolution { hrTick = tick s + 1
72+
, hrInput=[]
73+
, hrPrevious = slHistory s
74+
}
75+
}
76+
in s'
4877

4978
trimHistory :: HistorySize -> Slice a -> Slice a
50-
trimHistory n s = s { slHistory = genericTake n (slHistory s) }
79+
trimHistory n s = s { slHistory = trimHistory' n (slHistory s) }
80+
where trimHistory' _ base | hrIsBase base = base
81+
trimHistory' 1 hr = BaseState { hrTick = hrTick hr
82+
, hrInput = hrInput hr
83+
, baseWorld = evolve . hrWorld . hrPrevious $ hr
84+
}
85+
trimHistory' n' hr = hr { hrPrevious = trimHistory' (n' - 1) (hrPrevious hr) }
5186

5287
addUserInput :: Tick -> (World -> World) -> Slice a -> Maybe (Slice a)
53-
addUserInput inputTick f s = do worldAtTimeOfInput <- lookup inputTick (slHistory s)
54-
-- evaluating strictly so any exceptions will be raised while request is still being handled
55-
let !userWorld = evolveUpTo (f worldAtTimeOfInput) inputTick (tick s)
56-
return $ s { slUserWorlds = userWorld : slUserWorlds s }
57-
where evolveUpTo w t t' | t == t' = w
58-
| otherwise = evolveUpTo (evolve w) (t + 1) t'
88+
addUserInput inputTick f s = do newHistory <- addUserInput' (slHistory s)
89+
return $ s { slHistory = newHistory }
90+
where addUserInput' hr | hrTick hr == inputTick = return $ hr { hrInput = f : hrInput hr }
91+
| hrIsBase hr = Nothing
92+
| otherwise = do newPrevious <- addUserInput' (hrPrevious hr)
93+
return $ hr { hrPrevious = newPrevious }
5994

src/Util.hs

+4
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ jsonProperty _ _ = Nothing
1919
jsonNumber :: JSON -> Maybe Rational
2020
jsonNumber (Number r) = Just r
2121
jsonNumber _ = Nothing
22+
23+
aps :: Num n => n -> (a -> a) -> a -> a
24+
aps 0 _ a = a
25+
aps n f a = aps (n - 1) f (f a)

test/TestHelper.hs

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module TestHelper
66
, module Test.HUnit
77
, module ArbitraryInstances
88
, module Debug.Trace
9+
, module Util
910
, dbg
1011
)
1112
where
@@ -20,5 +21,7 @@ import Test.QuickCheck hiding (Testable)
2021
import Test.HUnit hiding (Test, Testable)
2122
import ArbitraryInstances
2223

24+
import Util
25+
2326
dbg :: Show a => String -> a -> b -> b
2427
dbg s a = trace (s ++ ": " ++ show a)

test/Timeline/SliceTests.hs

+31-8
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,56 @@ tests = testGroup "Timeline.Slice" [
4242

4343
Just sliceWithInput = addUserInput (tick slice) (drawPatternAt (0,0) input) slice
4444
slice' = nextSlice projector sliceWithInput
45-
worldWithInput = drawPatternAt (0,0) input $ world slice
45+
expectedWorldWithInput = drawPatternAt (0,0) input $ world slice
4646

47-
in world slice' == evolve worldWithInput &&
48-
world (nextSlice projector slice') == (evolve . evolve) worldWithInput
47+
in world slice' == evolve expectedWorldWithInput &&
48+
world (nextSlice projector slice') == (evolve . evolve) expectedWorldWithInput
4949

5050
,"nextSlice unions multiple user inputs" `testProperty` \slice (Blind projector) input1 input2 ->
5151
let _ = (slice, projector) :: (Slice (), Projector ())
5252

5353
Just sliceWithInput1 = addUserInput (tick slice) (drawPatternAt (0,0) input1) slice
5454
Just sliceWithInput2 = addUserInput (tick slice) (drawPatternAt (0,0) input2) sliceWithInput1
5555

56-
worldWithInput1 = drawPatternAt (0,0) input1 (world slice)
57-
worldWithInput2 = drawPatternAt (0,0) input2 (world slice)
56+
expectedWorldWithInput1 = drawPatternAt (0,0) input1 (world slice)
57+
expectedWorldWithInput2 = drawPatternAt (0,0) input2 (world slice)
5858

59-
in world (nextSlice projector sliceWithInput2) == evolve (merge worldWithInput1 worldWithInput2)
59+
in world (nextSlice projector sliceWithInput2) == evolve (merge expectedWorldWithInput1 expectedWorldWithInput2)
6060

6161
,"nextSlice evolves world from user historical input" `testProperty` \slice (Blind projector) input ->
6262
let _ = (slice, projector) :: (Slice (), Projector ())
63-
futureSlice = foldl (.) id (replicate 3 $ nextSlice projector) slice
63+
futureSlice = (3 `aps` nextSlice projector) slice
6464
Just futureSliceWithInput = addUserInput (tick slice) (drawPatternAt (0,0) input) futureSlice
6565
slice' = nextSlice projector futureSliceWithInput
66-
in world slice' == foldl (.) id (replicate 4 $ evolve) (drawPatternAt (0,0) input $ world slice)
66+
in world slice' == (4 `aps` evolve) (drawPatternAt (0,0) input $ world slice)
67+
68+
,"nextSlice unions multiple user inputs at multiple time points" `testProperty` \slice (Blind projector) input1 input2 ->
69+
let _ = (slice, projector) :: (Slice (), Projector ())
70+
input1Tick = tick slice + 1
71+
input2Tick = input1Tick - 1
72+
73+
sliceAtInput1 = (2 `aps` nextSlice projector) slice
74+
Just sliceWithInput1 = addUserInput input1Tick (drawPatternAt (0,0) input1) sliceAtInput1
75+
76+
sliceAtInput2 = nextSlice projector sliceWithInput1
77+
Just sliceWithInput2 = addUserInput input2Tick (drawPatternAt (0,0) input2) sliceAtInput2
78+
79+
expectedWorldWithInput2 = drawPatternAt (0,0) input2 (world slice)
80+
expectedWorldWithInput1 = drawPatternAt (0,0) input1 (evolve expectedWorldWithInput2)
81+
82+
in world (nextSlice projector sliceWithInput2) == (3 `aps` evolve) expectedWorldWithInput1
83+
6784

6885
,"addUserInput returns Nothing when input tick is outside history" `testProperty` \slice (Blind projector) input ->
6986
let _ = (slice, projector) :: (Slice (), Projector ())
7087
futureSlice = trimHistory 1 $ nextSlice projector slice
7188
in isNothing (addUserInput (tick slice) (drawPatternAt (0,0) input) futureSlice) == True
89+
90+
,"trimHistory does not stop evolution" `testProperty` \slice (Blind projector) ->
91+
let _ = (slice, projector) :: (Slice (), Projector ())
92+
slice' = (4 `aps` nextSlice projector) slice
93+
slice'' = trimHistory 2 slice'
94+
in world slice' == world slice''
7295
]
7396

7497
data CloseTogether = CloseTogether Tick Tick deriving (Show)

test/TimelineTests.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ tests = testGroup "Timeline" [
1818
`catch` (\e -> return $ show (e :: SomeException))
1919
x @?= "Caught It"
2020

21-
,"interefereAt returns the next tick that will include the change" `testCase` do
21+
,"interfereAt returns the next tick that will include the change" `testCase` do
2222
withTimeline (defaultConfig { tlTickDelay = 10000 }) $ \timeline -> do
2323
sliceAfter 2 timeline
2424
interfereAt 1 id timeline >>= (@?= Just 4)

0 commit comments

Comments
 (0)