chess

Terminal based Chess trainer using Anki
git clone git://git.dimitrijedobrota.com/chess.git
Log | Files | Refs

commit 2fc01f0a8ff7da1fafbcf537702e54c2cf94711e
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date:   Mon,  5 Sep 2022 22:32:50 +0200

Initial commit

Diffstat:
A.clang-format | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.gitignore | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AMakefile | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/anki.h | 34++++++++++++++++++++++++++++++++++
Ainclude/board.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/display.h | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/move.h | 17+++++++++++++++++
Asrc/anki.c | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/board.c | 310+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/display.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.c | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/move.c | 47+++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 1515 insertions(+), 0 deletions(-)

diff --git a/.clang-format b/.clang-format @@ -0,0 +1,178 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: true +AlignArrayOfStructures: Right +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: true +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Right +PPIndentWidth: -1 +ReferenceAlignment: Pointer +ReflowComments: true +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/.gitignore b/.gitignore @@ -0,0 +1,58 @@ +.ccls-cache/ +.ccls +docs/* +bin +bin/* + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/Makefile b/Makefile @@ -0,0 +1,69 @@ +# GNU Makefile for Chess Anki Trainer +# +# Usage: make [-f path\Makefile] [DEBUG=Y] [NO_UNICODE=Y] [NO_MOUSE=Y] target + +NAME = chess +CC = gcc + +CFLAGS = -I include +LDFLAGS =-lcurl -ljson-c -lncursesw -lm + +SRC = src +OBJ = obj +BINDIR = bin + +BIN = bin/$(NAME) +SRCS=$(wildcard $(SRC)/*.c) +OBJS=$(patsubst $(SRC)/%.c, $(OBJ)/%.o, $(SRCS)) + +ifeq ($(OS),Windows_NT) + RM = del + NAME := $(NAME).exe + DEL_CLEAN = $(subst /,\,$(BIN)) $(subst /,\,$(OBJS)) +else + RM = rm -f + DEL_CLEAN = $(BIN) $(OBJS) +endif + +ifeq ($(DEBUG),Y) + CFLAGS += -lciid -ldisplayd + CFLAGS += -Wall -Wextra -Werror -Wpedantic -fsanitize=address -ggdb +else + CFLAGS += -lcii -ldisplay +endif +ifeq ($(NO_UNICODE),Y) + CFLAGS += -D NO_UNICODE +endif + +ifeq ($(NO_MOUSE),Y) + CFLAGS += -D NO_MOUSE +endif + +all: $(BIN) + +$(BIN): $(OBJS) + $(CC) $^ $(CFLAGS) $(LDFLAGS) -o $@ + +$(OBJ)/%.o: $(SRC)/%.c + $(CC) -c $< -o $@ $(CFLAGS) $(LDFLAGS) + +clean: + -$(RM) $(DEL_CLEAN) + +help: + @echo "Game of Life Simulation" + @echo + @echo "Usage: make [-f path\Makefile] [DEBUG=Y] [NO_UNICODE=Y] target" + @echo + @echo "Target rules:" + @echo " all - Compiles binary file [Default]" + @echo " clean - Clean the project by removing binaries" + @echo " help - Prints a help message with target rules" + @echo + @echo "Optional parameters:" + @echo " DEBUG - Compile binary file with debug flags enabled" + @echo " NO_UNICODE - Compile binary file that does not use Unicode characters" + @echo " NO_MOUSE - Compile binary file that does not have mouse support even if terminal supports it" + @echo + +.PHONY: all clean help docs diff --git a/include/anki.h b/include/anki.h @@ -0,0 +1,34 @@ +#ifndef ANKI_H +#define ANKI_H + +#include <except.h> +#include <stddef.h> + +#define Move_T card_T +typedef struct Move_T *Move_T; + +extern const Except_T ANKIE_CONNECT, ANKIE_CURL, ANKIE_DECK, ANKIE_DECK_SELECT, + ANKIE_OK, ANKIE_PROFILE, ANKIE_PROFILE_SELECT; + +void anki_start(void); +void anki_stop(void); + +Move_T anki_current_card(void); +void anki_grade(int pass); +void anki_suspend(Move_T self); + +char *card_fen(Move_T self); +char *card_game(Move_T self); +char *card_start(Move_T self); +char *card_name(Move_T self); +char *card_pgn(Move_T self); +char *card_player(Move_T self); +char *card_id(Move_T self); + +const char **anki_get_profiles(size_t *size); +const char **anki_get_decks(size_t *size); +void anki_load_profile(char *name); +void anki_load_deck(char *name); + +#undef Move_T +#endif diff --git a/include/board.h b/include/board.h @@ -0,0 +1,61 @@ +#ifndef BOARD_H +#define BOARD_H + +#include "anki.h" +#include "move.h" + +#define Move_T Board_T +#define G Grave_T + +typedef struct Move_T *Move_T; +typedef struct G *G; + +enum AC { AC_QUIT = -3, AC_SUSPEND, AC_INDEX }; + +#define BUFF_SIZE 10 + +typedef void (*review_f)(void); +typedef struct game_T *game_T; +struct game_T { + card_T card; + move_T *moves; + review_f review_next; + + int pass; + int fail; + + size_t display_current; + size_t move_current; + + size_t move_start; + size_t move_fail; + + char buffer[BUFF_SIZE + 1]; + int buffer_crnt; + + size_t moves_size; + size_t boards_size; + Move_T boards[]; +}; + +Move_T Board_new(void); +Move_T Board_from_FEN(char *fen); +void Board_free(Move_T *self); + +Move_T Board_play(Move_T self, char *m); +void Board_print(Move_T self); + +char Board_atIndex(Move_T self, int i, int j); + +int piece_get_index(char l); + +G Board_grave(Move_T self, char player); +char Grave_atIndex(G self, int index); +int Grave_size(G self); + +/* GAME */ +game_T game_new(size_t moves); + +#undef Move_T +#undef G +#endif diff --git a/include/display.h b/include/display.h @@ -0,0 +1,70 @@ +#ifndef DISPAY_H +#define DISPAY_H + +#include <pane.h> +#include <widgetList.h> + +#include "board.h" +#include "except.h" + +#define MAX_READ 5 + +void display_start(void); +void display_stop(void); + +/* void display_board(Board_T self); */ +void display_moves(int halfmoves); +void display_title(char *name); +void display_error(char *err); + +void display_fail(void); +void display_pass(void); +void display_clear(void); + +int display_input(char *buf, int *index); + +char *display_select_menu(const char **list, int size); +char display_getch(void); + +typedef struct boardInfo_T *boardInfo_T; +struct boardInfo_T { + int something; +}; + +typedef struct boardStyle_T *boardStyle_T; +struct boardStyle_T { + int square_light; + int square_dark; + int border; + int piece; + int annotation; +}; + +typedef struct movesInfo_T *movesInfo_T; +struct movesInfo_T { + widgetList_T list; +}; + +typedef struct movesStyle_T *movesStyle_T; +struct movesStyle_T { + int padding; + int foreground; + int background; + int active; +}; + +typedef struct titleInfo_T *titleInfo_T; +struct titleInfo_T { + widget_T subwidget; +}; + +typedef struct titleStyle_T *titleStyle_T; +struct titleStyle_T { + int something; +}; + +void board_display(widget_T widget); +void moves_display(widget_T widget); +int game_handleInput(data_T data, struct tb_event ev); + +#endif diff --git a/include/move.h b/include/move.h @@ -0,0 +1,17 @@ +#ifndef MOVE_H +#define MOVE_H + +#include <stddef.h> + +typedef struct move_T *move_T; +struct move_T { + char turn[10]; + char white[10]; + char black[10]; +}; + +move_T *Move_list(char *pgn, size_t *size); +char *Move_halfmove(move_T *moves, int halfturn); + +#undef Board_T +#endif diff --git a/src/anki.c b/src/anki.c @@ -0,0 +1,248 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <curl/curl.h> +#include <json-c/json.h> + +#include "anki.h" +#include "assert.h" +#include "display.h" +#include "except.h" +#include "mem.h" + +#define Move_T card_T + +struct card_T { + char *id; + char *name; + char *game; + char *fen; + char *pgn; + char *player; + char *start; +}; + +Move_T card; + +#define ANKI_URL "127.0.0.1:8765" +#define ANKI_CURRENT_CARD "{\"version\": 6, \"action\": \"guiCurrentCard\"}" +#define ANKI_EXIT "{\"version\": 6, \"action\": \"guiExitAnki\"}" +#define ANKI_INIT "{\"version\": 6, \"action\": \"requestPermission\"}" +#define ANKI_SYNC "{\"version\": 6, \"action\": \"sync\"}" + +#define ANKI_PROFILES "{\"version\": 6, \"action\": \"getProfiles\"}" +#define ANKI_DECKS "{\"version\": 6, \"action\": \"deckNames\"}" + +#define ANKI_GRADE_FAIL \ + "{\"version\": 6, \"action\": \"multi\", \"params\": { \"actions\": [ " \ + "{\"action\": \"guiShowAnswer\"}, { \"action\": \"guiAnswerCard\", " \ + "\"params\": {\"ease\": 1}}]}}" +#define ANKI_GRADE_PASS \ + "{\"version\": 6, \"action\": \"multi\", \"params\": { \"actions\": [ " \ + "{\"action\": \"guiShowAnswer\"}, { \"action\": \"guiAnswerCard\", " \ + "\"params\": {\"ease\": 3}}]}}" + +#define JSON_PRINT_FLAGS JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY +#define JSON_PRINT(x) \ + fprintf(stderr, "%s\n---\n", \ + json_object_to_json_string_ext((x), JSON_PRINT_FLAGS)); +#define JSON_STRING_ASSERT(x, value) \ + strstr(json_object_get_string((x)), (value)) != NULL + +CURL *curl; +CURLcode res; + +struct json_object *result; +size_t got_data(void *buffer, size_t itemsize, size_t nitems, void *ignore); + +const Except_T ANKIE_CONNECT = {"Anki: Couldn't connect to the server"}, + ANKIE_CURL = {"Anki: Couldn't initialize libcurl"}, + ANKIE_DECK = {"Anki: Invalid deck"}, + ANKIE_DECK_SELECT = {"Anki: Invalid deck selection"}, + ANKIE_GRADE = {"Anki: Unable to grade current card"}, + ANKIE_OK = {"everything is ok"}, + ANKIE_PROFILE = {"Anki: Invalid profile"}, + ANKIE_PROFILE_SELECT = {"Anki: Invalid profile selection"}, + ANKIE_SUSPEND = {"Anki: Unable to suspend current card"}; + +void curl_do(char *action) { + curl_easy_setopt(curl, CURLOPT_URL, ANKI_URL); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, action); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, got_data); + + res = curl_easy_perform(curl); + + switch (res) { + case CURLE_OK: + break; + case CURLE_COULDNT_CONNECT: + RAISE(ANKIE_CONNECT); + default: + RAISE(ANKIE_CONNECT); + } + + /* JSON_PRINT(result); */ +} + +size_t got_data(void *buffer, size_t itemsize, size_t nitems, void *ignore) { + (void)ignore; + + size_t bytes = itemsize * nitems; + free(result); + struct json_object *parsed_json = json_tokener_parse(buffer); + json_object_object_get_ex(parsed_json, "result", &result); + return bytes; +} + +void anki_connect(void) { + struct json_object *permission; + curl_do(ANKI_INIT); + json_object_object_get_ex(result, "permission", &permission); + + if (!(JSON_STRING_ASSERT(permission, "granted"))) + RAISE(ANKIE_CONNECT); +} + +void card_dtor(Move_T self) { + FREE(self->fen); + FREE(self->game); + FREE(self->id); + FREE(self->name); + FREE(self->pgn); + FREE(self->player); + FREE(self->start); +} + +void card_set_field(Move_T self, char *key, const char *name) { + char **field; + + if (strcmp(key, "id") == 0) + field = &self->id; + else if (strcmp(key, "name") == 0) + field = &self->name; + else if (strcmp(key, "FEN") == 0) + field = &self->fen; + else if (strcmp(key, "PGN") == 0) + field = &self->pgn; + else if (strcmp(key, "player") == 0) + field = &self->player; + else if (strcmp(key, "game") == 0) + field = &self->game; + else if (strcmp(key, "start") == 0) + field = &self->start; + else + return; + + *field = ALLOC(strlen(name) * sizeof(char) + 1); + strcpy(*field, name); +} + +Move_T anki_current_card(void) { + struct json_object *fields, *card_id; + /* FREE(card); */ + NEW(card); + + curl_do(ANKI_CURRENT_CARD); + if (result == NULL) + return NULL; + + json_object_object_get_ex(result, "fields", &fields); + json_object_object_get_ex(result, "cardId", &card_id); + + json_object_object_foreach(fields, key, val_t) { + struct json_object *value; + json_object_object_get_ex(val_t, "value", &value); + card_set_field(card, key, json_object_get_string(value)); + } + + card_set_field(card, "id", json_object_get_string(card_id)); + + if (!(card->name && card->pgn)) { + printf("Card info incomplete. Suspending it...\n"); + anki_suspend(card); + return 0; + } + + return card; +} + +void anki_start(void) { + if (!(curl = curl_easy_init())) + RAISE(ANKIE_CURL); + anki_connect(); +} + +const char **anki_list(size_t *size) { + size_t len = json_object_array_length(result), i; + const char **list; + + list = ALLOC(len * sizeof(const char *)); + for (i = 0; i < len; i++) + list[i] = json_object_get_string(json_object_array_get_idx(result, i)); + + *size = len; + return list; +} + +const char **anki_get_profiles(size_t *size) { + curl_do(ANKI_PROFILES); + return anki_list(size); +} + +void anki_load_profile(char *name) { + static char request[100]; + sprintf(request, + "{\"version\": 6, \"action\": \"loadProfile\", \"params\": { " + "\"name\": \"%s\" }}", + name); + curl_do(request); + if (!(JSON_STRING_ASSERT(result, "true"))) + RAISE(ANKIE_PROFILE); +} + +const char **anki_get_decks(size_t *size) { + curl_do(ANKI_DECKS); + return anki_list(size); +} + +void anki_load_deck(char *name) { + static char request[1000]; + + if (!name) + RAISE(ANKIE_DECK_SELECT); + sprintf(request, + "{\"version\": 6, \"action\": \"guiDeckReview\", \"params\": { " + "\"name\": \"%s\" }}", + name); + curl_do(request); + + if (!(JSON_STRING_ASSERT(result, "true"))) + RAISE(ANKIE_DECK); +} + +void anki_grade(int pass) { + curl_do((pass) ? ANKI_GRADE_PASS : ANKI_GRADE_FAIL); + if (!(JSON_STRING_ASSERT(result, "true"))) + RAISE(ANKIE_GRADE); +} + +void anki_suspend(Move_T self) { + static char request[100]; + sprintf(request, + "{\"action\": \"suspend\", \"version\": 6, \"params\": { \"cards\": " + "[%s] }}", + self->id); + curl_do(request); + if (!(JSON_STRING_ASSERT(result, "true"))) + RAISE(ANKIE_SUSPEND); +} + +void anki_stop(void) { curl_easy_cleanup(curl); } +char *card_name(Move_T self) { return self->name; } +char *card_pgn(Move_T self) { return self->pgn; } +char *card_start(Move_T self) { return self->start; } +char *card_game(Move_T self) { return self->game; } +char *card_player(Move_T self) { return self->player; } +char *card_fen(Move_T self) { return self->fen; } +char *card_id(Move_T self) { return self->id; } diff --git a/src/board.c b/src/board.c @@ -0,0 +1,310 @@ +#include "board.h" +#include "assert.h" +#include "mem.h" + +#include <ctype.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> + +#include "move.h" + +#define T Board_T +#define G Grave_T +typedef struct G *G; + +#define MAX_PLAY 1024 + +const char def_board[8][8] = {"rnbqkbnr", "pppppppp", " ", " ", + " ", " ", "PPPPPPPP", "RNBQKBNR"}; +static char piece_lookup[] = {'K', 'Q', 'R', 'B', 'N', 'P', + 'k', 'q', 'r', 'b', 'n', 'p'}; +static int piece_value[] = {100, 8, 5, 3, 3, 1, 100, 8, 5, 3, 3, 1}; + +struct G { + char arr[20]; + int size; + int value; +}; + +struct T { + int turn; + char *board; + G white; + G black; +}; + +typedef struct { + int x[8]; + int y[8]; + int size; + int limited; +} move_offset_t; + +move_offset_t piece_offset[] = { + { {0, 0, 1, -1, -1, -1, 1, 1}, {-1, 1, 0, 0, -1, 1, 1, -1}, 8, 1}, + { {0, 0, 1, -1, -1, -1, 1, 1}, {-1, 1, 0, 0, -1, 1, 1, -1}, 8, 0}, + { {0, 0, 1, -1, 0, 0, 0, 0}, {-1, 1, 0, 0, 0, 0, 0, 0}, 4, 0}, + { {-1, -1, 1, 1, 0, 0, 0, 0}, {-1, 1, 1, -1, 0, 0, 0, 0}, 4, 0}, + {{-2, -1, 1, 2, 2, 1, -1, -2}, {1, 2, 2, 1, -1, -2, -2, -1}, 8, 1} +}; + +void __grave_copy(G self, G new) { + new->size = self->size; + new->value = self->value; + for (int i = 0; i < self->size; i++) + new->arr[i] = self->arr[i]; +} + +void __board_send_grave(T self, char piece, char color) { + G g = (color == 'w') ? self->white : self->black; + + g->arr[g->size++] = piece; + g->value += piece_value[piece_get_index(piece)]; +} + +char __board_get_at(T self, char file, int rank) { + if (file > 'h' || file < 'a' || rank < 1 || rank > 8) + return '!'; + return self->board[(8 - rank) * 8 + file - 'a']; +} + +void __board_set_at(T self, char file, int rank, char to) { + self->board[(8 - rank) * 8 + file - 'a'] = to; +} + +typedef int (*__board_find_next_f)(T, char, char *, int *); + +int __board_find_next_rank(T self, char piece, char *file, int *rank) { + for (; *file <= 'h'; (*file)++) + if (__board_get_at(self, *file, *rank) == piece) + return 1; + return 0; +} + +int __board_find_next_file(T self, char piece, char *file, int *rank) { + for (; *rank <= 8; (*rank)++) + if (__board_get_at(self, *file, *rank) == piece) + return 1; + return 0; +} + +int __board_find_next(T self, char piece, char *file, int *rank) { + for (; *file <= 'h'; (*file)++) { + for (; *rank <= 8; (*rank)++) + if (__board_get_at(self, *file, *rank) == piece) + return 1; + *rank = 1; + } + return 0; +} + +void __board_copy(T self, T new) { + __grave_copy(self->white, new->white); + __grave_copy(self->black, new->black); + new->turn = self->turn; + for (int i = 0; i < 8; i++) + for (int j = 0; j < 8; j++) + new->board[i * 8 + j] = self->board[i * 8 + j]; +} + +int __board_can_move(T self, char piece, char f_s, int r_s, char f_e, int r_e, + int take) { + + char piece_e = __board_get_at(self, f_e, r_e); + if (take && !(piece_e != ' ' || (piece == 'P'))) + return 0; + + if (toupper(piece) == 'P') { + int mul = (piece == 'P') ? 1 : -1; + int step = ((piece == 'P' && r_s == 2) || r_s == 7) ? 2 : 1; + if (!take) { + if (f_s != f_e) + return 0; + for (int i = 1; i <= step; i++) { + char piece_c = __board_get_at(self, f_s, r_s + i * mul); + if (piece_c != ' ') + return 0; + if (r_s + i * mul == r_e) + return 1; + } + } else { + return (r_s + mul == r_e && (f_s + 1 == f_e || f_s - 1 == f_e)); + } + } else { + piece = toupper(piece); + move_offset_t *move = piece_offset + piece_get_index(piece); + + for (int i = 0; i < move->size; i++) { + int rank = r_s, file = f_s; + do { + file += move->y[i]; + rank += move->x[i]; + if (rank == r_e && file == f_e) + return 1; + } while (!move->limited && __board_get_at(self, file, rank) == ' '); + } + } + return 0; +} + +G grave_new(void) { + G p; + NEW(p); + p->size = 0; + p->value = 0; + return p; +} + +char Grave_atIndex(G self, int index) { + assert(self); + return self->arr[index]; +} + +int Grave_size(G self) { + assert(self); + return self->size; +} + +G Board_grave(T self, char player) { + assert(self); + printf("%c\n", player); + return (player == 'b') ? self->white : self->black; +} + +T Board_new(void) { + T board; + NEW(board); + board->white = grave_new(); + board->black = grave_new(); + board->board = ALLOC(64 * sizeof(char) + 1); + for (int i = 0; i < 8; i++) + for (int j = 0; j < 8; j++) + board->board[i * 8 + j] = def_board[i][j]; + + board->turn = 0; + return board; +} + +T Board_from_FEN(char *fen) { + T board = Board_new(); + + int j = 0; + do { + if (isalpha(*fen)) + board->board[j++] = *fen; + else if (isdigit(*fen)) + for (int i = 0; i < *fen - '0'; i++) + board->board[j++] = ' '; + } while (*fen++ != '\0' && *fen != ' '); + return board; +} + +void Board_free(T *self) { + assert(self); + FREE((*self)->board); + FREE(*self); +} + +T Board_play(T self, char *m) { + assert(self); + assert(m); + + static char mover = 'b'; + mover = (mover == 'b') ? 'w' : 'b'; + + T new = Board_new(); + __board_copy(self, new); + + int (*conv)(int) = (mover == 'w') ? toupper : tolower; + int castle_rank = (mover == 'w') ? 1 : 8; + + if (strcmp(m, "O-O") == 0) { + __board_set_at(new, 'e', castle_rank, ' '); + __board_set_at(new, 'f', castle_rank, conv('R')); + __board_set_at(new, 'g', castle_rank, conv('K')); + __board_set_at(new, 'h', castle_rank, ' '); + } else if (strcmp(m, "O-O-O") == 0) { + __board_set_at(new, 'a', castle_rank, ' '); + __board_set_at(new, 'c', castle_rank, conv('K')); + __board_set_at(new, 'd', castle_rank, conv('R')); + __board_set_at(new, 'e', castle_rank, ' '); + } else { + // Decoding move MESS Start + int l = strlen(m); + if (m[l - 1] == '+' || m[l - 1] == '#') + l--; + + char file = m[l - 2]; + int rank = m[l - 1] - '0'; + + int take = 0; + char select = '\0', moved; + if (l == 4 && m[1] == 'x') + take = 1; + else if (l >= 4) { + select = m[1]; + take = m[2] == 'x'; + } + + if (isupper(m[0])) + moved = conv(m[0]); + else { + moved = conv('P'); + select = m[0]; + } + // MESS End + + char file_s = (isalpha(select)) ? select : 'a'; + int rank_s = (isdigit(select)) ? select - '0' : 1; + + __board_find_next_f find_f; + if (select) + find_f = + (isalpha(select)) ? __board_find_next_file : __board_find_next_rank; + else + find_f = __board_find_next; + + while (find_f(self, moved, &file_s, &rank_s)) { + if (__board_can_move(self, moved, file_s, rank_s, file, rank, take)) { + if (take) + __board_send_grave(new, __board_get_at(self, file, rank), mover); + __board_set_at(new, file_s, rank_s, ' '); + __board_set_at(new, file, rank, moved); + break; + } + rank_s++; + } + } + return new; +} + +int piece_get_index(char l) { + for (size_t k = 0; k < sizeof(piece_lookup); k++) + if (piece_lookup[k] == l) + return k; + + return -1; +} + +char Board_atIndex(T self, int i, int j) { + assert(i >= 0 && i < 9 && j >= 0 && j < 9); + return self->board[8 * i + j]; +} + +/* GAME */ + +game_T game_new(size_t moves_size) { + game_T game = ALLOC(sizeof(*game) + moves_size * sizeof(struct move_T)); + game->moves_size = moves_size; + game->display_current = 0; + game->buffer_crnt = 0; + game->pass = 0; + game->fail = 0; + memset(game->buffer, '\0', BUFF_SIZE); + game->buffer[BUFF_SIZE] = '\0'; + + return game; +} + +#undef G diff --git a/src/display.c b/src/display.c @@ -0,0 +1,189 @@ +#include <ctype.h> +#include <stddef.h> +#include <string.h> + +#include <pane.h> +#include <utils.h> + +#include "anki.h" +#include "display.h" +#include "move.h" + +#define USTART L'\u2654' + +wchar_t convert(char l) { + if (l == ' ') + return ' '; + int p = piece_get_index(l); + return (p >= 0) ? p + USTART : L'!'; +} + +void board_display(widget_T widget) { + size_t i, j; + Pane_T pane = widget->pane; + boardInfo_T info = (boardInfo_T)widget->info; + boardStyle_T style = (boardStyle_T)widget->style; + game_T game = (game_T)widget->data->payload; + Board_T board = game->boards[game->display_current]; + + UNUSED(info); + + int bg_color, fg_color; + size_t x_start = centerHorisontal(pane, 16); + size_t y_start = centerVertical(pane, 8); + for (i = 0; i < 8; i++) { + for (j = 0; j < 8; j++) { + bg_color = ((i + j) % 2) ? style->square_dark : style->square_light; + fg_color = style->piece; + tb_set_cell(x_start + 2 * i, y_start + j, + convert(Board_atIndex(board, j, i)), fg_color, bg_color); + tb_set_cell(x_start + 2 * i + 1, y_start + j, ' ', fg_color, bg_color); + } + } + + /* display_grave(Board_grave(board, 'w'), y++, startx); */ + /* display_grave(Board_grave(board, 'b'), y++, startx); */ + + if (style->annotation) { + char *files = "A B C D E F G H"; + char *ranks = "87654321"; + if (style->annotation & 8) + tb_print(x_start, y_start - 1, TB_GREEN, 0, files); + if (style->annotation & 2) + tb_print(x_start, y_start + 8, TB_GREEN, 0, files); + for (i = 0; i < 8; i++) { + if (style->annotation & 1) + tb_printf(x_start - 2, y_start + i, TB_GREEN, 0, "%c", ranks[i]); + if (style->annotation & 4) + tb_printf(x_start + 17, y_start + i, TB_GREEN, 0, "%c", ranks[i]); + } + } + + /* draw_border(x_start - 1, y_start - 1, x_start + 16, y_start + 8, TB_RED); + */ + if (style->border) { + tb_printf(x_start, y_start - 1, 0, style->border, " "); + tb_printf(x_start, y_start + 8, 0, style->border, " "); + for (i = y_start - 1; i < y_start + 9; i++) { + tb_printf(x_start - 2, i, 0, style->border, " "); + tb_printf(x_start + 16, i, 0, style->border, " "); + } + } + + /* if (game->pass) */ + /* tb_printf(x_start, y_start - 2, TB_GREEN, 0, "PASS"); */ + /* else if (game->fail) */ + /* tb_printf(x_start, y_start - 2, TB_GREEN, 0, "FAIL"); */ + /* else */ + /* tb_printf(x_start, y_start - 2, TB_GREEN, 0, " "); */ + + tb_printf(x_start, pane_y(pane) + pane_height(pane) - 1, TB_GREEN, 0, "%*s", + BUFF_SIZE, game->buffer); + + tb_present(); +} + +int game_handleInput(data_T data, struct tb_event ev) { + game_T game = (game_T)data->payload; + + /* if() */ + + switch (ev.key) { + case TB_KEY_ARROW_LEFT: + if (game->display_current > 0) + game->display_current--; + return 1; + case TB_KEY_ARROW_RIGHT: + if (game->display_current < game->move_current) + game->display_current++; + return 1; + case TB_KEY_ARROW_UP: + game->display_current = 0; + return 1; + case TB_KEY_ARROW_DOWN: + game->display_current = game->move_current; + return 1; + case TB_KEY_ENTER: + if (game->move_current == game->boards_size) { + anki_grade(game->pass); + game->review_next(); + return 1; + } + + if (game->buffer_crnt) { + if (strcmp(game->buffer, + Move_halfmove(game->moves, game->move_current)) != 0) { + game->move_fail = game->move_current + 1; + game->move_current = game->boards_size; + game->fail = 1; + } else { + game->display_current = ++game->move_current; + + if (game->move_current == game->boards_size) + game->pass = 1; + + memset(game->buffer, '\0', BUFF_SIZE); + game->buffer_crnt = 0; + } + } + + return 1; + case TB_KEY_BACKSPACE: + case TB_KEY_BACKSPACE2: + CLAMP(game->buffer_crnt, 1, BUFF_SIZE + 1); + game->buffer[--game->buffer_crnt] = '\0'; + return 1; + default: + if (!isalnum(ev.ch) && ev.ch != '+' && ev.ch != '#' && ev.ch != '-') + break; + + if (game->buffer_crnt >= BUFF_SIZE) + break; + + game->buffer[game->buffer_crnt++] = ev.ch; + return 1; + } + + return 0; +} + +void move_display(game_T game, movesStyle_T style, size_t move_index, + char *move, int x, int y) { + int color, background; + color = (game->move_start == move_index) ? TB_GREEN : style->foreground; + color = (game->move_fail == move_index) ? TB_RED : color; + background = + (game->display_current == move_index) ? style->active : style->background; + tb_printf(x, y, color, background, "%*s", style->padding, move); +} + +void moves_display(widget_T widget) { + size_t i; + Pane_T pane = widget->pane; + movesInfo_T info = (movesInfo_T)widget->info; + movesStyle_T style = (movesStyle_T)widget->style; + game_T game = (game_T)widget->data->payload; + + if (!widget->inited) { + widget->inited = 1; + info->list = widgetList_new(widget->pane, game->moves_size); + } + + widgetList_T list = info->list; + int display_row = + ACLAMP(((int)game->display_current - 1) / 2, 0, ((int)game->moves_size)); + widgetList_cacl(list, display_row); + + pane_clear(pane, 0); + int x = centerHorisontal(pane, 4 * style->padding), y = pane_y(pane); + size_t index_end = + MIN(list->index_start + list->display_num, (game->move_current + 1) / 2); + for (i = list->index_start; i < index_end; i++, y++) { + move_T move = game->moves[i]; + move_display(game, style, -1, move->turn, x, y); + move_display(game, style, i * 2 + 1, move->white, x + style->padding, y); + if (i * 2 + 2 <= game->move_current) + move_display(game, style, i * 2 + 2, move->black, x + 2 * style->padding, + y); + } +} diff --git a/src/main.c b/src/main.c @@ -0,0 +1,234 @@ +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define TB_IMPL +#define TB_OPT_TRUECOLOR + +#include <except.h> +#include <mem.h> +#include <menu.h> +#include <pane.h> +#include <utils.h> +#include <widgetList.h> + +#include "anki.h" +#include "board.h" +#include "display.h" +#include "move.h" + +void set_selectDeck(void); +void set_selectProfile(void); + +void start(void); +void stop(void); + +struct menuInfo_T mainMenuInfo; +struct menuStyle_T mainMenuStyle = { + .separator = "<------------->", + .padding = 2, + .spacing = 1, +}; +struct widget_T mainMenu_widget = { + .pane = NULL, + .callback = pane_menu, + .title = "Main Menu", + .style = &mainMenuStyle, + .info = &mainMenuInfo, +}; + +menu_T anki_to_menu(const char **(*getter)(size_t *), menuItem_select_f select, + menu_back_f back) { + size_t i, size; + const char **profiles = getter(&size); + + menu_T menu = menu_new(size, back); + for (i = 0; i < size; i++) { + menu->items[i].select_f = select; + menu->items[i].name = profiles[i]; + } + + return menu; +} + +struct boardInfo_T boardInfo; +struct boardStyle_T boardStyle = { + .square_dark = TB_GREEN, + .square_light = TB_WHITE, + .piece = TB_BLACK, + .border = 0, + .annotation = 3, +}; +struct widget_T board_widget = { + .pane = NULL, + .callback = board_display, + .title = "Board", + .style = &boardStyle, + .info = &boardInfo, +}; + +#define MOVE_PADDING 6 + +struct movesInfo_T movesInfo; +struct movesStyle_T movesStyle = { + .padding = MOVE_PADDING, + .foreground = TB_WHITE, + .background = 0, + .active = TB_BLUE, +}; +struct widget_T moves_widget = { + .pane = NULL, + .callback = moves_display, + .title = "Moves", + .style = &movesStyle, + .info = &movesInfo, +}; + +void title_callback(widget_T widget) { + game_T game = (game_T)widget->data->payload; + titleInfo_T info = (titleInfo_T)widget->info; + titleStyle_T style = (titleStyle_T)widget->style; + + data_T data = data_new(card_name(game->card), NULL); + widget_setData(info->subwidget, data); + info->subwidget->pane = widget->pane; + info->subwidget->callback(info->subwidget); + + UNUSED(style); +} + +struct widget_T center_widget = { + .pane = NULL, + .callback = widgetCenter_print, + .title = "Title", +}; + +struct titleInfo_T titleInfo = { + .subwidget = &center_widget, +}; + +struct titleStyle_T titleStyle; + +struct widget_T title_widget = { + .pane = NULL, + .callback = title_callback, + .title = "Title", + .style = &titleStyle, + .info = &titleInfo, +}; + +void review_card() { + char *fen; + size_t moves_size, i; + + card_T card = anki_current_card(); + move_T *moves = Move_list(card_pgn(card), &moves_size); + game_T game = game_new(moves_size); + + game->boards[0] = (fen = card_fen(card)) ? Board_from_FEN(fen) : Board_new(); + for (i = 0; i < moves_size; i++) { + int base = i * 2; + game->boards[base + 1] = Board_play(game->boards[base], moves[i]->white); + game->boards[base + 2] = + Board_play(game->boards[base + 1], moves[i]->black); + } + game->boards_size = + moves_size * 2 - (strcmp(moves[moves_size - 1]->black, "*") == 0); + + game->card = card; + game->review_next = review_card; + game->move_start = game->move_current = game->display_current = + atoi(card_start(card)); + game->moves = moves; + /* game->pass = 1; */ + + data_T data = data_new(game, game_handleInput); + widget_setData(&board_widget, data); + widget_setData(&moves_widget, data); + widget_setData(&title_widget, data); + + /* widgetCenter_print(widgetTitle, card_name(card)); */ +} + +void deck_selected(char *name, int ignore) { + UNUSED(ignore); + Pane_T *children1, *children2; + + anki_load_deck(name); + + pane_clear(MAIN, 1); + children1 = pane_vsplit(MAIN, 2, -5, 1); + children2 = pane_split(children1[1], 2, -(MOVE_PADDING * 3 + 4), 2); + + review_card(); + widget_activate(&title_widget, children1[0]); + widget_activate(&moves_widget, children2[0]); + widget_activate(&board_widget, children2[1]); +} + +void profile_selected(char *name, int ignore) { + UNUSED(ignore); + anki_load_profile(name); + set_selectDeck(); +} + +void set_selectProfile() { + menu_T menu = anki_to_menu(anki_get_profiles, profile_selected, stop); + data_T data = data_new(menu, menu_hadleInput); + widget_setData(&mainMenu_widget, data); +} + +void set_selectDeck() { + menu_T menu = anki_to_menu(anki_get_decks, deck_selected, set_selectProfile); + data_T data = data_new(menu, menu_hadleInput); + widget_setData(&mainMenu_widget, data); +} + +int main(void) { + start(); + { + set_selectProfile(); + widget_activate(&mainMenu_widget, MAIN); + pane_event_loop(); + } + stop(); + + return 0; +} + +void stop(void) { + anki_stop(); + pane_stop(); + exit(1); +} + +struct widget_T error_widget = { + .pane = NULL, + .callback = widgetCenter_print, + .title = "Error", +}; + +void start(void) { + struct tb_event ev; + char message[] = "Can't start anki, make sure it's running and try again"; + + pane_start(0); + data_T data = data_new(message, NULL); + widget_setData(&error_widget, data); + widget_activate(&error_widget, MAIN); + + TRY { anki_start(); } + EXCEPT(ANKIE_CONNECT) { + error_widget.callback(&error_widget); + tb_poll_event(&ev); + if (ev.ch == 'q' || ev.key == TB_KEY_ESC) + stop(); + start(); + } + ELSE { + stop(); + RERAISE; + } + END_TRY; +} diff --git a/src/move.c b/src/move.c @@ -0,0 +1,47 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "mem.h" +#include "move.h" + +#define MAX_PLAY 1024 + +move_T Move_new(char *turn, char *white, char *black) { + move_T m; + NEW0(m); + + strcpy(m->turn, turn); + strcpy(m->white, white); + strcpy(m->black, black); + return m; +} + +move_T *Move_list(char *pgn, size_t *size) { + char *turn, *white, *black, *crnt; + char **test[] = {&turn, &white, &black}; + size_t moves_size = 0, cnt = 0; + + move_T *moves = malloc(MAX_PLAY * sizeof(*moves)); + for (crnt = strtok(pgn, " "); crnt; crnt = strtok(NULL, " ")) { + *test[cnt++] = crnt; + if (cnt == 3) { + moves[moves_size++] = Move_new(turn, white, black); + *test[1] = NULL; + cnt = 0; + } + } + + if (*test[1]) + moves[moves_size++] = Move_new(turn, white, ""); + + if (size) + *size = moves_size; + + return moves; +} + +char *Move_halfmove(move_T *moves, int halfturn) { + return (halfturn % 2) ? moves[halfturn / 2]->black + : moves[halfturn / 2]->white; +}