study gamebook mode WIP

pull/3446/merge
Thibault Duplessis 2017-08-14 19:44:04 -05:00
parent 43a994eb38
commit 7861ddfd34
15 changed files with 174 additions and 28 deletions

View File

@ -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,

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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 =>

View File

@ -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,

View File

@ -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

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}
}
}
})
]);
}

View File

@ -1,5 +0,0 @@
import { prop } from 'common';
import AnalyseController from '../../ctrl';
export default function(root: AnalyseController, studyData: StudyData) {
}

View File

@ -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);
}

View File

@ -107,6 +107,7 @@ export interface StudyChapter {
tags: TagArray[]
practice: boolean;
conceal?: number;
gamebook: boolean;
features: StudyChapterFeatures;
}

View File

@ -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;