forked from lichess-org/lila
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathChapter.scala
183 lines (141 loc) · 5.14 KB
/
Chapter.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package lila.study
import chess.format.pgn.{ Glyph, Tag, Tags }
import chess.format.FEN
import chess.variant.Variant
import chess.{ Color, Centis }
import org.joda.time.DateTime
import chess.opening.{ FullOpening, FullOpeningDB }
import lila.tree.Node.{ Shapes, Comment, Gamebook }
import lila.user.User
case class Chapter(
_id: Chapter.Id,
studyId: Study.Id,
name: Chapter.Name,
setup: Chapter.Setup,
root: Node.Root,
tags: Tags,
order: Int,
ownerId: User.ID,
conceal: Option[Chapter.Ply] = None,
practice: Option[Boolean] = None,
gamebook: Option[Boolean] = None,
description: Option[String] = None,
relay: Option[Chapter.Relay] = None,
serverEval: Option[Chapter.ServerEval] = None,
createdAt: DateTime
) extends Chapter.Like {
def updateRoot(f: Node.Root => Option[Node.Root]) =
f(root) map { newRoot =>
copy(root = newRoot)
}
def addNode(node: Node, path: Path, newRelay: Option[Chapter.Relay] = None): Option[Chapter] =
updateRoot {
_.withChildren(_.addNodeAt(node, path))
} map {
_.copy(relay = newRelay orElse relay)
}
def setShapes(shapes: Shapes, path: Path): Option[Chapter] =
updateRoot(_.setShapesAt(shapes, path))
def setComment(comment: Comment, path: Path): Option[Chapter] =
updateRoot(_.setCommentAt(comment, path))
def setGamebook(gamebook: Gamebook, path: Path): Option[Chapter] =
updateRoot(_.setGamebookAt(gamebook, path))
def deleteComment(commentId: Comment.Id, path: Path): Option[Chapter] =
updateRoot(_.deleteCommentAt(commentId, path))
def toggleGlyph(glyph: Glyph, path: Path): Option[Chapter] =
updateRoot(_.toggleGlyphAt(glyph, path))
def setClock(clock: Option[Centis], path: Path): Option[Chapter] =
updateRoot(_.setClockAt(clock, path))
def forceVariation(force: Boolean, path: Path): Option[Chapter] =
updateRoot(_.forceVariationAt(force, path))
def opening: Option[FullOpening] =
if (!Variant.openingSensibleVariants(setup.variant)) none
else FullOpeningDB searchInFens root.mainline.map(_.fen)
def isEmptyInitial = order == 1 && root.children.nodes.isEmpty
def cloneFor(study: Study) = copy(
_id = Chapter.makeId,
studyId = study.id,
ownerId = study.ownerId,
createdAt = DateTime.now
)
def metadata = Chapter.Metadata(_id = _id, name = name, setup = setup)
def isPractice = ~practice
def isGamebook = ~gamebook
def isConceal = conceal.isDefined
def withoutChildren = copy(root = root.withoutChildren)
def withoutChildrenIfPractice = if (isPractice) copy(root = root.withoutChildren) else this
def relayAndTags = relay map { Chapter.RelayAndTags(id, _, tags) }
def isOverweight = root.children.countRecursive >= Chapter.maxNodes
}
object Chapter {
// I've seen chapters with 35,000 nodes on prod.
// It works but could be used for DoS.
val maxNodes = 3000
case class Id(value: String) extends AnyVal with StringValue
implicit val idIso = lila.common.Iso.string[Id](Id.apply, _.value)
case class Name(value: String) extends AnyVal with StringValue
implicit val nameIso = lila.common.Iso.string[Name](Name.apply, _.value)
sealed trait Like {
val _id: Chapter.Id
val name: Chapter.Name
val setup: Chapter.Setup
def id = _id
def initialPosition = Position.Ref(id, Path.root)
}
case class Setup(
gameId: Option[lila.game.Game.ID],
variant: Variant,
orientation: Color,
fromFen: Option[Boolean] = None
) {
def isFromFen = ~fromFen
}
case class Relay(
index: Int, // game index in the source URL
path: Path,
lastMoveAt: DateTime
) {
def secondsSinceLastMove: Int = (nowSeconds - lastMoveAt.getSeconds).toInt
}
case class ServerEval(path: Path, done: Boolean)
case class RelayAndTags(id: Id, relay: Relay, tags: Tags) {
def looksAlive =
tags.resultColor.isEmpty &&
relay.lastMoveAt.isAfter {
DateTime.now.minusMinutes {
tags.clockConfig.fold(40)(_.limitInMinutes.toInt / 2 atLeast 15 atMost 60)
}
}
def looksOver = !looksAlive
}
case class Metadata(
_id: Id,
name: Name,
setup: Setup
) extends Like
case class IdName(id: Id, name: Name)
case class Ply(value: Int) extends AnyVal with Ordered[Ply] {
def compare(that: Ply) = value - that.value
}
def defaultName(order: Int) = Name(s"Chapter $order")
private val defaultNameRegex = """Chapter \d+""".r
def isDefaultName(n: Name) = n.value.isEmpty || defaultNameRegex.matches(n.value)
def fixName(n: Name) = Name(n.value.trim take 80)
val idSize = 8
def makeId = Id(scala.util.Random.alphanumeric take idSize mkString)
def make(studyId: Study.Id, name: Name, setup: Setup, root: Node.Root, tags: Tags, order: Int, ownerId: User.ID, practice: Boolean, gamebook: Boolean, conceal: Option[Ply], relay: Option[Relay] = None) = Chapter(
_id = makeId,
studyId = studyId,
name = fixName(name),
setup = setup,
root = root,
tags = tags,
order = order,
ownerId = ownerId,
practice = practice option true,
gamebook = gamebook option true,
conceal = conceal,
relay = relay,
createdAt = DateTime.now
)
}