study gamebook mode WIP
parent
43a994eb38
commit
7861ddfd34
|
@ -10,7 +10,7 @@ import reactivemongo.bson._
|
|||
import lila.db.BSON
|
||||
import lila.db.BSON.{ Reader, Writer }
|
||||
import lila.db.dsl._
|
||||
import lila.tree.Node.{ Shape, Shapes }
|
||||
import lila.tree.Node.{ Shape, Shapes, Comment, Comments, Gamebook }
|
||||
|
||||
import lila.common.Iso._
|
||||
import lila.common.Iso
|
||||
|
@ -84,7 +84,6 @@ object BSONHandlers {
|
|||
Shapes(_)
|
||||
)
|
||||
|
||||
import lila.tree.Node.{ Comment, Comments }
|
||||
private implicit val CommentIdBSONHandler = stringAnyValHandler[Comment.Id](_.value, Comment.Id.apply)
|
||||
private implicit val CommentTextBSONHandler = stringAnyValHandler[Comment.Text](_.value, Comment.Text.apply)
|
||||
implicit val CommentAuthorBSONHandler = new BSONHandler[BSONValue, Comment.Author] {
|
||||
|
@ -114,6 +113,8 @@ object BSONHandlers {
|
|||
Comments(_)
|
||||
)
|
||||
|
||||
implicit val GamebookBSONHandler = Macros.handler[Gamebook]
|
||||
|
||||
private implicit def CrazyDataBSONHandler: BSON[Crazyhouse.Data] = new BSON[Crazyhouse.Data] {
|
||||
private def writePocket(p: Crazyhouse.Pocket) = p.roles.map(_.forsyth).mkString
|
||||
private def readPocket(p: String) = Crazyhouse.Pocket(p.flatMap(chess.Role.forsyth)(scala.collection.breakOut))
|
||||
|
@ -147,6 +148,7 @@ object BSONHandlers {
|
|||
check = r boolD "c",
|
||||
shapes = r.getO[Shapes]("h") | Shapes.empty,
|
||||
comments = r.getO[Comments]("co") | Comments.empty,
|
||||
gamebook = r.getO[Gamebook]("ga"),
|
||||
glyphs = r.getO[Glyphs]("g") | Glyphs.empty,
|
||||
crazyData = r.getO[Crazyhouse.Data]("z"),
|
||||
clock = r.getO[Centis]("l"),
|
||||
|
@ -161,6 +163,7 @@ object BSONHandlers {
|
|||
"c" -> w.boolO(s.check),
|
||||
"h" -> s.shapes.value.nonEmpty.option(s.shapes),
|
||||
"co" -> s.comments.value.nonEmpty.option(s.comments),
|
||||
"ga" -> s.gamebook,
|
||||
"g" -> s.glyphs.nonEmpty,
|
||||
"l" -> s.clock,
|
||||
"z" -> s.crazyData,
|
||||
|
|
|
@ -6,7 +6,7 @@ import chess.variant.Variant
|
|||
import org.joda.time.DateTime
|
||||
|
||||
import chess.opening.{ FullOpening, FullOpeningDB }
|
||||
import lila.tree.Node.{ Shapes, Comment }
|
||||
import lila.tree.Node.{ Shapes, Comment, Gamebook }
|
||||
import lila.user.User
|
||||
|
||||
case class Chapter(
|
||||
|
@ -40,6 +40,9 @@ case class Chapter(
|
|||
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))
|
||||
|
||||
|
|
|
@ -68,6 +68,9 @@ final class ChapterRepo(coll: Coll) {
|
|||
def setComments(chapter: Chapter, path: Path, comments: lila.tree.Node.Comments): Funit =
|
||||
setNodeValue(chapter, path, "co", comments.value.nonEmpty option comments)
|
||||
|
||||
def setGamebook(chapter: Chapter, path: Path, gamebook: lila.tree.Node.Gamebook): Funit =
|
||||
setNodeValue(chapter, path, "ga", gamebook.nonEmpty option gamebook)
|
||||
|
||||
def setGlyphs(chapter: Chapter, path: Path, glyphs: chess.format.pgn.Glyphs): Funit =
|
||||
setNodeValue(chapter, path, "g", glyphs.nonEmpty)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import chess.format.{ Uci, UciCharPair, FEN }
|
|||
import chess.variant.Crazyhouse
|
||||
|
||||
import chess.Centis
|
||||
import lila.tree.Node.{ Shapes, Comment, Comments }
|
||||
import lila.tree.Node.{ Shapes, Comment, Comments, Gamebook }
|
||||
|
||||
sealed trait RootOrNode {
|
||||
val ply: Int
|
||||
|
@ -28,6 +28,7 @@ case class Node(
|
|||
check: Boolean,
|
||||
shapes: Shapes = Shapes(Nil),
|
||||
comments: Comments = Comments(Nil),
|
||||
gamebook: Option[Gamebook] = None,
|
||||
glyphs: Glyphs = Glyphs.empty,
|
||||
clock: Option[Centis],
|
||||
crazyData: Option[Crazyhouse.Data],
|
||||
|
@ -48,6 +49,8 @@ case class Node(
|
|||
def setComment(comment: Comment) = copy(comments = comments set comment)
|
||||
def deleteComment(commentId: Comment.Id) = copy(comments = comments delete commentId)
|
||||
|
||||
def setGamebook(gamebook: Gamebook) = copy(gamebook = gamebook.some)
|
||||
|
||||
def setShapes(s: Shapes) = copy(shapes = s)
|
||||
|
||||
def toggleGlyph(glyph: Glyph) = copy(glyphs = glyphs toggle glyph)
|
||||
|
@ -160,6 +163,8 @@ object Node {
|
|||
children: Children
|
||||
) extends RootOrNode {
|
||||
|
||||
def gamebook = none
|
||||
|
||||
def withChildren(f: Children => Option[Children]) =
|
||||
f(children) map { newChildren =>
|
||||
copy(children = newChildren)
|
||||
|
@ -184,6 +189,10 @@ object Node {
|
|||
if (path.isEmpty) copy(comments = comments delete commentId).some
|
||||
else updateChildrenAt(path, _ deleteComment commentId)
|
||||
|
||||
def setGamebookAt(gamebook: Gamebook, path: Path): Option[Root] =
|
||||
if (path.isEmpty) none
|
||||
else updateChildrenAt(path, _ setGamebook gamebook)
|
||||
|
||||
def toggleGlyphAt(glyph: Glyph, path: Path): Option[Root] =
|
||||
if (path.isEmpty) copy(glyphs = glyphs toggle glyph).some
|
||||
else updateChildrenAt(path, _ toggleGlyph glyph)
|
||||
|
|
|
@ -15,7 +15,7 @@ import lila.socket.actorApi.{ Connected => _, _ }
|
|||
import lila.socket.Socket.makeMessage
|
||||
import lila.socket.Socket.Uid
|
||||
import lila.socket.{ Handler, AnaMove, AnaDrop, AnaAny }
|
||||
import lila.tree.Node.{ Shape, Shapes, Comment }
|
||||
import lila.tree.Node.{ Shape, Shapes, Comment, Gamebook }
|
||||
import lila.user.User
|
||||
import makeTimeout.short
|
||||
|
||||
|
@ -130,11 +130,10 @@ private[study] final class SocketHandler(
|
|||
|
||||
case ("shapes", o) =>
|
||||
reading[AtPosition](o) { position =>
|
||||
(o \ "d" \ "shapes").asOpt[List[Shape]] foreach { shapes =>
|
||||
member.userId foreach { userId =>
|
||||
api.setShapes(userId, studyId, position.ref, Shapes(shapes take 32), uid)
|
||||
}
|
||||
}
|
||||
for {
|
||||
shapes <- (o \ "d" \ "shapes").asOpt[List[Shape]]
|
||||
userId <- member.userId
|
||||
} api.setShapes(userId, studyId, position.ref, Shapes(shapes take 32), uid)
|
||||
}
|
||||
|
||||
case ("setClock", o) =>
|
||||
|
@ -205,6 +204,14 @@ private[study] final class SocketHandler(
|
|||
} api.deleteComment(userId, studyId, position.ref, Comment.Id(id), uid)
|
||||
}
|
||||
|
||||
case ("setGamebook", o) =>
|
||||
reading[AtPosition](o) { position =>
|
||||
for {
|
||||
userId <- member.userId
|
||||
gamebook <- (o \ "gamebook").asOpt[Gamebook]
|
||||
} api.setGamebook(userId, studyId, position.ref, gamebook.pp, uid)
|
||||
}
|
||||
|
||||
case ("toggleGlyph", o) =>
|
||||
reading[AtPosition](o) { position =>
|
||||
for {
|
||||
|
@ -243,6 +250,7 @@ private[study] final class SocketHandler(
|
|||
private implicit val ChapterEditDataReader = Json.reads[ChapterMaker.EditData]
|
||||
private implicit val StudyDataReader = Json.reads[Study.Data]
|
||||
private implicit val setTagReader = Json.reads[actorApi.SetTag]
|
||||
private implicit val gamebookReader = Json.reads[Gamebook]
|
||||
|
||||
def join(
|
||||
studyId: Study.Id,
|
||||
|
|
|
@ -9,7 +9,7 @@ import lila.hub.actorApi.map.Tell
|
|||
import lila.hub.actorApi.timeline.{ Propagate, StudyCreate, StudyLike }
|
||||
import lila.hub.Sequencer
|
||||
import lila.socket.Socket.Uid
|
||||
import lila.tree.Node.{ Shapes, Comment }
|
||||
import lila.tree.Node.{ Shapes, Comment, Gamebook }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class StudyApi(
|
||||
|
@ -343,10 +343,11 @@ final class StudyApi(
|
|||
studyRepo.updateNow(study)
|
||||
newChapter.root.nodeAt(position.path) ?? { node =>
|
||||
node.comments.findBy(comment.by) ?? { c =>
|
||||
chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>-
|
||||
sendTo(study, Socket.SetComment(position, c, uid)) >>-
|
||||
indexStudy(study) >>-
|
||||
chapterRepo.setComments(newChapter, position.path, node.comments.filterEmpty) >>- {
|
||||
sendTo(study, Socket.SetComment(position, c, uid))
|
||||
indexStudy(study)
|
||||
sendStudyEnters(study, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
|
@ -394,6 +395,22 @@ final class StudyApi(
|
|||
}
|
||||
}
|
||||
|
||||
def setGamebook(userId: User.ID, studyId: Study.Id, position: Position.Ref, gamebook: Gamebook, uid: Uid) = sequenceStudyWithChapter(studyId, position.chapterId) {
|
||||
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
|
||||
chapter.setGamebook(gamebook, position.path) match {
|
||||
case Some(newChapter) =>
|
||||
studyRepo.updateNow(study)
|
||||
chapterRepo.setGamebook(newChapter, position.path, gamebook) >>- {
|
||||
indexStudy(study)
|
||||
sendStudyEnters(study, userId)
|
||||
}
|
||||
case None =>
|
||||
fufail(s"Invalid setGamebook $studyId $position") >>-
|
||||
reloadUidBecauseOf(study, uid, chapter.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addChapter(byUserId: User.ID, studyId: Study.Id, data: ChapterMaker.Data, sticky: Boolean, socket: ActorRef, uid: Uid) = sequenceStudy(studyId) { study =>
|
||||
Contribute(byUserId, study) {
|
||||
chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
|
||||
|
|
|
@ -38,6 +38,7 @@ object TreeBuilder {
|
|||
check = node.check,
|
||||
shapes = node.shapes,
|
||||
comments = node.comments,
|
||||
gamebook = node.gamebook,
|
||||
glyphs = node.glyphs,
|
||||
clock = node.clock,
|
||||
crazyData = node.crazyData,
|
||||
|
|
|
@ -20,6 +20,7 @@ sealed trait Node {
|
|||
def eval: Option[Eval]
|
||||
def shapes: Node.Shapes
|
||||
def comments: Node.Comments
|
||||
def gamebook: Option[Node.Gamebook]
|
||||
def glyphs: Glyphs
|
||||
def children: List[Branch]
|
||||
def opening: Option[FullOpening]
|
||||
|
@ -64,6 +65,7 @@ case class Root(
|
|||
def addChild(branch: Branch) = copy(children = children :+ branch)
|
||||
def prependChild(branch: Branch) = copy(children = branch :: children)
|
||||
def dropFirstChild = copy(children = if (children.isEmpty) children else children.tail)
|
||||
def gamebook = None
|
||||
}
|
||||
|
||||
case class Branch(
|
||||
|
@ -78,6 +80,7 @@ case class Branch(
|
|||
eval: Option[Eval] = None,
|
||||
shapes: Node.Shapes = Node.Shapes(Nil),
|
||||
comments: Node.Comments = Node.Comments(Nil),
|
||||
gamebook: Option[Node.Gamebook] = None,
|
||||
glyphs: Glyphs = Glyphs.empty,
|
||||
children: List[Branch] = Nil,
|
||||
opening: Option[FullOpening] = None,
|
||||
|
@ -168,6 +171,12 @@ object Node {
|
|||
val empty = Comments(Nil)
|
||||
}
|
||||
|
||||
case class Gamebook(
|
||||
deviation: String
|
||||
) {
|
||||
def nonEmpty = deviation.nonEmpty
|
||||
}
|
||||
|
||||
// TODO copied from lila.game
|
||||
// put all that shit somewhere else
|
||||
private implicit val crazyhousePocketWriter: OWrites[Crazyhouse.Pocket] = OWrites { v =>
|
||||
|
@ -226,6 +235,7 @@ object Node {
|
|||
private implicit val commentsWriter: Writes[Node.Comments] = Writes[Node.Comments] { s =>
|
||||
JsArray(s.list.map(commentWriter.writes))
|
||||
}
|
||||
implicit val gamebookWriter = Json.writes[Node.Gamebook]
|
||||
import Eval.JsonHandlers.evalWrites
|
||||
|
||||
def makeNodeJsonWriter(alwaysChildren: Boolean): Writes[Node] = Writes { node =>
|
||||
|
@ -238,6 +248,7 @@ object Node {
|
|||
add("check", true, check) _ compose
|
||||
add("eval", eval.filterNot(_.isEmpty)) _ compose
|
||||
add("comments", comments, comments.nonEmpty) _ compose
|
||||
add("gamebook", gamebook) _ compose
|
||||
add("glyphs", glyphs.nonEmpty) _ compose
|
||||
add("shapes", shapes.list, shapes.list.nonEmpty) _ compose
|
||||
add("opening", opening) _ compose
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
div.lichess_game div.lichess_ground {
|
||||
margin-bottom: -317px;
|
||||
}
|
||||
.gamebook {
|
||||
height: 300px;
|
||||
}
|
||||
.gamebook .editor {
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
.gamebook .editor .title {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.gamebook .editor label span {
|
||||
display: block;
|
||||
}
|
||||
.gamebook .editor textarea {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 5em;
|
||||
border-width: 1px 0;
|
||||
}
|
|
@ -41,7 +41,7 @@ export function ctrl(root: AnalyseController): CommentForm {
|
|||
doSubmit(text);
|
||||
};
|
||||
|
||||
const doSubmit = throttle(500, false, function(text) {
|
||||
const doSubmit = throttle(500, false, (text: string) => {
|
||||
const cur = current();
|
||||
if (cur) root.study!.makeChange('setComment', {
|
||||
ch: cur.chapterId,
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { h } from 'snabbdom'
|
||||
import AnalyseController from '../../../ctrl';
|
||||
import { VNode } from 'snabbdom/vnode'
|
||||
import { throttle } from 'common';
|
||||
|
||||
export default function(ctrl: AnalyseController): VNode {
|
||||
|
||||
return h('div.gamebook', {
|
||||
hook: {
|
||||
insert: _ => {
|
||||
window.lichess.loadCss('/assets/stylesheets/gamebook.css')
|
||||
window.lichess.loadCss('/assets/stylesheets/material.form.css')
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('div.editor', [
|
||||
h('span.title', 'Gamebook editor'),
|
||||
renderDeviation(ctrl)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
let prevPath: Tree.Path;
|
||||
|
||||
|
||||
const save = throttle(500, false, (ctrl: AnalyseController, gamebook: Tree.Gamebook) => {
|
||||
ctrl.socket.send('setGamebook', {
|
||||
path: ctrl.path,
|
||||
ch: ctrl.study!.vm.chapterId,
|
||||
gamebook: gamebook
|
||||
});
|
||||
});
|
||||
|
||||
function renderDeviation(ctrl: AnalyseController): VNode {
|
||||
const node = ctrl.node,
|
||||
path = ctrl.path,
|
||||
gamebook: Tree.Gamebook = node.gamebook || {},
|
||||
deviation = gamebook.deviation || '';
|
||||
return h('div.deviation', [
|
||||
h('label', {
|
||||
attrs: { for: 'gamebook-deviation' }
|
||||
}, 'When any other move is played:'),
|
||||
h('textarea#gamebook-deviation', {
|
||||
hook: {
|
||||
insert(vnode: VNode) {
|
||||
const el = vnode.elm as HTMLInputElement;
|
||||
el.value = deviation;
|
||||
function onChange() {
|
||||
node.gamebook = node.gamebook || {};
|
||||
node.gamebook.deviation = el.value.trim();
|
||||
setTimeout(() => save(ctrl, node.gamebook, 50));
|
||||
}
|
||||
el.onkeyup = onChange;
|
||||
el.onpaste = onChange;
|
||||
prevPath = path;
|
||||
},
|
||||
postpatch(_, vnode: VNode) {
|
||||
if (prevPath !== path) {
|
||||
(vnode.elm as HTMLInputElement).value = deviation;
|
||||
prevPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { prop } from 'common';
|
||||
import AnalyseController from '../../ctrl';
|
||||
|
||||
export default function(root: AnalyseController, studyData: StudyData) {
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
import { h } from 'snabbdom'
|
||||
// import { h } from 'snabbdom'
|
||||
import AnalyseController from '../../ctrl';
|
||||
import { StudyController } from '../interfaces';
|
||||
import { VNode } from 'snabbdom/vnode'
|
||||
import renderEditor from './editor/gamebookEditorView';
|
||||
|
||||
export function view(root: AnalyseController): VNode | undefined {
|
||||
const study = root.study;
|
||||
const study: StudyController = root.study;
|
||||
if (!study || !study.data.chapter.gamebook) return;
|
||||
if (study.members.canContribute()) return builder(root, study);
|
||||
}
|
||||
|
||||
function builder(root: AnalyseController, study: StudyController): VNode {
|
||||
|
||||
return h('div.gamebook.builder', 'builder here');
|
||||
if (study.members.canContribute()) return renderEditor(root);
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ export interface StudyChapter {
|
|||
tags: TagArray[]
|
||||
practice: boolean;
|
||||
conceal?: number;
|
||||
gamebook: boolean;
|
||||
features: StudyChapterFeatures;
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,7 @@ declare namespace Tree {
|
|||
fen: Fen;
|
||||
children: Node[];
|
||||
comments?: Comment[];
|
||||
gamebook?: Gamebook;
|
||||
dests?: string;
|
||||
drops: string | undefined | null;
|
||||
check: boolean;
|
||||
|
@ -202,6 +203,10 @@ declare namespace Tree {
|
|||
text: string;
|
||||
}
|
||||
|
||||
export interface Gamebook {
|
||||
deviation?: string;
|
||||
}
|
||||
|
||||
export interface Opening {
|
||||
name: string;
|
||||
eco: string;
|
||||
|
|
Loading…
Reference in New Issue