/* Copyright (C) 2019 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "asset.h" #include "cards.h" #include "layout.h" #include "stack.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) enum { Stock, Waste1, Waste3, Foundation1, Foundation2, Foundation3, Foundation4, Tableau1, Tableau2, Tableau3, Tableau4, Tableau5, Tableau6, Tableau7, StacksLen, }; static struct Stack stacks[StacksLen]; // TODO: Scoring method, score, time. static struct { uint draw; } game = { .draw = 3, }; typedef void StackFn(struct Stack *dst, struct Stack *src, Uint8 len); static struct { StackFn *fn; uint dst; uint src; uint len; } undo; static void gameDo(StackFn *fn, uint dst, uint src, uint len) { undo.fn = fn; undo.dst = src; undo.src = dst; undo.len = len; fn(&stacks[dst], &stacks[src], len); } static bool gameUndo(void) { if (!undo.fn) return false; undo.fn(&stacks[undo.dst], &stacks[undo.src], undo.len); undo.fn = NULL; return true; } static void gameDeal(void) { undo.fn = NULL; for (uint i = 0; i < StacksLen; ++i) { stackClear(&stacks[i]); } for (Card i = 1; i <= 52; ++i) { stackPush(&stacks[Stock], -i); } stackShuffle(&stacks[Stock]); for (uint i = Tableau1; i <= Tableau7; ++i) { stackMoveTo(&stacks[i], &stacks[Stock], i - Tableau1); stackFlipTo(&stacks[i], &stacks[Stock], 1); } } static void gameDraw(void) { stackMoveTo(&stacks[Waste1], &stacks[Waste3], stacks[Waste3].len); if (stacks[Stock].len) { if (game.draw > 1) { gameDo(stackFlipTo, Waste3, Stock, game.draw); } else { gameDo(stackFlipTo, Waste1, Stock, 1); } } else { gameDo(stackFlipTo, Stock, Waste1, stacks[Waste1].len); } } static bool gameFind(uint *stack, uint *index, Card card) { for (*stack = 0; *stack < StacksLen; ++*stack) { for (*index = 0; *index < stacks[*stack].len; ++*index) { if (stacks[*stack].cards[*index] == card) return true; } } return false; } static bool gameAvail(Card card) { uint stack, index; if (card < 0) return false; if (!gameFind(&stack, &index, card)) return false; bool top = (card == stackTop(&stacks[stack])); if (stack == Waste1) { return top && !stacks[Waste3].len; } else if (stack == Waste3) { return top; } else if (stack >= Foundation1 && stack <= Foundation4) { return top; } else if (stack >= Tableau1 && stack <= Tableau7) { return true; } else { return false; } } static bool gameReveal(Card card) { uint stack, index; if (!gameFind(&stack, &index, card)) return false; if (stack < Tableau1 || stack > Tableau7) return false; if (card != stackTop(&stacks[stack])) return false; if (card > 0) return false; gameDo(stackFlipTo, stack, stack, 1); return true; } static bool gameMove(uint dest, Card card) { uint source, index; if (!gameFind(&source, &index, card)) return false; if (source == dest) return false; uint count = stacks[source].len - index; Card destTop = stackTop(&stacks[dest]); if (dest >= Foundation1 && dest <= Foundation4) { if (count > 1) return false; if (!destTop && cardRank(card) != Cards_A) return false; if (destTop && cardSuit(card) != cardSuit(destTop)) return false; if (destTop && cardRank(card) != cardRank(destTop) + 1) return false; gameDo(stackMoveTo, dest, source, 1); return true; } if (dest >= Tableau1 && dest <= Tableau7) { if (!destTop && cardRank(card) != Cards_K) return false; if (destTop && cardColor(card) == cardColor(destTop)) return false; if (destTop && cardRank(card) != cardRank(destTop) - 1) return false; gameDo(stackMoveTo, dest, source, count); return true; } return false; } enum { CardWidth = Cards_CardWidth, CardHeight = Cards_CardHeight, StackMarginX = 11, StackMarginY = 6, StockX = StackMarginX, StockY = StackMarginY, WasteX = StockX + CardWidth + StackMarginX, WasteY = StackMarginY, FoundationX = WasteX + 2 * (CardWidth + StackMarginX), FoundationY = StackMarginY, TableauX = StackMarginX, TableauY = StockY + CardHeight + StackMarginY, FanRightDeltaX = 14, FanDownDeltaYBack = 3, FanDownDeltaYFront = 15, WindowWidth = 7 * CardWidth + 8 * StackMarginX, WindowHeight = 2 * (StackMarginY + CardHeight) + 6 * FanDownDeltaYBack + 12 * FanDownDeltaYFront + StackMarginY, }; static const struct Style FanRight = { 1, 0, 0, FanRightDeltaX, SquareDeltaY }; static const struct Style FanDown = { 1, 0, FanDownDeltaYBack, 0, FanDownDeltaYFront }; static struct SDL_Rect stackRects[StacksLen]; static struct Layout layout; static uint backTexture = Cards_Back1; static void updateLayout(void) { layoutClear(&layout); SDL_Rect stock = { StockX, StockY, CardWidth, CardHeight }; stackRects[Stock] = stock; listPush(&layout.main, &stock, Cards_O); layoutStack(&layout, &stock, &stacks[Stock], &Square); SDL_Rect waste = { WasteX, WasteY, CardWidth, CardHeight }; layoutStack(&layout, &waste, &stacks[Waste1], &Square); layoutStack(&layout, &waste, &stacks[Waste3], &FanRight); SDL_Rect found = { FoundationX, FoundationY, CardWidth, CardHeight }; for (uint i = Foundation1; i <= Foundation4; ++i) { stackRects[i] = found; listPush(&layout.main, &found, Cards_Empty); SDL_Rect rect = found; layoutStack(&layout, &rect, &stacks[i], &Square); found.x += CardWidth + StackMarginX; } SDL_Rect table = { TableauX, TableauY, CardWidth, CardHeight }; for (uint i = Tableau1; i <= Tableau7; ++i) { stackRects[i] = table; stackRects[i].h = WindowHeight; SDL_Rect rect = table; layoutStack(&layout, &rect, &stacks[i], &FanDown); table.x += CardWidth + StackMarginX; } } static bool keyDown(SDL_KeyboardEvent key) { switch (key.keysym.sym) { case SDLK_F2: gameDeal(); return true; case SDLK_BACKSPACE: return gameUndo(); case SDLK_b: { backTexture -= Cards_Back1; backTexture += 1; backTexture %= 12; backTexture += Cards_Back1; return true; } case SDLK_d: { game.draw = (game.draw == 1 ? 3 : 1); gameDeal(); return true; } default: return false; } } static bool mouseButtonDown(SDL_MouseButtonEvent button) { struct SDL_Point point = { button.x, button.y }; if (SDL_PointInRect(&point, &stackRects[Stock])) { gameDraw(); return true; } struct Item *item = listFind(&layout.main, &point); if (!item) return false; if (!gameAvail(item->card)) return gameReveal(item->card); if (button.clicks % 2 == 0) { for (uint dest = Foundation1; dest <= Foundation4; ++dest) { if (gameMove(dest, item->card)) return true; } return false; } layout.dragItem = *item; return true; } static bool mouseButtonUp(SDL_MouseButtonEvent button) { (void)button; if (!layout.dragItem.card) return false; for (uint dest = 0; dest < StacksLen; ++dest) { if (SDL_HasIntersection(&layout.dragItem.rect, &stackRects[dest])) { if (gameMove(dest, layout.dragItem.card)) break; } } layout.dragItem.card = 0; return true; } static bool mouseMotion(SDL_MouseMotionEvent motion) { if (!motion.state) return false; layout.dragItem.rect.x += motion.xrel; layout.dragItem.rect.y += motion.yrel; return true; } static SDL_Renderer *render; static SDL_Texture *textures[Cards_CardCount]; static void renderList(const struct List *list) { for (uint i = 0; i < list->len; ++i) { int tex = list->items[i].card; if (tex < 0) tex = backTexture; SDL_RenderCopy(render, textures[tex], NULL, &list->items[i].rect); } } static void err(const char *title) { int error = SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, title, SDL_GetError(), NULL ); if (error) fprintf(stderr, "%s\n", SDL_GetError()); exit(EXIT_FAILURE); } int main(void) { if (SDL_Init(SDL_INIT_VIDEO) < 0) err("SDL_Init"); atexit(SDL_Quit); struct Paths paths; if (assetPaths(&paths) < 0) err("SDL_GetPrefPath"); SDL_RWops *rw = assetOpenCards(&paths); if (!rw) return EXIT_FAILURE; SDL_Surface *surfaces[Cards_CardCount]; int error = Cards_LoadCards( surfaces, Cards_CardCount, rw, Cards_ColorKey | Cards_AlphaCorners | Cards_BlackBorders ); if (error) err("Cards_LoadCards"); SDL_RWclose(rw); SDL_Window *window; error = SDL_CreateWindowAndRenderer( WindowWidth, WindowHeight, SDL_WINDOW_ALLOW_HIGHDPI, &window, &render ); if (error) err("SDL_CreateWindowAndRenderer"); SDL_SetWindowTitle(window, "Solitaire"); SDL_RenderSetIntegerScale(render, SDL_TRUE); SDL_RenderSetLogicalSize(render, WindowWidth, WindowHeight); for (uint i = 0; i < Cards_CardCount; ++i) { textures[i] = NULL; if (!surfaces[i]) continue; textures[i] = SDL_CreateTextureFromSurface(render, surfaces[i]); if (!textures[i]) err("SDL_CreateTextureFromSurface"); SDL_FreeSurface(surfaces[i]); } srand(time(NULL)); backTexture = Cards_Back1 + randUniform(12); gameDeal(); for (;;) { updateLayout(); SDL_SetRenderDrawColor(render, 0x00, 0xAA, 0x55, 0xFF); SDL_RenderClear(render); renderList(&layout.main); renderList(&layout.drag); SDL_RenderPresent(render); SDL_Event event; for (;;) { SDL_WaitEvent(&event); if (event.type == SDL_QUIT) { goto quit; } else if (event.type == SDL_KEYDOWN) { if (keyDown(event.key)) break; } else if (event.type == SDL_MOUSEBUTTONDOWN) { if (mouseButtonDown(event.button)) break; } else if (event.type == SDL_MOUSEBUTTONUP) { if (mouseButtonUp(event.button)) break; } else if (event.type == SDL_MOUSEMOTION) { if (mouseMotion(event.motion)) break; } } } quit: SDL_Quit(); }