├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.en.md ├── README.md ├── asset ├── demo.gif └── image1.png ├── bot ├── bot.py ├── conf.py └── util.py ├── lib └── CMakeLists.txt ├── notes.md ├── script ├── build.bat ├── build.sh ├── daemon.sh ├── log_analyzer.sh ├── server.conf └── uno_server_ctl.sh ├── src ├── CMakeLists.txt ├── common │ ├── common.h │ ├── config.cpp │ ├── config.h │ ├── terminal.cpp │ ├── terminal.h │ ├── util.cpp │ └── util.h ├── game │ ├── cards.cpp │ ├── cards.h │ ├── game_board.cpp │ ├── game_board.h │ ├── info.cpp │ ├── info.h │ ├── player.cpp │ ├── player.h │ ├── stat.cpp │ └── stat.h ├── main.cpp ├── network │ ├── client.cpp │ ├── client.h │ ├── msg.cpp │ ├── msg.h │ ├── server.cpp │ ├── server.h │ ├── session.cpp │ └── session.h └── ui │ ├── inputter.cpp │ ├── inputter.h │ ├── outputter.cpp │ ├── outputter.h │ ├── ui_manager.cpp │ ├── ui_manager.h │ ├── view.cpp │ ├── view.h │ ├── view_formatter.cpp │ └── view_formatter.h └── test ├── CMakeLists.txt ├── bot_test.py ├── game ├── card_test.cpp ├── game_board_test.cpp ├── game_stat_test.cpp ├── info_test.cpp └── player_stat_test.cpp ├── main.cpp ├── mock.h └── network └── session_test.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # build 35 | build 36 | 37 | # vscode 38 | .vscode 39 | 40 | # python 41 | __pycache__ 42 | playground.py 43 | 44 | # log 45 | logs 46 | *.log 47 | 48 | # script 49 | playground.sh -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/cxxopts"] 2 | path = lib/cxxopts 3 | url = git@github.com:jarro2783/cxxopts.git 4 | [submodule "lib/googletest"] 5 | path = lib/googletest 6 | url = git@github.com:google/googletest.git 7 | [submodule "lib/yaml-cpp"] 8 | path = lib/yaml-cpp 9 | url = git@github.com:jbeder/yaml-cpp.git 10 | [submodule "lib/spdlog"] 11 | path = lib/spdlog 12 | url = git@github.com:gabime/spdlog.git 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # enable x.y version format 2 | cmake_policy(SET CMP0048 NEW) 3 | project(UNO VERSION 1.0) 4 | 5 | cmake_minimum_required(VERSION 3.14) 6 | 7 | set(CMAKE_CXX_STANDARD 17) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | # set(CMAKE_BUILD_TYPE Debug) 11 | 12 | option(BUILD_TEST "build tests" OFF) 13 | option(ENABLE_LOG "enable spdlog" OFF) 14 | 15 | # build lib before adding ENABLE_LOG definition to avoid recompiling lib when toggling that option 16 | add_subdirectory(lib) 17 | 18 | if(ENABLE_LOG) 19 | add_compile_definitions("ENABLE_LOG") 20 | # add_definitions(-DENABLE_LOG) 21 | endif() 22 | 23 | add_subdirectory(src) 24 | 25 | if(BUILD_TEST) 26 | add_subdirectory(test) 27 | endif() 28 | 29 | install(TARGETS uno RUNTIME DESTINATION bin) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tao Bocheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | [简体中文](./README.md) | English 2 | 3 | # UNO 4 | 5 | ![](https://img.shields.io/badge/version-v1.0-9cf) 6 | 7 | Hooray! Card game **UNO**! 8 | 9 | ## Demo 10 | 11 | ![](./asset/demo.gif) 12 | 13 | *a game of 3 players* 14 | 15 | ## Install 16 | 17 | ### Download executable 18 | 19 | Visit [Releases](https://github.com/Gusabary/UNO/releases) page to download executable directly. 20 | 21 | ### Build from source code 22 | 23 | Of course you can also build from source code. 24 | 25 | #### Prerequisite 26 | 27 | + Make sure your CMake version is >= 3.14 28 | + Make sure your C++ compiler has C++17 support 29 | 30 | | Platform | Build | 31 | | -------- | ------------------------------------------------------------ | 32 | | Linux | gcc 8.4.0 ![](https://img.shields.io/badge/build-passing-brightgreen) | 33 | | Windows | MSVC 19.28 (Visual Studio 16 2019) ![](https://img.shields.io/badge/build-passing-brightgreen) | 34 | | MacOS | AppleClang 10.0.1 ![](https://img.shields.io/badge/build-passing-brightgreen) | 35 | 36 | #### Start building 37 | 38 | Clone the repository and run the build script: 39 | 40 | ```bash 41 | git clone git@github.com:Gusabary/UNO.git 42 | cd UNO/script 43 | 44 | # for linux and mac 45 | chmod +x ./build.sh 46 | ./build.sh 47 | 48 | # for windows 49 | .\build.bat 50 | ``` 51 | 52 | After the build process, `uno` executable will be under `build/src` directory. 53 | 54 | (If encounter some problems related to git submodule when building, take this [issue](https://github.com/Gusabary/UNO/issues/1) for a reference) 55 | 56 | Optionally, `uno` can be installed by running: 57 | 58 | ```shell 59 | make install 60 | ``` 61 | 62 | Also, you can specify CMake option `BUILD_TEST` and `ENABLE_LOG` to build test and log. 63 | 64 | Browse Wiki to see complete info about [build options](https://github.com/Gusabary/UNO/wiki/Configuration). 65 | 66 | ## Getting started 67 | 68 | It's recommended to use VSCode built-in terminal. There might be an annoying blink effect in most of other terminals, which negatively affects player experience. 69 | 70 | ### Start your first game 71 | 72 | ```shell 73 | ./uno -c 8.133.165.59:20020 -u username 74 | ``` 75 | 76 | `-c` indicates the address of the game service and `-u` indicates the username of player. 77 | 78 | `8.133.165.59:20020` is a pre-started game service, which is a 2-player game and includes a bot (so you'll play with it). If this service has been occupied, you can try another service on port 20021 ~ 20024. 79 | 80 | Browse Wiki to see more about [configuration info](https://github.com/Gusabary/UNO/wiki/Configuration) and [pre-started game services](https://github.com/Gusabary/UNO/wiki/Prestarted-Game-Services). 81 | 82 | After entering the game, UI is like below: 83 | 84 | ![](./asset/image1.png) 85 | 86 | Just like what the hint text shows, press `,` or `.` to move the cursor, press Enter to play the card denoted by the cursor and press Space to skip (i.e. draw a card, unless the last played card is Skip card). 87 | 88 | Browse Wiki to see complete introduction to [UI and operation mode](https://github.com/Gusabary/UNO/wiki/UI-and-Operation-Mode), and the [game rules](https://github.com/Gusabary/UNO/wiki/Game-Rules) which might be a little different from UNO you've played. 89 | 90 | ### Start your own game service 91 | 92 | ``` 93 | ./uno -l 9091 94 | ``` 95 | 96 | `-l` indicated the port your game service will listen on. 97 | 98 | Server started, player can connect to it with `./uno -c localhost:9091`. If the machine on which server is running has a public IP (e.g. `x.y.z.w`), players can connect to it with `./uno -c x.y.z.w:9091`. (If failure, check network configuration like firewall, security group and port mapping to make sure your service is correctly exposed) 99 | 100 | Optionally, specify `-n` argument to set the number of players of the game service. (default is 3) 101 | 102 | ## To-do 103 | 104 | - [ ] Improve player experience about network 105 | - [ ] Chinese support in the game 106 | - [ ] Better support for Windows platform 107 | - [ ] Perfect the details of UNO rules 108 | - [ ] Configure keyboard mappings 109 | - [ ] Find suitable Unicode characters to represent Reverse and Skip card 110 | - [ ] Unique banner 111 | 112 | Browse Wiki to see detailed [requirements of to-do features](https://github.com/Gusabary/UNO/wiki/Requirements-of-Todo-Features). 113 | 114 | *Help Wanted!* 115 | 116 | ## Miscellaneous 117 | 118 | The gadget cost me on and off most of this semester. My intention is to learn [Asio network library](http://think-async.com/Asio/index.html) and modern C++ language features with practice and the game itself is just something like a tool or medium. So there is still some flaw about game rules of UNO and player experience. However, with the thousands of lines of code, my comprehension and use of modern C++ is indeed improved, including smart pointers, rvalue reference, lambda expressions and concurrency. Also, I was trying to use some C++17 features like structural binding, constexpr if, `std::optional` and so on, although it seems that they are used not so reasonably somewhere. 119 | 120 | Anyway, the past is in the past. If you are interested and have spare time, welcome to join us to make the gadget better. If not, that's also fine. Start an exciting online UNO game with your friends! 121 | 122 | ## License 123 | 124 | [MIT](./LICENSE) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.en.md) 2 | 3 | # UNO 4 | 5 | ![](https://img.shields.io/badge/version-v1.0-9cf) 6 | 7 | 没错!纸牌游戏 **UNO**! 8 | 9 | ## 示例 10 | 11 | ![](./asset/demo.gif) 12 | 13 | *三人对局* 14 | 15 | ## 安装 16 | 17 | ### 下载可执行文件 18 | 19 | 前往 [Releases](https://github.com/Gusabary/UNO/releases) 页面以下载构建好的可执行文件。 20 | 21 | ### 从源码构建 22 | 23 | 当然您也可以从源码构建。 24 | 25 | #### 准备工作 26 | 27 | + 确保 CMake 版本 >= 3.14 28 | + 确保编译器支持 C++17 29 | 30 | | 平台 | 构建 | 31 | | ------- | ------------------------------------------------------------ | 32 | | Linux | GCC 8.4.0 ![](https://img.shields.io/badge/build-passing-brightgreen) | 33 | | Windows | MSVC 19.28 (Visual Studio 16 2019) ![](https://img.shields.io/badge/build-passing-brightgreen) | 34 | | MacOS | AppleClang 10.0.1 ![](https://img.shields.io/badge/build-passing-brightgreen) | 35 | 36 | #### 开始构建 37 | 38 | 克隆仓库并进行构建: 39 | 40 | ```bash 41 | git clone git@github.com:Gusabary/UNO.git 42 | cd UNO/script 43 | 44 | # for linux and mac 45 | chmod +x ./build.sh 46 | ./build.sh 47 | 48 | # for windows 49 | .\build.bat 50 | ``` 51 | 52 | 构建完成后,`uno` 可执行文件会在 `build/src` 目录下。 53 | 54 | (如果在构建过程中遇到有关 git 子模块的配置问题,可以参考该 [issue](https://github.com/Gusabary/UNO/issues/1)) 55 | 56 | 可选地,将 `uno` 可执行文件安装到 `PATH` 路径下: 57 | 58 | ```shell 59 | make install 60 | ``` 61 | 62 | 在构建的过程中,可以指定 CMake 的 `BUILD_TEST` 和 `ENABLE_LOG` 选项以构建测试和日志。 63 | 64 | 浏览 Wiki 以查看完整的[构建选项](https://github.com/Gusabary/UNO/wiki/%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF)信息。 65 | 66 | ## 快速开始 67 | 68 | 推荐使用 VSCode 的终端,其他大部分终端都会有类似闪烁的现象,比较影响游戏体验。 69 | 70 | ### 开始第一场对局 71 | 72 | ```shell 73 | ./uno -c 8.133.165.59:20020 -u username 74 | ``` 75 | 76 | 其中,`-c` 参数为游戏的服务器地址,`-u` 参数为玩家的用户名。 77 | 78 | `8.133.165.59:20020` 为预启动的一个游戏服务,该服务配置为两人对局并有一个电脑,即人机对战。如果该服务已被占用,玩家可以尝试连接 20021 ~ 20024 中的任意端口。 79 | 80 | 浏览 Wiki 以查看更多关于[启动参数配置](https://github.com/Gusabary/UNO/wiki/%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF)和[预启动游戏服务](https://github.com/Gusabary/UNO/wiki/%E9%A2%84%E5%90%AF%E5%8A%A8%E7%9A%84%E6%B8%B8%E6%88%8F%E6%9C%8D%E5%8A%A1)的信息。 81 | 82 | 进入游戏后界面如下所示: 83 | 84 | ![](./asset/image1.png) 85 | 86 | 按照提示文字所显示的,按下 `,` 或 `.` 键以移动光标,按下回车键以打出光标目前所指的手牌,按下空格键以跳过出牌(即摸牌,除非前一个玩家打出 Skip 牌则可以不摸) 87 | 88 | 浏览 Wiki 以查看完整的 [UI 及操作方式](https://github.com/Gusabary/UNO/wiki/UI-%E5%8F%8A%E6%93%8D%E4%BD%9C%E6%96%B9%E5%BC%8F)的介绍,以及可能和您玩过的 UNO 稍有不同的[游戏规则](https://github.com/Gusabary/UNO/wiki/%E6%B8%B8%E6%88%8F%E8%A7%84%E5%88%99)。 89 | 90 | ### 启动自己的游戏服务 91 | 92 | ``` 93 | ./uno -l 9091 94 | ``` 95 | 96 | 其中,`-l` 参数为游戏服务所在的端口。 97 | 98 | 启动以后,玩家可以通过 `./uno -c localhost:9091` 进行连接。如果游戏服务所在的机器有公网 IP(例如 `x.y.z.w`),别的玩家可以通过 `./uno -c x.y.z.w:9091` 进行连接。(如果网络连接失败,建议先检查防火墙、安全组、端口映射等网络配置,确保服务已暴露出去) 99 | 100 | 可选地,通过 `-n` 参数指定该对局的玩家人数(默认为 3 人)。 101 | 102 | ## 待实现 103 | 104 | - [ ] 改善关于网络连接的玩家体验 105 | - [ ] 游戏内中文支持 106 | - [ ] 更完善的 Windows 支持 107 | - [ ] 完善 UNO 规则细节 108 | - [ ] 配置键盘映射方式 109 | - [ ] 寻找合适的 Unicode 字符以表示 Reverse 和 Skip 牌 110 | - [ ] 个性化 banner 111 | 112 | 浏览 Wiki 以查看[待实现功能的具体需求](https://github.com/Gusabary/UNO/wiki/%E5%BE%85%E5%AE%9E%E7%8E%B0%E5%8A%9F%E8%83%BD%E7%9A%84%E8%AF%A6%E7%BB%86%E9%9C%80%E6%B1%82)。 113 | 114 | *寻求帮助!* 115 | 116 | ## 杂感 117 | 118 | 这个小玩意儿断断续续差不多写了大半个学期,本意是想以实践的方式学习 [Asio 网络库](http://think-async.com/Asio/index.html)和现代 C++ 的一些语言特性,游戏本身只是一个载体,所以不管是在对 UNO 规则的完整支持还是玩家的体验上,仍然有不少的缺憾。但是通过这小几千行代码,我的的确确精进了不少对于现代 C++ 的理解和运用,包括智能指针、右值引用、Lambda 表达式以及并发。我也在尝试使用 C++17 的一些语法特性,比如结构化绑定、编译期 if、`std::optional` 等等等等,不过也并非所有地方都使用得恰到好处,有些写法就难免有矫揉造作之嫌。 119 | 120 | 总之,第一阶段的工作已经告一段落,目前的成果离我最开始的设想与预期也没有很大的偏差。如果您有兴趣和时间,欢迎与我们一起将这个小玩意儿变得更好;如果没有也没关系,叫上朋友们一起来一局紧张刺激的线上 UNO 吧! 121 | 122 | ## 使用许可 123 | 124 | [MIT](./LICENSE) -------------------------------------------------------------------------------- /asset/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gusabary/UNO/05b8a12c89dac843da8eb3edfdb3655fe8835ca7/asset/demo.gif -------------------------------------------------------------------------------- /asset/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gusabary/UNO/05b8a12c89dac843da8eb3edfdb3655fe8835ca7/asset/image1.png -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | import os, sys 3 | import re 4 | import util 5 | 6 | class Bot: 7 | def __init__(self, endpoint, username="bot", player_num=2, debug=False): 8 | project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | if os.name == 'nt': 10 | # windows 11 | uno_path = project_path + "\\build\\src\\Debug\\uno.exe" 12 | else: 13 | # unix 14 | uno_path = project_path + "/build/src/uno" 15 | args = [uno_path, "-c", endpoint, "-u", username] 16 | 17 | self.game = Popen(args, stdin=PIPE, stdout=PIPE) 18 | self.player_num = player_num 19 | 20 | self.last_played_card = None 21 | self.handcards = [] 22 | self.cursor_index = 0 23 | 24 | self.hint_stat = -1 25 | self.last_hint_stat = -1 26 | self.time_to_think = -1 27 | 28 | self.debug = debug 29 | 30 | def press_keyboard(self, action=' '): 31 | if self.debug: 32 | if action == ' ': 33 | print("press: ") 34 | elif action == '\n': 35 | print("press: \\n") 36 | else: 37 | print("press:", action) 38 | 39 | action = bytes(action, "UTF-8") 40 | self.game.stdin.write(action) 41 | self.game.stdin.flush() 42 | 43 | def single_loop(self): 44 | if self.debug: 45 | print('--------------------------------------------------------------------------') 46 | frame_info = util.next_frame(self.game, self.player_num, self.debug) 47 | if self.debug: 48 | print(frame_info) 49 | self.hint_stat, self.last_played_card, self.handcards, self.cursor_index = frame_info 50 | if self.hint_stat > 0: 51 | # it's my turn 52 | if self.hint_stat != self.last_hint_stat: 53 | # see something new, need time to think 54 | self.time_to_think = 2 55 | self.last_hint_stat = self.hint_stat 56 | 57 | if self.hint_stat == 4: 58 | # always want to play again 59 | self.press_keyboard('y') 60 | return 61 | 62 | if self.time_to_think == 0: 63 | # ok, do some action 64 | if self.hint_stat == 1: 65 | self.try_play_card() 66 | elif self.hint_stat == 2: 67 | self.try_play_card_immediately() 68 | elif self.hint_stat == 3: 69 | self.specify_next_color() 70 | else: 71 | assert False 72 | else: 73 | self.time_to_think -= 1 74 | else: 75 | # not my turn 76 | self.last_hint_stat = -1 77 | 78 | def game_loop(self): 79 | while True: 80 | # abstract out single loop method for better test 81 | self.single_loop() 82 | 83 | def try_play_card(self): 84 | # press keyboard only once for each frame 85 | index_of_card_to_play = -1 86 | for i in range(len(self.handcards)): 87 | if util.can_be_played_after(self.handcards[i], self.last_played_card, len(self.handcards)): 88 | index_of_card_to_play = i 89 | break 90 | if self.debug: 91 | print("index of card to play:", index_of_card_to_play, "\tcursor index:", self.cursor_index) 92 | if index_of_card_to_play == -1: 93 | # no card can be played 94 | self.press_keyboard(' ') 95 | else: 96 | # there is a card to play 97 | if index_of_card_to_play < self.cursor_index: 98 | self.press_keyboard(',') 99 | elif index_of_card_to_play > self.cursor_index: 100 | self.press_keyboard('.') 101 | else: 102 | self.press_keyboard('\n') 103 | 104 | def try_play_card_immediately(self): 105 | card_to_play = self.handcards[self.cursor_index] 106 | if util.can_be_played_after(card_to_play, self.last_played_card, len(self.handcards)): 107 | self.press_keyboard('\n') 108 | else: 109 | self.press_keyboard(' ') 110 | 111 | def specify_next_color(self): 112 | first_handcard = self.handcards[0] 113 | if first_handcard in ['W', '+4']: 114 | self.press_keyboard('r') 115 | else: 116 | self.press_keyboard(first_handcard[0]) 117 | 118 | if __name__ == "__main__": 119 | endpoint = sys.argv[1] 120 | player_num = int(sys.argv[2]) 121 | if len(sys.argv) > 3: 122 | username = sys.argv[3] 123 | else: 124 | username = "bot" 125 | bot = Bot(endpoint, player_num=player_num, username=username, debug=True) 126 | bot.game_loop() -------------------------------------------------------------------------------- /bot/conf.py: -------------------------------------------------------------------------------- 1 | pos_of_player_box = [ 2 | [], [], 3 | [(10, 0), (0, 10)], 4 | [(10, 6), (0, 0), (0, 32)], 5 | [(15, 10), (7, 0), (0, 20), (7, 40)] 6 | ] 7 | 8 | pos_of_last_played_card = [ 9 | (), (), 10 | (8, 20), 11 | (8, 26), 12 | (10, 30) 13 | ] 14 | 15 | base_scale_of_view = [ 16 | (), (), 17 | (16, 42), 18 | (16, 54), 19 | (21, 62) 20 | ] 21 | 22 | pos_of_uno_text = [ 23 | (), (), 24 | (8, 24), 25 | (8, 30), 26 | (11, 29) 27 | ] -------------------------------------------------------------------------------- /bot/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | import conf 3 | 4 | def get_ui_line_nums(player_num, handcards_num): 5 | base_line_num = conf.base_scale_of_view[player_num][0] 6 | # -1 // 8 == -1 in python 7 | extra_line_num = max((handcards_num - 1) // 8, 0) 8 | return base_line_num + extra_line_num 9 | 10 | def remove_escape(s): 11 | return s.replace("\x1b[31m", "") \ 12 | .replace("\x1b[32m", "") \ 13 | .replace("\x1b[33m", "") \ 14 | .replace("\x1b[34m", "") \ 15 | .replace("\x1b[0m", "") 16 | 17 | def is_special_card(card): 18 | if card in ['W', '+4']: 19 | return True 20 | if card[1:] in ['S', 'R', '+2']: 21 | return True 22 | return False 23 | 24 | def can_be_played_after(card_to_play, last_played_card, handcards_num): 25 | _card_to_play = remove_escape(card_to_play) 26 | _last_played_card = remove_escape(last_played_card) 27 | _last_color = _last_played_card[0] 28 | _last_text = _last_played_card[1:] 29 | 30 | if handcards_num == 1 and is_special_card(_card_to_play): 31 | return False 32 | if _last_text == 'S': 33 | return _card_to_play[1:] == 'S' 34 | if _last_text == '+2': 35 | return _card_to_play[1:] in ['+2', '+4'] 36 | if _last_text == '+4': 37 | return _card_to_play[1:] == '+4' 38 | if _card_to_play in ['W', '+4']: 39 | return True 40 | return _card_to_play[0] == _last_color or _card_to_play[1:] == _last_text 41 | 42 | def update_handcards(handcards, line): 43 | cursor_index = -1 44 | _line = line.split()[1:-1] 45 | for i in range(len(_line)): 46 | if _line[i][0] == '>': 47 | cursor_index = i 48 | _line[i] = _line[i][1:] 49 | break 50 | 51 | return handcards + _line, cursor_index 52 | 53 | def next_frame(game, player_num, debug=False): 54 | cur_line_num = 0 55 | is_updating_handcards = False 56 | stat = 0 57 | lines_left_num = -1 58 | 59 | # return value 60 | last_played_card = None 61 | handcards = [] 62 | cursor_index = -1 63 | 64 | _pos_of_last_played_card = conf.pos_of_last_played_card[player_num] 65 | _pos_of_my_box = conf.pos_of_player_box[player_num][0] 66 | 67 | while True: 68 | line = game.stdout.readline().decode("UTF-8").replace("\n", "") 69 | if debug: 70 | print(cur_line_num, "\t", line) 71 | else: 72 | print(line) 73 | line = remove_escape(line) 74 | 75 | if cur_line_num == 0 and re.match(".*Want to play again", line): 76 | stat = 4 77 | return stat, last_played_card, handcards, cursor_index 78 | 79 | if cur_line_num == _pos_of_last_played_card[0]: 80 | # last played card 81 | if len(line[_pos_of_last_played_card[1]:].split()) > 0: 82 | last_played_card = line[_pos_of_last_played_card[1]:].split()[0].strip() 83 | 84 | if cur_line_num == _pos_of_my_box[0] + 3: 85 | # handcards 86 | is_updating_handcards = True 87 | 88 | if is_updating_handcards: 89 | if re.match(".*\+--------", line): 90 | is_updating_handcards = False 91 | else: 92 | handcards, _cursor_index = update_handcards(handcards, line) 93 | if _cursor_index > -1: 94 | cursor_index = _cursor_index + 8 * (cur_line_num - _pos_of_my_box[0] - 3) 95 | 96 | if cur_line_num == get_ui_line_nums(player_num, len(handcards)) - 1: 97 | if line[_pos_of_my_box[1]] != '[': 98 | # it's not my turn 99 | stat = 0 100 | return stat, last_played_card, handcards, cursor_index 101 | 102 | if cur_line_num == get_ui_line_nums(player_num, len(handcards)): 103 | if re.match("Now it's your turn", line): 104 | lines_left_num = 3 105 | stat = 1 106 | elif re.match("Press Enter to play the card just drawn immediately", line): 107 | lines_left_num = 2 108 | stat = 2 109 | elif re.match("Specify the next color", line): 110 | lines_left_num = 1 111 | stat = 3 112 | else: 113 | assert False 114 | 115 | lines_left_num -= 1 116 | if lines_left_num == 0: 117 | return stat, last_played_card, handcards, cursor_index 118 | 119 | cur_line_num += 1 -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(ASIO_CMAKE_ASIO_TAG asio-1-18-0) 2 | set(ASIO_CMAKE_ASIO_SOURCE_DIR ${PROJECT_SOURCE_DIR}/build) 3 | set(ASIO_CMAKE_ASIO_DEP_DIR "${ASIO_CMAKE_ASIO_SOURCE_DIR}/asio-${ASIO_CMAKE_ASIO_TAG}-src") 4 | 5 | include(FetchContent) 6 | FetchContent_Declare(asio 7 | GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git 8 | GIT_TAG ${ASIO_CMAKE_ASIO_TAG} 9 | GIT_SHALLOW TRUE # shallow clone 10 | SOURCE_DIR ${ASIO_CMAKE_ASIO_DEP_DIR} 11 | ) 12 | FetchContent_MakeAvailable(asio) 13 | 14 | add_library(asio INTERFACE) 15 | target_include_directories(asio INTERFACE ${asio_SOURCE_DIR}/asio/include) 16 | target_compile_definitions(asio INTERFACE 17 | ASIO_STANDALONE # don't use Boost 18 | ASIO_NO_DEPRECATED 19 | ) 20 | 21 | if(UNIX) 22 | target_link_libraries(asio INTERFACE pthread) 23 | elseif(WIN32) 24 | # macro see @ https://stackoverflow.com/a/40217291/1746503 25 | macro(get_WIN32_WINNT version) 26 | if (CMAKE_SYSTEM_VERSION) 27 | set(ver ${CMAKE_SYSTEM_VERSION}) 28 | string(REGEX MATCH "^([0-9]+).([0-9])" ver ${ver}) 29 | string(REGEX MATCH "^([0-9]+)" verMajor ${ver}) 30 | # Check for Windows 10, b/c we'll need to convert to hex 'A'. 31 | if ("${verMajor}" MATCHES "10") 32 | set(verMajor "A") 33 | string(REGEX REPLACE "^([0-9]+)" ${verMajor} ver ${ver}) 34 | endif ("${verMajor}" MATCHES "10") 35 | # Remove all remaining '.' characters. 36 | string(REPLACE "." "" ver ${ver}) 37 | # Prepend each digit with a zero. 38 | string(REGEX REPLACE "([0-9A-Z])" "0\\1" ver ${ver}) 39 | set(${version} "0x${ver}") 40 | endif(CMAKE_SYSTEM_VERSION) 41 | endmacro(get_WIN32_WINNT) 42 | 43 | if(NOT DEFINED _WIN32_WINNT) 44 | get_WIN32_WINNT(ver) 45 | set(_WIN32_WINNT ${ver}) 46 | endif() 47 | 48 | message(STATUS "Set _WIN32_WINNET=${_WIN32_WINNT}") 49 | 50 | target_compile_definitions(asio 51 | INTERFACE 52 | _WIN32_WINNT=${_WIN32_WINNT} 53 | WIN32_LEAN_AND_MEAN 54 | ) 55 | endif() 56 | 57 | add_subdirectory(cxxopts) 58 | add_subdirectory(yaml-cpp) 59 | 60 | if(BUILD_TEST) 61 | add_subdirectory(googletest) 62 | endif() 63 | 64 | if(ENABLE_LOG) 65 | add_subdirectory(spdlog) 66 | endif() -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | + after [upgrading cmake version](https://blog.csdn.net/fancyler/article/details/78009812), it may need to flush some cache under project root directory with the command below: 4 | 5 | ```bash 6 | hash -r 7 | ``` 8 | 9 | + when passing a member function as a callable argument, remember that the first parameter of `std::function` should be the class containing that member function and the callable argument should be the *address* of the member function: 10 | 11 | ```c++ 12 | explicit Session(tcp::socket socket, std::function deliverCallback); 13 | 14 | Session s(std::move(socket), &Server::Deliver); 15 | ``` 16 | 17 | What's more, if the member function is declared as `const`, the first parameter of `std::function` should also be `const`. 18 | 19 | + when using `async_read` instead of `async_read_some`, it will block if the current read stream cannot fill the `asio::buffer`. that is to say, if the length of the `asio::buffer` is too large, the `async_read` may block until an EOF. 20 | 21 | + in the implementation of asio's `async_` functions, it seems that you cannot capture a char* in the lambda of callback. instead, a `self` got from `shared_from_this()` should be captured. 22 | 23 | + `bad_weak_ptr` could happen if using `shared_from_this()` on an object which has no `shared_ptr` pointing to it yet. 24 | 25 | + `std::*_pointer_cast` could only be applied to `shared_ptr` (because copy operation of `unique_ptr` is deleted). if you must cast the type of a `unique_ptr`, use below: 26 | 27 | ```c++ 28 | dst.reset(dynamic_cast(src.release())); 29 | ``` 30 | note that `release()` should be used here instead of `get()` because you need to release the ownership of managed object by `src`, otherwise `dst` would gain nothing 31 | 32 | sounds like **move semantic**, aha? 33 | 34 | + when copying data to vector (or something like this), you need to pay more attention: 35 | 36 | ```c++ 37 | std::vector v; 38 | int a[] = {1, 2, 3}; 39 | std::copy(a, a + 3, std::back_inserter(v)); 40 | ``` 41 | 42 | `std::back_inserter()` returns a `std::back_insert_iterator`, which is an output iterator pointing to the **end** of the container and `push_back()` method of the container will be called whenever the iterator is assigned to. 43 | 44 | But if you must copy to the begin of the container, it's better to resize the container in advance to ensure that the size of container is larger than the size of data copied: 45 | 46 | ```c++ 47 | std::vector v; 48 | int a[] = {1, 2, 3}; 49 | v.resize(3); 50 | std::copy(a, a + 3, v.begin()); 51 | ``` 52 | 53 | + note that `size()` of `std::vector` (maybe most stl containers are also applied) return a **unsigned** value: 54 | 55 | ```c++ 56 | std::vector v; 57 | std::cout << v.size() - 1 << std::endl; // 18446744073709551615 58 | ``` 59 | 60 | + `constexpr` does not always have an address. for example, the following code will complain `undefined reference to 'Card::NonWildColors'`. that is because `Card::NonWildColors` doesn't have an address, which cannot be traversed in a range-based loop 61 | 62 | ```c++ 63 | struct Card { 64 | constexpr static std::initializer_list NonWildColors = {/*data*/}; 65 | }; 66 | 67 | for (auto color : Card::NonWildColors) { 68 | // do something 69 | } 70 | ``` 71 | 72 | the right way is to declare as `const static` and define somewhere else: 73 | 74 | ```c++ 75 | struct Card { 76 | const static std::initializer_list NonWildColors; 77 | }; 78 | 79 | const std::initializer_list Card::NonWildTexts = {/*data*/}; 80 | ``` 81 | 82 | *[reference](https://stackoverflow.com/questions/8452952/c-linker-error-with-class-static-constexpr)* 83 | 84 | + when passing a method of a class as callable, usually there are two ways: 85 | 86 | + using `std::bind`: 87 | 88 | ```c++ 89 | class A { 90 | void f() {} 91 | void g() { 92 | some_func(std::bind(&A::f, this)); 93 | } 94 | }; 95 | ``` 96 | 97 | + using lambda: 98 | 99 | ```c++ 100 | class A { 101 | void f() {} 102 | void g() { 103 | some_func([this]() { return f(); }]); 104 | } 105 | }; 106 | ``` 107 | 108 | + when using asio, invocation to `io_context::run()` will block if there is still some async work to complete. *[reference](https://think-async.com/Asio/asio-1.18.0/doc/asio/tutorial/tuttimer2.html)* 109 | 110 | + to install MinGw on Windows, take [this](https://www.tutorialspoint.com/How-to-Install-Cplusplus-Compiler-on-Windows) for a reference. after installation, some necessary binarys should already have been added to *PATH*, run `g++` to have a check. and then, we can use it like `cmake -G "MinGW Makefiles" ..` 111 | 112 | + using Python to run an executable and provide input interactively, take [this](https://stackoverflow.com/questions/32570029/input-to-c-executable-python-subprocess) for a reference. 113 | 114 | + use `firewall-cmd` commands to query and manipulate configuration about firewall to expose service. *[reference](https://www.tecmint.com/fix-no-route-to-host-ssh-error-in-linux/)* -------------------------------------------------------------------------------- /script/build.bat: -------------------------------------------------------------------------------- 1 | cd .. 2 | git submodule update --init 3 | rd /s /q build 4 | md build 5 | cd .\build 6 | cmake .. 7 | cmake --build . -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | git submodule update --init 5 | rm -rf build 6 | mkdir build && cd build 7 | cmake .. 8 | make -------------------------------------------------------------------------------- /script/daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script_dir=$(cd "$(dirname "$0")" >/dev/null 2>&1; pwd -P) 4 | proj_dir=$(dirname $script_dir) 5 | log_dir=$proj_dir"/logs" 6 | 7 | server_path=$proj_dir"/build/src/uno" 8 | bot_path=$proj_dir"/bot/bot.py" 9 | conf_path=$script_dir"/server.conf" 10 | 11 | declare -A player_num_by_port 12 | declare -A bot_num_by_port 13 | 14 | print_log() 15 | { 16 | echo "[$(date '+%Y/%m/%d %H:%M:%S')] $1" 17 | } 18 | 19 | prepare_room() 20 | { 21 | port=$1 22 | totol_player_num=$2 23 | bot_num=$3 24 | log_path=$log_dir"/server-$port.log" 25 | print_log "starting server... [port: $port, player_num: $totol_player_num, bot_num: $bot_num]" 26 | $server_path -l $port -n $totol_player_num --log $log_path >/dev/null 2>&1 & 27 | sleep 0.2 28 | print_log "server started." 29 | for ((i = 0; i < $bot_num; i++)) 30 | do 31 | print_log "starting bot... [port: $port, bot_index: $i]" 32 | python3 $bot_path localhost:$port $totol_player_num bot$i >/dev/null 2>&1 & 33 | print_log "bot started." 34 | done 35 | print_log "server and bot started, room prepared." 36 | print_log "-------------------------------------" 37 | } 38 | 39 | init_all_rooms() 40 | { 41 | for port in "${!player_num_by_port[@]}"; 42 | do 43 | prepare_room $port ${player_num_by_port[$port]} ${bot_num_by_port[$port]} 44 | done 45 | } 46 | 47 | load_config() 48 | { 49 | while read port player_num bot_num 50 | do 51 | player_num_by_port[$port]=$player_num 52 | bot_num_by_port[$port]=$bot_num 53 | done < $conf_path 54 | } 55 | 56 | daemonize() 57 | { 58 | while : 59 | do 60 | sleep 10 61 | for port in "${!player_num_by_port[@]}"; 62 | do 63 | if [ -z $(netstat -nlt | awk '{print $4}' | grep $port) ]; then 64 | print_log "server on port $port crashed, restarting..." 65 | prepare_room $port ${player_num_by_port[$port]} ${bot_num_by_port[$port]} 66 | print_log "server restarted." 67 | fi 68 | # maybe need to check bot num ? 69 | done 70 | print_log "check complete" 71 | done 72 | } 73 | 74 | load_config 75 | init_all_rooms 76 | daemonize 77 | -------------------------------------------------------------------------------- /script/log_analyzer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script_dir=$(cd "$(dirname "$0")" >/dev/null 2>&1; pwd -P) 4 | proj_dir=$(dirname $script_dir) 5 | log_dir=$proj_dir"/logs" 6 | conf_path=$script_dir"/server.conf" 7 | 8 | declare -A restart_num 9 | declare -A start_num 10 | declare -A end_num 11 | 12 | declare -A player_num_by_port 13 | declare -A bot_num_by_port 14 | 15 | bot_num_filter=-1 16 | player_num_filter=-1 17 | days_filter=100 18 | 19 | parse_arg() 20 | { 21 | # -v: show version 22 | # -n x/y: x is bot num, y is total player num 23 | # -d x: just show logs of recent x days 24 | while getopts ":vn:d:" options; do 25 | case "${options}" in 26 | v) 27 | echo "log_analyzer version 1.0" 28 | exit 0 29 | ;; 30 | n) 31 | bot_num_filter=$(echo $OPTARG | cut -d'/' -f1) 32 | player_num_filter=$(echo $OPTARG | cut -d'/' -f2) 33 | if [[ -z $bot_num_filter ]]; then 34 | bot_num_filter=-1 35 | fi 36 | if [[ -z $player_num_filter ]]; then 37 | player_num_filter=-1 38 | fi 39 | ;; 40 | d) 41 | days_filter=$OPTARG 42 | ;; 43 | :) 44 | echo "Error: -${OPTARG} requires an argument." 45 | exit -1 46 | ;; 47 | *) 48 | exit -1 49 | ;; 50 | esac 51 | done 52 | } 53 | 54 | satisfy_n_filter() 55 | { 56 | if [[ $bot_num_filter != -1 && $bot_num_filter != $1 ]]; then 57 | echo 0 58 | elif [[ $player_num_filter != -1 && $player_num_filter != $2 ]]; then 59 | echo 0 60 | else 61 | echo 1 62 | fi 63 | } 64 | 65 | load_config() 66 | { 67 | while read port player_num bot_num 68 | do 69 | if [[ $(satisfy_n_filter $bot_num $player_num) == 1 ]]; then 70 | player_num_by_port[$port]=$player_num 71 | bot_num_by_port[$port]=$bot_num 72 | fi 73 | done < $conf_path 74 | } 75 | 76 | load_hint() 77 | { 78 | for port in "${!player_num_by_port[@]}"; 79 | do 80 | log_path=$log_dir"/server-"$port".log" 81 | while read date _ _ _ _ hint 82 | do 83 | date=$(echo $date | cut -d'[' -f2) 84 | # date is in format of yyyy-mm-dd 85 | seconds_diff=$(( $(date -d 'now' +%s) - $(date -d $date +%s) )) 86 | day_diff=$(( $seconds_diff / (60*60*24) )) 87 | if [[ $day_diff -lt $days_filter ]]; then 88 | if [[ $hint == "spdlog" ]]; then 89 | ((restart_num[$port]+=1)) 90 | elif [[ $hint == "Starts." ]]; then 91 | ((start_num[$port]+=1)) 92 | elif [[ $hint == "Ends." ]]; then 93 | ((end_num[$port]+=1)) 94 | fi 95 | fi 96 | done < $log_path 97 | done 98 | } 99 | 100 | print() 101 | { 102 | echo -e " port\t restart start end" 103 | echo -e "-----------------------------------" 104 | for port in "${!player_num_by_port[@]}"; 105 | do 106 | bot_num=${bot_num_by_port[$port]} 107 | player_num=${player_num_by_port[$port]} 108 | _restart_num=${restart_num[$port]} 109 | _start_num=${start_num[$port]} 110 | _end_num=${end_num[$port]} 111 | echo -e "$port ($bot_num/$player_num)\t$_restart_num\t$_start_num\t $_end_num" 112 | done | 113 | sort 114 | } 115 | 116 | parse_arg $@ 117 | load_config 118 | load_hint 119 | print 120 | -------------------------------------------------------------------------------- /script/server.conf: -------------------------------------------------------------------------------- 1 | 20020 2 1 2 | 20021 2 1 3 | 20022 2 1 4 | 20023 2 1 5 | 20024 2 1 6 | 20025 2 0 7 | 20026 2 0 8 | 20027 2 0 9 | 20028 2 0 10 | 20029 2 0 11 | 20030 3 2 12 | 20031 3 2 13 | 20032 3 2 14 | 20033 3 1 15 | 20034 3 1 16 | 20035 3 1 17 | 20036 3 0 18 | 20037 3 0 19 | 20038 3 0 20 | 20040 4 3 21 | 20041 4 3 22 | 20042 4 3 23 | 20043 4 2 24 | 20044 4 2 25 | 20045 4 2 26 | 20046 4 1 27 | 20047 4 1 28 | 20048 4 1 29 | 20049 4 0 30 | 20050 4 0 31 | 20051 4 0 32 | -------------------------------------------------------------------------------- /script/uno_server_ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script_dir=$(cd "$(dirname "$0")" >/dev/null 2>&1; pwd -P) 4 | daemon_script_path=$script_dir"/daemon.sh" 5 | log_path=$script_dir"/uno.log" 6 | conf_path=$script_dir"/server.conf" 7 | 8 | start_daemon() 9 | { 10 | $daemon_script_path >$log_path 2>&1 & 11 | } 12 | 13 | # target can be daemon or a port 14 | get_target() 15 | { 16 | if [[ $1 == "daemon" ]]; 17 | then 18 | if [ ! -z "$(ps aux | grep daemon.sh | grep -v grep)" ]; then 19 | echo "daemon status: running" 20 | else 21 | echo "daemon status: shutdown" 22 | fi 23 | else 24 | if [ ! -z "$(netstat -nlt | awk '{print $4}' | grep $1)" ]; then 25 | echo "server-$1 status: running" 26 | else 27 | echo "server-$1 status: shutdown" 28 | fi 29 | fi 30 | } 31 | 32 | list_ports() 33 | { 34 | while read port player_num bot_num 35 | do 36 | if [ ! -z "$(netstat -nlt | awk '{print $4}' | grep $port)" ]; then 37 | echo "server-$port status: running" 38 | else 39 | echo "server-$port status: shutdown" 40 | fi 41 | done < $conf_path 42 | } 43 | 44 | stop_target() 45 | { 46 | if [[ $1 == "daemon" ]]; then 47 | kill $(ps aux | grep daemon.sh | grep -v grep | awk '{print $2}') 48 | echo "daemon stopped." 49 | elif [[ $1 == "all" ]]; then 50 | while read port player_num bot_num 51 | do 52 | stop_target $port 53 | done < $conf_path 54 | else 55 | kill $(ps aux | grep -e "-l $1" | grep -v grep | awk '{print $2}') 56 | echo "server-$1 stopped." 57 | fi 58 | } 59 | 60 | # mode can be [start, get, list, stop] 61 | mode=$1 62 | 63 | # target can be [daemon, $port] 64 | target=$2 65 | 66 | if [[ $mode == "start" ]]; then 67 | start_daemon 68 | elif [[ $mode == "get" ]]; then 69 | get_target $target 70 | elif [[ $mode == "list" ]]; then 71 | list_ports 72 | elif [[ $mode == "stop" ]]; then 73 | stop_target $target 74 | elif [[ $mode == "-v" ]]; then 75 | echo "uno_server_ctl version 1.0" 76 | fi -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | aux_source_directory(. APP_DIR) 2 | aux_source_directory(common APP_DIR) 3 | aux_source_directory(game APP_DIR) 4 | aux_source_directory(network APP_DIR) 5 | aux_source_directory(ui APP_DIR) 6 | 7 | add_executable(uno ${APP_DIR}) 8 | target_link_libraries(uno PRIVATE asio) 9 | target_link_libraries(uno PRIVATE cxxopts) 10 | target_link_libraries(uno PRIVATE yaml-cpp) 11 | 12 | if(ENABLE_LOG) 13 | target_link_libraries(uno PRIVATE spdlog::spdlog) 14 | endif() -------------------------------------------------------------------------------- /src/common/common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace UNO { namespace Common { 7 | 8 | class Common { 9 | public: 10 | static int mPlayerNum; 11 | static int mTimeoutPerTurn; 12 | static int mHandCardsNumPerRow; 13 | // color escape 14 | static std::string mRedEscape; 15 | static std::string mYellowEscape; 16 | static std::string mGreenEscape; 17 | static std::string mBlueEscape; 18 | const static std::map mEscapeMap; 19 | }; 20 | }} -------------------------------------------------------------------------------- /src/common/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | namespace UNO { namespace Common { 4 | 5 | // define static common variables here 6 | int Common::mPlayerNum; 7 | int Common::mTimeoutPerTurn; 8 | int Common::mHandCardsNumPerRow; 9 | std::string Common::mRedEscape; 10 | std::string Common::mYellowEscape; 11 | std::string Common::mGreenEscape; 12 | std::string Common::mBlueEscape; 13 | const std::map Common::mEscapeMap = { 14 | {"red", "\033[31m"}, 15 | {"yellow", "\033[33m"}, 16 | {"green", "\033[32m"}, 17 | {"blue", "\033[34m"}, 18 | {"cyan", "\033[36m"}, 19 | {"brightRed", "\033[91m"}, 20 | {"brightYellow", "\033[93m"}, 21 | {"brightGreen", "\033[92m"}, 22 | {"brightBlue", "\033[94m"}, 23 | {"brightCyan", "\033[96m"} 24 | }; 25 | 26 | const std::string Config::CMD_OPT_SHORT_LISTEN = "l"; 27 | const std::string Config::CMD_OPT_LONG_LISTEN = "listen"; 28 | const std::string Config::CMD_OPT_BOTH_LISTEN = CMD_OPT_SHORT_LISTEN + ", " + CMD_OPT_LONG_LISTEN; 29 | const std::string Config::CMD_OPT_SHORT_CONNECT = "c"; 30 | const std::string Config::CMD_OPT_LONG_CONNECT = "connect"; 31 | const std::string Config::CMD_OPT_BOTH_CONNECT = CMD_OPT_SHORT_CONNECT + ", " + CMD_OPT_LONG_CONNECT; 32 | const std::string Config::CMD_OPT_SHORT_USERNAME = "u"; 33 | const std::string Config::CMD_OPT_LONG_USERNAME = "username"; 34 | const std::string Config::CMD_OPT_BOTH_USERNAME = CMD_OPT_SHORT_USERNAME + ", " + CMD_OPT_LONG_USERNAME; 35 | const std::string Config::CMD_OPT_SHORT_PLAYERS = "n"; 36 | const std::string Config::CMD_OPT_LONG_PLAYERS = "players"; 37 | const std::string Config::CMD_OPT_BOTH_PLAYERS = CMD_OPT_SHORT_PLAYERS + ", " + CMD_OPT_LONG_PLAYERS; 38 | const std::string Config::CMD_OPT_SHORT_CFGFILE = "f"; 39 | const std::string Config::CMD_OPT_LONG_CFGFILE = "file"; 40 | const std::string Config::CMD_OPT_BOTH_CFGFILE = CMD_OPT_SHORT_CFGFILE + ", " + CMD_OPT_LONG_CFGFILE; 41 | const std::string Config::CMD_OPT_LONG_LOGFILE = "log"; 42 | const std::string Config::CMD_OPT_SHORT_VERSION = "v"; 43 | const std::string Config::CMD_OPT_LONG_VERSION = "version"; 44 | const std::string Config::CMD_OPT_BOTH_VERSION = CMD_OPT_SHORT_VERSION + ", " + CMD_OPT_LONG_VERSION; 45 | const std::string Config::CMD_OPT_SHORT_HELP = "h"; 46 | const std::string Config::CMD_OPT_LONG_HELP = "help"; 47 | const std::string Config::CMD_OPT_BOTH_HELP = CMD_OPT_SHORT_HELP + ", " + CMD_OPT_LONG_HELP; 48 | const std::string Config::FILE_OPT_SERVER = "server"; 49 | const std::string Config::FILE_OPT_CLIENT = "client"; 50 | const std::string Config::FILE_OPT_LISTEN = "listenOn"; 51 | const std::string Config::FILE_OPT_CONNECT = "connectTo"; 52 | const std::string Config::FILE_OPT_USERNAME = "username"; 53 | const std::string Config::FILE_OPT_PLAYERS = "playerNum"; 54 | const std::string Config::FILE_OPT_RED = "red"; 55 | const std::string Config::FILE_OPT_YELLOW = "yellow"; 56 | const std::string Config::FILE_OPT_GREEN = "green"; 57 | const std::string Config::FILE_OPT_BLUE = "blue"; 58 | 59 | Config::Config(int argc, const char **argv) 60 | { 61 | mOptions = std::make_unique("uno", "UNO - uno card game"); 62 | mOptions->add_options() 63 | (CMD_OPT_BOTH_LISTEN, "the port number that server will listen on", cxxopts::value()) 64 | (CMD_OPT_BOTH_CONNECT, "the endpoint that client (player) will connect to", cxxopts::value()) 65 | (CMD_OPT_BOTH_USERNAME, "the username of the player", cxxopts::value()) 66 | (CMD_OPT_BOTH_PLAYERS, "the number of players", cxxopts::value()) 67 | (CMD_OPT_BOTH_CFGFILE, "the path of config file", cxxopts::value()) 68 | (CMD_OPT_LONG_LOGFILE, "the path of log file", cxxopts::value()) 69 | (CMD_OPT_BOTH_VERSION, "show version of application", cxxopts::value()) 70 | (CMD_OPT_BOTH_HELP, "show help info", cxxopts::value()); 71 | 72 | try { 73 | mCmdlineOpts = std::make_unique(mOptions->parse(argc, argv)); 74 | } 75 | catch (std::exception &e) { 76 | std::cout << mOptions->help() << std::endl; 77 | std::cout << e.what() << std::endl; 78 | std::exit(-1); 79 | } 80 | std::string configFile; 81 | if (mCmdlineOpts->count(CMD_OPT_LONG_CFGFILE)) { 82 | auto configFile = (*mCmdlineOpts)[CMD_OPT_LONG_CFGFILE].as(); 83 | auto rootNode = YAML::LoadFile(configFile); 84 | if (rootNode[FILE_OPT_SERVER].IsDefined()) { 85 | mServerNode = std::make_unique(rootNode[FILE_OPT_SERVER]); 86 | } 87 | if (rootNode[FILE_OPT_CLIENT].IsDefined()) { 88 | mClientNode = std::make_unique(rootNode[FILE_OPT_CLIENT]); 89 | } 90 | } 91 | } 92 | 93 | std::unique_ptr Config::Parse() 94 | { 95 | HandleImmediateConfig(); 96 | // options from command line takes precedence over ones from config file 97 | ParseFileOpts(); 98 | try { 99 | ParseCmdlineOpts(); 100 | } 101 | catch (std::exception &e) { 102 | std::cout << mOptions->help() << std::endl; 103 | std::cout << e.what() << std::endl; 104 | std::exit(-1); 105 | } 106 | 107 | // handle common config here 108 | SetUpCommonConfig(); 109 | 110 | // the main function will handle game config 111 | return std::move(mGameConfigInfo); 112 | } 113 | 114 | void Config::HandleImmediateConfig() 115 | { 116 | if (mCmdlineOpts->count(CMD_OPT_LONG_HELP)) { 117 | std::cout << mOptions->help() << std::endl; 118 | std::exit(0); 119 | } 120 | if (mCmdlineOpts->count(CMD_OPT_LONG_VERSION)) { 121 | std::cout << "uno version 1.0" << std::endl; 122 | std::exit(0); 123 | } 124 | } 125 | 126 | void Config::ParseFileOpts() 127 | { 128 | // parse server node 129 | if (mServerNode && mCmdlineOpts->count(CMD_OPT_LONG_LISTEN)) { 130 | if ((*mServerNode)[FILE_OPT_PLAYERS].IsDefined()) { 131 | mCommonConfigInfo->mPlayerNum = (*mServerNode)[FILE_OPT_PLAYERS].as(); 132 | } 133 | } 134 | 135 | // parse client node 136 | if (mClientNode && mCmdlineOpts->count(CMD_OPT_LONG_CONNECT)) { 137 | if ((*mClientNode)[FILE_OPT_USERNAME].IsDefined()) { 138 | mGameConfigInfo->mUsername = (*mClientNode)[FILE_OPT_USERNAME].as(); 139 | } 140 | if ((*mClientNode)[FILE_OPT_RED].IsDefined()) { 141 | mCommonConfigInfo->mRedEscape = (*mClientNode)[FILE_OPT_RED].as(); 142 | } 143 | if ((*mClientNode)[FILE_OPT_YELLOW].IsDefined()) { 144 | mCommonConfigInfo->mYellowEscape = (*mClientNode)[FILE_OPT_YELLOW].as(); 145 | } 146 | if ((*mClientNode)[FILE_OPT_GREEN].IsDefined()) { 147 | mCommonConfigInfo->mGreenEscape = (*mClientNode)[FILE_OPT_GREEN].as(); 148 | } 149 | if ((*mClientNode)[FILE_OPT_BLUE].IsDefined()) { 150 | mCommonConfigInfo->mBlueEscape = (*mClientNode)[FILE_OPT_BLUE].as(); 151 | } 152 | } 153 | } 154 | 155 | void Config::ParseCmdlineOpts() 156 | { 157 | // check options 158 | if (mCmdlineOpts->count(CMD_OPT_LONG_LISTEN) && mCmdlineOpts->count(CMD_OPT_LONG_CONNECT)) { 159 | throw std::runtime_error("cannot specify both -l and -c options at the same time"); 160 | } 161 | if (!mCmdlineOpts->count(CMD_OPT_LONG_LISTEN) && !mCmdlineOpts->count(CMD_OPT_LONG_CONNECT)) { 162 | throw std::runtime_error("must specify either -l or -c option"); 163 | } 164 | if (mCmdlineOpts->count(CMD_OPT_LONG_CONNECT) && !mCmdlineOpts->count(CMD_OPT_LONG_USERNAME) 165 | && (!mClientNode || !(*mClientNode)[FILE_OPT_USERNAME].IsDefined())) { 166 | throw std::runtime_error("must specify -u option if -c option is specified"); 167 | } 168 | if (mCmdlineOpts->count(CMD_OPT_LONG_CONNECT) && mCmdlineOpts->count(CMD_OPT_LONG_PLAYERS)) { 169 | throw std::runtime_error("only server side can specify -n option"); 170 | } 171 | 172 | // -l 173 | if (mCmdlineOpts->count(CMD_OPT_LONG_LISTEN)) { 174 | mGameConfigInfo->mIsServer = true; 175 | mGameConfigInfo->mPort = (*mCmdlineOpts)[CMD_OPT_LONG_LISTEN].as(); 176 | } 177 | 178 | // -c 179 | if (mCmdlineOpts->count(CMD_OPT_LONG_CONNECT)) { 180 | mGameConfigInfo->mIsServer = false; 181 | std::string endpoint = (*mCmdlineOpts)[CMD_OPT_LONG_CONNECT].as(); 182 | int pos = endpoint.find(":"); 183 | mGameConfigInfo->mHost = endpoint.substr(0, pos); 184 | mGameConfigInfo->mPort = endpoint.substr(pos + 1); 185 | } 186 | 187 | // -u 188 | if (mCmdlineOpts->count(CMD_OPT_LONG_USERNAME)) { 189 | mGameConfigInfo->mUsername = (*mCmdlineOpts)[CMD_OPT_LONG_USERNAME].as(); 190 | } 191 | 192 | // -n 193 | if (mCmdlineOpts->count(CMD_OPT_LONG_PLAYERS)) { 194 | mCommonConfigInfo->mPlayerNum = (*mCmdlineOpts)[CMD_OPT_LONG_PLAYERS].as(); 195 | } 196 | 197 | // --log 198 | if (mCmdlineOpts->count(CMD_OPT_LONG_LOGFILE)) { 199 | mGameConfigInfo->mLogPath = (*mCmdlineOpts)[CMD_OPT_LONG_LOGFILE].as(); 200 | } 201 | } 202 | 203 | void Config::SetUpCommonConfig() 204 | { 205 | Common::mPlayerNum = mCommonConfigInfo->mPlayerNum.value_or(3); 206 | Common::mTimeoutPerTurn = 15; 207 | Common::mHandCardsNumPerRow = 8; 208 | 209 | auto redIter = Common::mEscapeMap.find(mCommonConfigInfo->mRedEscape.value_or("red")); 210 | if (redIter == Common::mEscapeMap.end()) { 211 | Common::mRedEscape = Common::mEscapeMap.at("red"); 212 | } 213 | else { 214 | Common::mRedEscape = redIter->second; 215 | } 216 | 217 | auto yellowIter = Common::mEscapeMap.find(mCommonConfigInfo->mYellowEscape.value_or("yellow")); 218 | if (yellowIter == Common::mEscapeMap.end()) { 219 | Common::mYellowEscape = Common::mEscapeMap.at("yellow"); 220 | } 221 | else { 222 | Common::mYellowEscape = yellowIter->second; 223 | } 224 | 225 | auto greenIter = Common::mEscapeMap.find(mCommonConfigInfo->mGreenEscape.value_or("green")); 226 | if (greenIter == Common::mEscapeMap.end()) { 227 | Common::mGreenEscape = Common::mEscapeMap.at("green"); 228 | } 229 | else { 230 | Common::mGreenEscape = greenIter->second; 231 | } 232 | 233 | auto blueIter = Common::mEscapeMap.find(mCommonConfigInfo->mBlueEscape.value_or("blue")); 234 | if (blueIter == Common::mEscapeMap.end()) { 235 | Common::mBlueEscape = Common::mEscapeMap.at("blue"); 236 | } 237 | else { 238 | Common::mBlueEscape = blueIter->second; 239 | } 240 | } 241 | }} -------------------------------------------------------------------------------- /src/common/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "common.h" 8 | 9 | namespace UNO { namespace Common { 10 | 11 | /** 12 | * config info that is used by \c GameBoard and \c Player, 13 | * which is outside the \c Config class 14 | */ 15 | struct GameConfigInfo { 16 | bool mIsServer; 17 | std::string mHost; 18 | std::string mPort; 19 | std::string mUsername; 20 | std::string mLogPath{"logs/uno.log"}; 21 | }; 22 | 23 | /** 24 | * config info that is used by Config::SetUpCommonConfig, 25 | * which is inside the \c Config class 26 | */ 27 | struct CommonConfigInfo { 28 | std::optional mPlayerNum; 29 | std::optional mRedEscape; 30 | std::optional mYellowEscape; 31 | std::optional mGreenEscape; 32 | std::optional mBlueEscape; 33 | }; 34 | 35 | class Config { 36 | public: 37 | Config(int argc, const char **argv); 38 | 39 | /** 40 | * Parse the config info from both yaml file and command line. 41 | * \return config info that will be used outside the class 42 | */ 43 | std::unique_ptr Parse(); 44 | 45 | private: 46 | /** 47 | * Handle immediate config in a short cut, like -v and -h. 48 | */ 49 | void HandleImmediateConfig(); 50 | /** 51 | * Parse the config info from yaml file. 52 | */ 53 | void ParseFileOpts(); 54 | 55 | /** 56 | * Parse the config info from command line. 57 | */ 58 | void ParseCmdlineOpts(); 59 | 60 | /** 61 | * Initialize variables in \c Common::Common with config info just parsed 62 | * and those variables are what will be actually used in the game. 63 | */ 64 | void SetUpCommonConfig(); 65 | 66 | private: 67 | std::unique_ptr mOptions; 68 | std::unique_ptr mCmdlineOpts; 69 | std::unique_ptr mServerNode; 70 | std::unique_ptr mClientNode; 71 | 72 | std::unique_ptr mGameConfigInfo{std::make_unique()}; 73 | std::unique_ptr mCommonConfigInfo{std::make_unique()}; 74 | 75 | private: 76 | const static std::string CMD_OPT_SHORT_LISTEN; 77 | const static std::string CMD_OPT_LONG_LISTEN; 78 | const static std::string CMD_OPT_BOTH_LISTEN; 79 | const static std::string CMD_OPT_SHORT_CONNECT; 80 | const static std::string CMD_OPT_LONG_CONNECT; 81 | const static std::string CMD_OPT_BOTH_CONNECT; 82 | const static std::string CMD_OPT_SHORT_USERNAME; 83 | const static std::string CMD_OPT_LONG_USERNAME; 84 | const static std::string CMD_OPT_BOTH_USERNAME; 85 | const static std::string CMD_OPT_SHORT_PLAYERS; 86 | const static std::string CMD_OPT_LONG_PLAYERS; 87 | const static std::string CMD_OPT_BOTH_PLAYERS; 88 | const static std::string CMD_OPT_SHORT_CFGFILE; 89 | const static std::string CMD_OPT_LONG_CFGFILE; 90 | const static std::string CMD_OPT_BOTH_CFGFILE; 91 | const static std::string CMD_OPT_LONG_LOGFILE; 92 | const static std::string CMD_OPT_SHORT_VERSION; 93 | const static std::string CMD_OPT_LONG_VERSION; 94 | const static std::string CMD_OPT_BOTH_VERSION; 95 | const static std::string CMD_OPT_SHORT_HELP; 96 | const static std::string CMD_OPT_LONG_HELP; 97 | const static std::string CMD_OPT_BOTH_HELP; 98 | 99 | const static std::string FILE_OPT_SERVER; 100 | const static std::string FILE_OPT_CLIENT; 101 | const static std::string FILE_OPT_LISTEN; 102 | const static std::string FILE_OPT_CONNECT; 103 | const static std::string FILE_OPT_USERNAME; 104 | const static std::string FILE_OPT_PLAYERS; 105 | const static std::string FILE_OPT_RED; 106 | const static std::string FILE_OPT_YELLOW; 107 | const static std::string FILE_OPT_GREEN; 108 | const static std::string FILE_OPT_BLUE; 109 | }; 110 | }} -------------------------------------------------------------------------------- /src/common/terminal.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #include 3 | #endif 4 | #include "terminal.h" 5 | 6 | namespace UNO { namespace Common { 7 | 8 | #if defined(__unix__) || defined(__APPLE__) 9 | Terminal::Terminal() 10 | { 11 | /// XXX: what if throwing an exception 12 | // save the old attr 13 | tcgetattr(STDIN_FILENO, &mOldAttr); 14 | } 15 | 16 | void Terminal::GetNewAttr() 17 | { 18 | tcgetattr(STDIN_FILENO, &mNewAttr); 19 | } 20 | 21 | void Terminal::SetModeAutoFlush() 22 | { 23 | GetNewAttr(); 24 | mNewAttr.c_lflag &= ~ICANON; 25 | mNewAttr.c_lflag &= ~ECHO; 26 | ApplyNewAttr(); 27 | } 28 | 29 | void Terminal::SetModeNoEcho() 30 | { 31 | GetNewAttr(); 32 | mNewAttr.c_lflag &= ~ECHO; 33 | ApplyNewAttr(); 34 | } 35 | 36 | void Terminal::ApplyNewAttr() 37 | { 38 | tcsetattr(STDIN_FILENO, TCSANOW, &mNewAttr); 39 | } 40 | 41 | void Terminal::Recover() 42 | { 43 | tcsetattr(STDIN_FILENO, TCSANOW, &mOldAttr); 44 | } 45 | 46 | Terminal::~Terminal() 47 | { 48 | Recover(); 49 | } 50 | #endif 51 | 52 | void Terminal::ClearStdInBuffer() 53 | { 54 | #if defined(__unix__) || defined(__APPLE__) 55 | tcflush(STDIN_FILENO, TCIFLUSH); 56 | #elif defined(_WIN32) 57 | while (true) { 58 | auto ret = _kbhit(); 59 | if (ret != 0) { 60 | _getch(); 61 | } 62 | else { 63 | break; 64 | } 65 | } 66 | #endif 67 | } 68 | 69 | }} -------------------------------------------------------------------------------- /src/common/terminal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(__unix__) || defined(__APPLE__) 4 | #include 5 | #include 6 | #endif 7 | 8 | namespace UNO { namespace Common { 9 | 10 | class Terminal { 11 | public: 12 | #if defined(__unix__) || defined(__APPLE__) 13 | /** 14 | * Save the old terminal mode. 15 | */ 16 | Terminal(); 17 | 18 | /** 19 | * Recover the old terminal mode. RAII. 20 | */ 21 | ~Terminal(); 22 | 23 | /** 24 | * Change the terminal mode so that input will auto flush 25 | * (i.e. no longer need a '\n') 26 | */ 27 | void SetModeAutoFlush(); 28 | 29 | /** 30 | * Change the terminal mode so that input will not echo in the console. 31 | */ 32 | void SetModeNoEcho(); 33 | 34 | /** 35 | * Recover the old terminal explicitly. 36 | */ 37 | void Recover(); 38 | #endif 39 | 40 | /** 41 | * Clear those chars that have been inputted but not consumed yet in the input buffer. 42 | */ 43 | static void ClearStdInBuffer(); 44 | 45 | #if defined(__unix__) || defined(__APPLE__) 46 | private: 47 | void GetNewAttr(); 48 | 49 | void ApplyNewAttr(); 50 | 51 | private: 52 | struct termios mNewAttr; 53 | struct termios mOldAttr; 54 | #endif 55 | }; 56 | }} -------------------------------------------------------------------------------- /src/common/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #if defined(__unix__) || defined(__APPLE__) 4 | #include 5 | #elif defined(_WIN32) 6 | #include 7 | #include 8 | #endif 9 | 10 | #include "util.h" 11 | 12 | namespace UNO { namespace Common { 13 | 14 | int Util::Wrap(int numToWrap, int range) 15 | { 16 | int ret = numToWrap % range; 17 | if (ret < 0) { 18 | ret += range; 19 | } 20 | return ret; 21 | } 22 | 23 | int Util::WrapWithPlayerNum(int numToWrap) 24 | { 25 | return Wrap(numToWrap, Common::mPlayerNum); 26 | } 27 | 28 | int Util::GetSegmentNum(int handcardNum) { return (handcardNum - 1) / Common::mHandCardsNumPerRow + 1; } 29 | 30 | int Util::GetSegmentIndex(int handcardIndex) { return handcardIndex / Common::mHandCardsNumPerRow; } 31 | 32 | int Util::GetIndexInSegment(int handcardIndex) { return handcardIndex % Common::mHandCardsNumPerRow; } 33 | 34 | char Util::GetCharWithTimeout(int milliseconds, bool autoFlush) 35 | { 36 | #if defined(__unix__) || defined(__APPLE__) 37 | std::unique_ptr terminal; 38 | if (autoFlush) { 39 | terminal.reset(new Terminal()); 40 | terminal->SetModeAutoFlush(); 41 | } 42 | 43 | struct pollfd pfd = { STDIN_FILENO, POLLIN, 0 }; 44 | int ret = poll(&pfd, 1, milliseconds); 45 | 46 | if (ret == 0) { 47 | throw std::runtime_error("timeout"); 48 | } 49 | else if (ret == 1) { 50 | char c = getchar(); 51 | return c; 52 | } 53 | #elif defined(_WIN32) 54 | FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)); 55 | auto ret = WaitForSingleObject(GetStdHandle(STD_INPUT_HANDLE), milliseconds); 56 | 57 | if (ret == WAIT_TIMEOUT) { 58 | throw std::runtime_error("timeout"); 59 | } 60 | else if (ret == WAIT_OBJECT_0) { 61 | char c = _getch(); 62 | return c; 63 | } 64 | #endif 65 | return 0; 66 | } 67 | 68 | void Util::HideTerminalCursor() 69 | { 70 | #if defined(_WIN32) 71 | HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); 72 | CONSOLE_CURSOR_INFO cinfo; 73 | cinfo.bVisible = 0; 74 | cinfo.dwSize = 1; 75 | SetConsoleCursorInfo(hOutput, &cinfo); 76 | #elif defined(__unix__) || defined(__APPLE__) 77 | // the hidden terminal cursor won't recover automatically, 78 | // so it's not a good choice to do that on Linux 79 | // std::cout << "\033[?25l" << std::endl; 80 | #endif 81 | } 82 | 83 | }} -------------------------------------------------------------------------------- /src/common/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #ifdef ENABLE_LOG 6 | #include 7 | #endif 8 | 9 | #include "common.h" 10 | #include "terminal.h" 11 | 12 | namespace UNO { 13 | 14 | namespace Network { 15 | 16 | class IServer; 17 | 18 | class IClient; 19 | 20 | } 21 | 22 | namespace Common { 23 | 24 | class Util { 25 | public: 26 | static int Wrap(int numToWrap, int range); 27 | 28 | static int WrapWithPlayerNum(int numToWrap); 29 | 30 | /** 31 | * Consume a char with a timeout, if it's exceeded, an exception will be thrown 32 | * \param milliseconds: timeout in milliseconds 33 | * \param autoFlush: if true, the inputted char will get consumed regardless of '\n' 34 | * \return the consumed char 35 | */ 36 | static char GetCharWithTimeout(int milliseconds, bool autoFlush); 37 | 38 | /** 39 | * Get the number of segment, given the number of cards in hand. 40 | */ 41 | static int GetSegmentNum(int handcardNum); 42 | 43 | /** 44 | * Get the index of segment that a card in hand belongs to. 45 | */ 46 | static int GetSegmentIndex(int handcardIndex); 47 | 48 | /** 49 | * Get the index in a segment, given the index of the card in hand. 50 | */ 51 | static int GetIndexInSegment(int handcardIndex); 52 | 53 | /** 54 | * Hide the terminal cursor for better player experience, 55 | * especially when clean screen without using cls 56 | */ 57 | static void HideTerminalCursor(); 58 | 59 | /** 60 | * Dynamically cast a unique_ptr to one with another type. 61 | */ 62 | template 63 | static std::unique_ptr DynamicCast(SrcInfoUp &&srcInfo) { 64 | return std::unique_ptr(dynamic_cast(srcInfo.release())); 65 | } 66 | 67 | /** 68 | * Helper of ReceiveInfo for brief code. 69 | */ 70 | template 71 | static std::unique_ptr Receive(std::shared_ptr peer, Args... args) { 72 | if constexpr (std::is_same_v) { 73 | static_assert(sizeof...(args) == 1); 74 | return ReceiveHelper(peer, args...); 75 | } 76 | else if constexpr (std::is_same_v) { 77 | static_assert(sizeof...(args) == 0); 78 | return DynamicCast(peer->ReceiveInfo(&typeid(InfoT))); 79 | } 80 | else { 81 | assert(0); 82 | } 83 | } 84 | 85 | /** 86 | * Helper of DelieveInfo for brief code. 87 | */ 88 | template 89 | static void Deliver(std::shared_ptr peer, Args... args) { 90 | if constexpr (std::is_same_v) { 91 | DeliverHelper(peer, args...); 92 | } 93 | else if constexpr (std::is_same_v) { 94 | peer->DeliverInfo(&typeid(InfoT), InfoT{args...}); 95 | } 96 | else { 97 | assert(0); 98 | } 99 | } 100 | 101 | private: 102 | template 103 | static std::unique_ptr ReceiveHelper(std::shared_ptr server, int index) { 104 | return DynamicCast(server->ReceiveInfo(&typeid(InfoT), index)); 105 | } 106 | 107 | template 108 | static void DeliverHelper(std::shared_ptr server, int index, Args... args) { 109 | server->DeliverInfo(&typeid(InfoT), index, InfoT{args...}); 110 | } 111 | }; 112 | }} -------------------------------------------------------------------------------- /src/game/cards.cpp: -------------------------------------------------------------------------------- 1 | #include "cards.h" 2 | 3 | namespace UNO { namespace Game { 4 | 5 | const std::set CardSet::NonWildColors = 6 | { CardColor::RED, CardColor::YELLOW, CardColor::GREEN, CardColor::BLUE }; 7 | 8 | const std::set CardSet::NonWildTexts = 9 | { CardText::ZERO, CardText::ONE, CardText::TWO, CardText::THREE, CardText::FOUR, 10 | CardText::FIVE, CardText::SIX, CardText::SEVEN, CardText::EIGHT, CardText::NINE, 11 | CardText::SKIP, CardText::REVERSE, CardText::DRAW_TWO }; 12 | 13 | const std::set CardSet::DrawTexts = { CardText::DRAW_TWO, CardText::DRAW_FOUR }; 14 | 15 | HandCards::HandCards(const std::array &cards) 16 | { 17 | for (auto card : cards) { 18 | mCards.emplace(card); 19 | } 20 | } 21 | 22 | void HandCards::Draw(const std::vector &cards) 23 | { 24 | std::for_each(cards.begin(), cards.end(), [this](const Card &card) { 25 | mCards.emplace(card); 26 | }); 27 | } 28 | 29 | bool HandCards::CanBePlayedAfter(int index, Card lastPlayedCard) 30 | { 31 | assert(index < mCards.size()); 32 | Card cardToPlay = At(index); 33 | bool isUno = (mCards.size() == 1); 34 | return cardToPlay.CanBePlayedAfter(lastPlayedCard, isUno); 35 | } 36 | 37 | void HandCards::Erase(int index) 38 | { 39 | mCards.erase(std::next(mCards.begin(), index)); 40 | } 41 | 42 | int HandCards::GetIndex(Card card) const 43 | { 44 | auto it = mCards.find(card); 45 | assert(it != mCards.end()); 46 | return std::distance(mCards.begin(), it); 47 | } 48 | 49 | std::multiset::iterator HandCards::IteratorAt(int index) const { 50 | auto it = std::begin(mCards); 51 | std::advance(it, index); 52 | return it; 53 | } 54 | 55 | int HandCards::GetIndexOfNewlyDrawn(const HandCards &handcardsBeforeDraw) const 56 | { 57 | assert(Number() == handcardsBeforeDraw.Number() + 1); 58 | for (int i = 0; i < handcardsBeforeDraw.Number(); i++) { 59 | if (At(i) != handcardsBeforeDraw.At(i)) { 60 | return i; 61 | } 62 | } 63 | return handcardsBeforeDraw.Number(); 64 | } 65 | 66 | bool Card::CanBePlayedAfter(Card lastPlayedCard, bool isUno) 67 | { 68 | std::set specialTexts{CardText::SKIP, CardText::REVERSE, 69 | CardText::DRAW_TWO, CardText::WILD, CardText::DRAW_FOUR}; 70 | 71 | // special cards can not be played as the last one 72 | if (isUno && specialTexts.count(mText)) { 73 | return false; 74 | } 75 | 76 | // if the last played card is skip, you can only play a skip 77 | if (lastPlayedCard.mText == CardText::SKIP) { 78 | return mText == CardText::SKIP; 79 | } 80 | 81 | // if the last played card is draw two, you can only play a draw two or draw four 82 | if (lastPlayedCard.mText == CardText::DRAW_TWO) { 83 | return (mText == CardText::DRAW_TWO || mText == CardText::DRAW_FOUR); 84 | } 85 | 86 | // if the last played card is draw four, you can only play a draw four 87 | if (lastPlayedCard.mText == CardText::DRAW_FOUR) { 88 | return mText == CardText::DRAW_FOUR; 89 | } 90 | 91 | // wild card can always be played except above conditions 92 | if (mColor == CardColor::BLACK) { 93 | return true; 94 | } 95 | 96 | // if not wild card, only cards with the same num or color can be played 97 | return (mColor == lastPlayedCard.mColor || mText == lastPlayedCard.mText); 98 | } 99 | 100 | void Deck::Init() 101 | { 102 | this->Clear(); 103 | mDiscardPile.Clear(); 104 | for (auto color : CardSet::NonWildColors) { 105 | for (auto text : CardSet::NonWildTexts) { 106 | PushFront(color, text); 107 | if (text != CardText::ZERO) { 108 | // in UNO, there is only one zero for each color 109 | // and two cards for other text (except wild and wild draw four) 110 | PushFront(color, text); 111 | } 112 | } 113 | } 114 | 115 | for (int i = 0; i < 4; i++) { 116 | // there are four `wild` and `wild draw four` each 117 | PushFront(CardColor::BLACK, CardText::WILD); 118 | PushFront(CardColor::BLACK, CardText::DRAW_FOUR); 119 | } 120 | 121 | Shuffle(); 122 | } 123 | 124 | std::vector> Deck::DealInitHandCards(int playerNum) 125 | { 126 | std::vector> initHandCards(playerNum); 127 | for (int card = 0; card < 7; card++) { 128 | for (int player = 0; player < playerNum; player++) { 129 | initHandCards[player][card] = Draw(); 130 | } 131 | } 132 | return initHandCards; 133 | } 134 | 135 | Card Deck::Draw() 136 | { 137 | if (Empty()) { 138 | Swap(mDiscardPile); 139 | Shuffle(); 140 | } 141 | return PopFront(); 142 | } 143 | 144 | std::vector Deck::Draw(int number) 145 | { 146 | std::vector cards(number); 147 | std::generate(cards.begin(), cards.end(), [this]() { return Draw(); }); 148 | return cards; 149 | } 150 | 151 | std::ostream& operator<<(std::ostream& os, const HandCards& handCards) 152 | { 153 | os << "Your hand cards are: [" << handCards.ToString() << "]"; 154 | return os; 155 | } 156 | 157 | Card::Card(const char *str) 158 | { 159 | switch (*str) { 160 | case 'R': mColor = CardColor::RED; str++; break; 161 | case 'Y': mColor = CardColor::YELLOW; str++; break; 162 | case 'G': mColor = CardColor::GREEN; str++; break; 163 | case 'B': mColor = CardColor::BLUE; str++; break; 164 | default: mColor = CardColor::BLACK; 165 | } 166 | 167 | switch (*str) { 168 | case '0': mText = CardText::ZERO; break; 169 | case '1': mText = CardText::ONE; break; 170 | case '2': mText = CardText::TWO; break; 171 | case '3': mText = CardText::THREE; break; 172 | case '4': mText = CardText::FOUR; break; 173 | case '5': mText = CardText::FIVE; break; 174 | case '6': mText = CardText::SIX; break; 175 | case '7': mText = CardText::SEVEN; break; 176 | case '8': mText = CardText::EIGHT; break; 177 | case '9': mText = CardText::NINE; break; 178 | case 'S': mText = CardText::SKIP; break; 179 | case 'R': mText = CardText::REVERSE; break; 180 | case 'W': mText = CardText::WILD; break; 181 | case '+': mText = (*(str + 1) == '2') ? 182 | CardText::DRAW_TWO : CardText::DRAW_FOUR; 183 | break; 184 | case '\0': mText = CardText::EMPTY; break; 185 | default: assert(0); 186 | } 187 | } 188 | 189 | std::string Card::ToString() const 190 | { 191 | std::string color; 192 | std::string text; 193 | 194 | switch (mColor) { 195 | case CardColor::RED: color = "R"; break; 196 | case CardColor::YELLOW: color = "Y"; break; 197 | case CardColor::GREEN: color = "G"; break; 198 | case CardColor::BLUE: color = "B"; break; 199 | case CardColor::BLACK: color = ""; break; 200 | default: assert(0); 201 | } 202 | 203 | switch (mText) { 204 | case CardText::ZERO: text = "0"; break; 205 | case CardText::ONE: text = "1"; break; 206 | case CardText::TWO: text = "2"; break; 207 | case CardText::THREE: text = "3"; break; 208 | case CardText::FOUR: text = "4"; break; 209 | case CardText::FIVE: text = "5"; break; 210 | case CardText::SIX: text = "6"; break; 211 | case CardText::SEVEN: text = "7"; break; 212 | case CardText::EIGHT: text = "8"; break; 213 | case CardText::NINE: text = "9"; break; 214 | case CardText::SKIP: text = "S"; break; 215 | case CardText::REVERSE: text = "R"; break; 216 | case CardText::DRAW_TWO: text = "+2"; break; 217 | case CardText::WILD: text = "W"; break; 218 | case CardText::DRAW_FOUR: text = "+4"; break; 219 | case CardText::EMPTY: text = ""; break; 220 | default: assert(0); 221 | } 222 | 223 | return color.append(text); 224 | } 225 | 226 | int Card::Length() const 227 | { 228 | int length = 0; 229 | length += (mColor == CardColor::BLACK ? 0 : 1); 230 | length += (!CardSet::DrawTexts.count(mText) ? 1 : 2); 231 | return length; 232 | } 233 | 234 | std::string HandCards::ToString() const 235 | { 236 | return ToStringByCard(0, mCards.size()); 237 | } 238 | 239 | std::string HandCards::ToStringBySegment(int seg) const 240 | { 241 | int start = seg * 8; 242 | int len = std::min(static_cast(mCards.size() - start), 8); 243 | return ToStringByCard(start, len); 244 | } 245 | 246 | std::string HandCards::ToStringByCard(int start, int len) const 247 | { 248 | std::string str; 249 | std::for_each(std::next(mCards.begin(), start), std::next(mCards.begin(), start + len), 250 | [&str](Card card) { 251 | str.append(" ").append(card.ToString()).append(" "); 252 | } 253 | ); 254 | return str; 255 | } 256 | 257 | int HandCards::Length() const 258 | { 259 | return LengthBeforeIndex(mCards.size()); 260 | } 261 | 262 | int HandCards::LengthBeforeIndex(int index) const 263 | { 264 | int length = 0; 265 | std::for_each(mCards.begin(), IteratorAt(index), [&length](Card card) { 266 | // the length of card and spaces at both sides (e.g. " R4 ") 267 | length += (1 + card.Length() + 1); 268 | }); 269 | return length; 270 | } 271 | 272 | int HandCards::LengthBeforeIndexInSegment(int segIndex, int indexInSeg) const 273 | { 274 | int length = 0; 275 | int start = segIndex * 8; 276 | int len = indexInSeg; 277 | std::for_each(std::next(mCards.begin(), start), std::next(mCards.begin(), start + len), 278 | [&length](Card card) { 279 | length += (1 + card.Length() + 1); 280 | } 281 | ); 282 | return length; 283 | } 284 | 285 | CardColor Card::FromChar(char c) 286 | { 287 | switch (c) { 288 | case 'R': return CardColor::RED; 289 | case 'Y': return CardColor::YELLOW; 290 | case 'G': return CardColor::GREEN; 291 | case 'B': return CardColor::BLUE; 292 | } 293 | assert(0); 294 | } 295 | 296 | std::ostream& operator<<(std::ostream& os, const Card& card) 297 | { 298 | os << card.ToString(); 299 | return os; 300 | } 301 | 302 | std::ostream& operator<<(std::ostream& os, const CardColor& color) 303 | { 304 | std::string colorStr; 305 | switch (color) { 306 | case CardColor::RED: colorStr = "RED"; break; 307 | case CardColor::YELLOW: colorStr = "YELLOW"; break; 308 | case CardColor::GREEN: colorStr = "GREEN"; break; 309 | case CardColor::BLUE: colorStr = "BLUE"; break; 310 | default: assert(0); 311 | } 312 | 313 | os << colorStr; 314 | return os; 315 | } 316 | 317 | }} -------------------------------------------------------------------------------- /src/game/cards.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #ifdef ENABLE_LOG 12 | #include 13 | #endif 14 | 15 | #include "../common/common.h" 16 | 17 | namespace UNO { namespace Game { 18 | 19 | enum class CardColor : uint8_t { 20 | RED, YELLOW, GREEN, BLUE, BLACK 21 | }; 22 | std::ostream& operator<<(std::ostream& os, const CardColor& color); 23 | 24 | enum class CardText : uint8_t { 25 | ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, 26 | SKIP, REVERSE, DRAW_TWO, WILD, DRAW_FOUR, EMPTY 27 | /** 28 | * EMPTY is for conditions like: 29 | * 1. in the game start, the flipped card is yellow +2, 30 | * now for the first player, the last played card is `yellow empty`, 31 | * which means he can only play a yellow or wild card 32 | * 2. player A played a yellow +2 / +4, player B got the draw, 33 | * now for player C, the last played card is `yellow empty` 34 | * 3. player A played a green skip, player B was skipped, 35 | * now for player C, the last played card is `green empty` 36 | */ 37 | }; 38 | 39 | /** 40 | * Some special sets of \c CardColor or \c CardText. 41 | */ 42 | struct CardSet { 43 | const static std::set NonWildColors; 44 | const static std::set NonWildTexts; 45 | const static std::set DrawTexts; 46 | }; 47 | 48 | struct Card { 49 | CardColor mColor; 50 | CardText mText; 51 | 52 | Card() {} 53 | Card(const char *str); 54 | Card(CardColor color, CardText text) : mColor(color), mText(text) {} 55 | 56 | /** 57 | * Check whether a card can be played after another one. 58 | * \param lastPlayedCard: the last card which is just played 59 | * \param isUno: whether the card to play is the last (final) one in the player's handcard 60 | * \return whether the card can played after the last played one 61 | */ 62 | bool CanBePlayedAfter(Card lastPlayedCard, bool isUno = false); 63 | 64 | /** 65 | * Get the string format of a card, like 'W', 'R6', 'B+2'. 66 | */ 67 | std::string ToString() const; 68 | 69 | /** 70 | * Get the length of the string format of the card, which can be 1, 2, or 3. 71 | */ 72 | int Length() const; 73 | 74 | static CardColor FromChar(char c); 75 | 76 | bool operator<(const Card &rhs) const { 77 | return (mColor < rhs.mColor) || 78 | (mColor == rhs.mColor && mText < rhs.mText); 79 | } 80 | 81 | bool operator==(const Card &rhs) const { 82 | return (mColor == rhs.mColor) && (mText == rhs.mText); 83 | } 84 | 85 | bool operator!=(const Card &card) const { 86 | return !(*this == card); 87 | } 88 | 89 | friend std::ostream& operator<<(std::ostream& os, const Card& card); 90 | }; 91 | 92 | class HandCards { 93 | public: 94 | HandCards(const std::array &cards); 95 | 96 | /** 97 | * Draw a list of cards and add them into handcards. 98 | */ 99 | void Draw(const std::vector &cards); 100 | 101 | /** 102 | * Check whether the card positioned at \param index in the handcard 103 | * can be played after the \param lastPlayedCard. 104 | */ 105 | bool CanBePlayedAfter(int index, Card lastPlayedCard); 106 | 107 | /** 108 | * Remove the card positioned at \param index in the handcard. 109 | */ 110 | void Erase(int index); 111 | 112 | /** 113 | * Check whether there is any card in the handcard. 114 | */ 115 | bool Empty() const { return mCards.empty(); } 116 | 117 | /** 118 | * Get the index of a given card in the handcards. Noth that: 119 | * 1) the given card has to exist in the handcards (guaranteed by the caller) 120 | * 2) if the given card is duplicated in the handcards, return the index of the first one 121 | */ 122 | int GetIndex(Card card) const; 123 | 124 | /** 125 | * Get the card positioned at \param index in the handcard. 126 | */ 127 | Card At(int index) const { 128 | return *IteratorAt(index); 129 | } 130 | 131 | /** 132 | * Get the number of cards in the handcard. 133 | */ 134 | int Number() const { return mCards.size(); } 135 | 136 | /** 137 | * Get the string format of the entire handcard. 138 | */ 139 | std::string ToString() const; 140 | 141 | /** 142 | * Get the string format of the part of handcard, whcih is in segment \param seg. 143 | */ 144 | std::string ToStringBySegment(int seg) const; 145 | 146 | /** 147 | * Get the length of the string format of the entire handcard, 148 | * including the ' ' between adjacent cards and on both sides. 149 | */ 150 | int Length() const; 151 | 152 | /** 153 | * Get the length of the string format of the part of handcard, 154 | * which is (exclusively) before the segment \param index 155 | */ 156 | int LengthBeforeIndex(int index) const; 157 | 158 | /** 159 | * Get the length of the string format of the part of handcard, 160 | * which belongs to segment \param segIndex and before card \param indexInSeg. 161 | */ 162 | int LengthBeforeIndexInSegment(int segIndex, int indexInSeg) const; 163 | 164 | /** 165 | * Compare the handcards before and after drawing a card, return the index of the newly drawn. 166 | */ 167 | int GetIndexOfNewlyDrawn(const HandCards &handcardsBeforeDraw) const; 168 | 169 | friend std::ostream& operator<<(std::ostream& os, const HandCards& handCards); 170 | 171 | private: 172 | std::multiset::iterator IteratorAt(int index) const; 173 | 174 | std::string ToStringByCard(int start, int len) const; 175 | 176 | private: 177 | // use multiset to ensure that handcards are always in order 178 | std::multiset mCards; 179 | }; 180 | 181 | /** 182 | * \c CardPile: a plie of cards, can be derived as \c Deck and \c DiscardPile 183 | * providing some methods about push/pop, which can be used in different scenarios: 184 | * PushFront: Init deck in the game start. The card goes into discard pile. 185 | * PopFront: Draw from deck. 186 | * PushBack: When the flipped card is a wild card, put it back to under the deck. 187 | * PopBack: not used yet 188 | */ 189 | class CardPile { 190 | protected: 191 | template 192 | void PushFront(Types... args) { 193 | mPile.emplace_front(args...); 194 | } 195 | 196 | Card PopFront() { 197 | Card card = mPile.front(); 198 | mPile.pop_front(); 199 | return card; 200 | } 201 | 202 | template 203 | void PushBack(Types... args) { 204 | mPile.emplace_back(args...); 205 | } 206 | 207 | Card PopBack() { 208 | Card card = mPile.back(); 209 | mPile.pop_back(); 210 | return card; 211 | } 212 | 213 | void Shuffle() { 214 | std::random_device rd; 215 | std::mt19937 g(rd()); 216 | std::shuffle(mPile.begin(), mPile.end(), g); 217 | } 218 | 219 | void Swap(CardPile &pile) { std::swap(mPile, pile.mPile); } 220 | 221 | void Clear() { mPile.clear(); } 222 | 223 | bool Empty() const { return mPile.empty(); } 224 | 225 | public: 226 | // for test 227 | std::deque GetPile() const { return mPile; } 228 | 229 | private: 230 | std::deque mPile; 231 | }; 232 | 233 | class DiscardPile : public CardPile { 234 | public: 235 | /** 236 | * Add a card to discard pile. 237 | */ 238 | void Add(Card card) { PushFront(card); } 239 | 240 | /** 241 | * Clear the discard pile. 242 | */ 243 | void Clear() { CardPile::Clear(); } 244 | }; 245 | 246 | class Deck : public CardPile { 247 | public: 248 | Deck(DiscardPile &discardPile) : mDiscardPile(discardPile) {} 249 | 250 | /** 251 | * Init deck with the 108 UNO cards and shuffle. 252 | */ 253 | void Init(); 254 | 255 | /** 256 | * Deal 7 cards to each player as the initial handcards. 257 | */ 258 | std::vector> DealInitHandCards(int playerNum); 259 | 260 | /** 261 | * Draw a card from deck. 262 | */ 263 | Card Draw(); 264 | 265 | /** 266 | * Draw \param number cards from deck. 267 | */ 268 | std::vector Draw(int number); 269 | 270 | /** 271 | * Put a card to the bottom of deck. 272 | */ 273 | void PutToBottom(Card card) { PushBack(card); } 274 | 275 | private: 276 | // link a discard pile to deck. when the deck is exhausted, swap them 277 | DiscardPile &mDiscardPile; 278 | }; 279 | 280 | }} -------------------------------------------------------------------------------- /src/game/game_board.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "game_board.h" 4 | 5 | namespace UNO { namespace Game { 6 | 7 | GameBoard::GameBoard(std::shared_ptr serverSp) 8 | : mServer(serverSp), 9 | mDiscardPile(std::make_unique()), 10 | mDeck(std::make_unique(*mDiscardPile)) 11 | { 12 | mServer->RegisterReceiveJoinGameInfoCallback( 13 | [this](int index, const JoinGameInfo &info) { 14 | ReceiveUsername(index, info.mUsername); 15 | } 16 | ); 17 | mServer->RegisterAllPlayersJoinedCallback([this] { StartGame(); }); 18 | } 19 | 20 | void GameBoard::Start() 21 | { 22 | mServer->Run(); 23 | } 24 | 25 | std::shared_ptr GameBoard::CreateServer(const std::string &port) 26 | { 27 | return std::make_shared(port); 28 | } 29 | 30 | void GameBoard::ResetGame() 31 | { 32 | mServer->Reset(); 33 | mPlayerStats.clear(); 34 | } 35 | 36 | void GameBoard::ReceiveUsername(int index, const std::string &username) 37 | { 38 | std::cout << "receive, index: " << index << ", username: " << username << std::endl; 39 | mPlayerStats.emplace_back(username, 7); 40 | std::vector tmpUsernames; 41 | std::for_each(mPlayerStats.begin(), mPlayerStats.end(), 42 | [&tmpUsernames](const PlayerStat &stat) { 43 | tmpUsernames.push_back(stat.GetUsername()); 44 | } 45 | ); 46 | Common::Util::Deliver(mServer, index, Common::Common::mPlayerNum, tmpUsernames); 47 | for (int i = 0; i < index; i++) { 48 | Common::Util::Deliver(mServer, i, username); 49 | } 50 | } 51 | 52 | void GameBoard::StartGame() 53 | { 54 | #ifdef ENABLE_LOG 55 | spdlog::info("Game Starts."); 56 | #endif 57 | mDeck->Init(); 58 | std::vector> initHandCards = 59 | mDeck->DealInitHandCards(Common::Common::mPlayerNum); 60 | 61 | // flip a card 62 | Card flippedCard; 63 | while (true) { 64 | flippedCard = mDeck->Draw(); 65 | if (flippedCard.mColor == CardColor::BLACK) { 66 | // if the flipped card is a wild card, put it to under the deck and flip a new one 67 | mDeck->PutToBottom(flippedCard); 68 | } 69 | else { 70 | if (CardSet::DrawTexts.count(flippedCard.mText)) { 71 | // last played card will become EMPTY if the flipped card is `Draw` card 72 | flippedCard.mText = CardText::EMPTY; 73 | } 74 | break; 75 | } 76 | } 77 | 78 | // choose the first player randomly 79 | std::srand(std::time(nullptr)); 80 | int firstPlayer = std::rand() % Common::Common::mPlayerNum; 81 | 82 | std::vector tmpUsernames; 83 | std::for_each(mPlayerStats.begin(), mPlayerStats.end(), 84 | [&tmpUsernames](const PlayerStat &stat) { 85 | tmpUsernames.push_back(stat.GetUsername()); 86 | } 87 | ); 88 | for (int player = 0; player < Common::Common::mPlayerNum; player++) { 89 | Common::Util::Deliver(mServer, player, initHandCards[player], flippedCard, 90 | Common::Util::WrapWithPlayerNum(firstPlayer - player), tmpUsernames); 91 | 92 | std::rotate(tmpUsernames.begin(), tmpUsernames.begin() + 1, tmpUsernames.end()); 93 | } 94 | 95 | mGameStat.reset(new GameStat(firstPlayer, flippedCard)); 96 | GameLoop(); 97 | } 98 | 99 | void GameBoard::GameLoop() 100 | { 101 | while (!mGameStat->DoesGameEnd()) { 102 | try { 103 | auto actionInfo = Common::Util::Receive(mServer, mGameStat->GetCurrentPlayer()); 104 | switch (actionInfo->mActionType) { 105 | case ActionType::DRAW: 106 | HandleDraw(Common::Util::DynamicCast(actionInfo)); 107 | break; 108 | case ActionType::SKIP: 109 | HandleSkip(Common::Util::DynamicCast(actionInfo)); 110 | break; 111 | case ActionType::PLAY: 112 | HandlePlay(Common::Util::DynamicCast(actionInfo)); 113 | break; 114 | default: 115 | assert(0); 116 | } 117 | } 118 | catch (const std::exception &e) { 119 | /// TODO: handle the condition that someone has disconnected 120 | std::cout << "someone has disconnected, shutdown server" << std::endl; 121 | std::exit(-1); 122 | } 123 | } 124 | ResetGame(); 125 | } 126 | 127 | void GameBoard::HandleDraw(const std::unique_ptr &info) 128 | { 129 | std::cout << *info << std::endl; 130 | 131 | // draw from deck 132 | std::vector cardsToDraw = mDeck->Draw(info->mNumber); 133 | 134 | // respond to the deliverer 135 | Common::Util::Deliver(mServer, mGameStat->GetCurrentPlayer(), 136 | info->mNumber, cardsToDraw); 137 | 138 | // broadcast to other players 139 | Broadcast(*info); 140 | 141 | // update stat 142 | mPlayerStats[mGameStat->GetCurrentPlayer()].UpdateAfterDraw(info->mNumber); 143 | mGameStat->UpdateAfterDraw(); 144 | } 145 | 146 | void GameBoard::HandleSkip(const std::unique_ptr &info) 147 | { 148 | std::cout << *info << std::endl; 149 | 150 | // broadcast to other players 151 | Broadcast(*info); 152 | 153 | // update stat 154 | mPlayerStats[mGameStat->GetCurrentPlayer()].UpdateAfterSkip(); 155 | mGameStat->UpdateAfterSkip(); 156 | } 157 | 158 | void GameBoard::HandlePlay(const std::unique_ptr &info) 159 | { 160 | std::cout << *info << std::endl; 161 | 162 | mDiscardPile->Add(info->mCard); 163 | if (info->mCard.mColor == CardColor::BLACK) { 164 | // change the color to the specified next color to show in UI 165 | info->mCard.mColor = info->mNextColor; 166 | } 167 | 168 | // broadcast to other players 169 | Broadcast(*info); 170 | 171 | // update stat 172 | PlayerStat &stat = mPlayerStats[mGameStat->GetCurrentPlayer()]; 173 | stat.UpdateAfterPlay(info->mCard); 174 | if (stat.GetRemainingHandCardsNum() == 0) { 175 | Win(); 176 | } 177 | mGameStat->UpdateAfterPlay(info->mCard); 178 | } 179 | 180 | void GameBoard::Win() 181 | { 182 | mGameStat->GameEnds(); 183 | #ifdef ENABLE_LOG 184 | spdlog::info("Game Ends."); 185 | #endif 186 | } 187 | 188 | }} -------------------------------------------------------------------------------- /src/game/game_board.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "stat.h" 9 | #include "../network/server.h" 10 | 11 | namespace UNO { namespace Game { 12 | 13 | using namespace Network; 14 | 15 | class GameBoard { 16 | public: 17 | explicit GameBoard(std::shared_ptr serverSp); 18 | 19 | void Start(); 20 | 21 | static std::shared_ptr CreateServer(const std::string &port); 22 | 23 | // private: 24 | /** 25 | * Callback of receiving a \c JoinGameInfo from a player. 26 | * \param index: the index of the player 27 | * \param username: the player's username 28 | */ 29 | void ReceiveUsername(int index, const std::string &username); 30 | 31 | /** 32 | * Have received all players' info, start game. 33 | */ 34 | void StartGame(); 35 | 36 | /** 37 | * Main game loop, wait for \c ActionInfo from players and act accordingly. 38 | */ 39 | void GameLoop(); 40 | 41 | /** 42 | * Handle a \c DrawInfo from player. 43 | */ 44 | void HandleDraw(const std::unique_ptr &info); 45 | 46 | /** 47 | * Handle a \c SkipInfo from player. 48 | */ 49 | void HandleSkip(const std::unique_ptr &info); 50 | 51 | /** 52 | * Handle a \c PlayInfo from player. 53 | */ 54 | void HandlePlay(const std::unique_ptr &info); 55 | 56 | /** 57 | * Someone has won, end game. 58 | */ 59 | void Win(); 60 | 61 | /** 62 | * Reset the game state and prepare for restart. 63 | */ 64 | void ResetGame(); 65 | 66 | /** 67 | * Broadcast info to players other than the current one. 68 | */ 69 | template 70 | void Broadcast(ActionInfoT &info) { 71 | int currentPlayer = mGameStat->GetCurrentPlayer(); 72 | for (int i = 0; i < Common::Common::mPlayerNum; i++) { 73 | if (i != currentPlayer) { 74 | info.mPlayerIndex = Common::Util::WrapWithPlayerNum(currentPlayer - i); 75 | mServer->DeliverInfo(&typeid(ActionInfoT), i, info); 76 | } 77 | } 78 | } 79 | 80 | public: 81 | // for tests 82 | // const std::unique_ptr &GetServer() const { return mServer; } 83 | 84 | const std::unique_ptr &GetDiscardPile() const { return mDiscardPile; } 85 | 86 | const std::unique_ptr &GetDeck() const { return mDeck; } 87 | 88 | const std::unique_ptr &GetGameStat() const { return mGameStat; } 89 | 90 | const std::vector &GetPlayerStats() const { return mPlayerStats; } 91 | 92 | private: 93 | std::shared_ptr mServer; 94 | 95 | // state of game board 96 | std::unique_ptr mDiscardPile; 97 | std::unique_ptr mDeck; 98 | std::unique_ptr mGameStat; 99 | 100 | // state of all players 101 | std::vector mPlayerStats; 102 | }; 103 | }} -------------------------------------------------------------------------------- /src/game/info.cpp: -------------------------------------------------------------------------------- 1 | #include "info.h" 2 | 3 | namespace UNO { namespace Game { 4 | 5 | using namespace Network; 6 | 7 | void JoinGameInfo::Serialize(uint8_t *buffer) const 8 | { 9 | JoinGameMsg *msg = reinterpret_cast(buffer); 10 | msg->mType = MsgType::JOIN_GAME; 11 | msg->mLen = mUsername.size(); 12 | std::strcpy(msg->mUsername, mUsername.c_str()); 13 | } 14 | 15 | std::unique_ptr JoinGameInfo::Deserialize(const uint8_t *buffer) 16 | { 17 | const JoinGameMsg *msg = reinterpret_cast(buffer); 18 | std::unique_ptr info = std::make_unique(); 19 | info->mUsername = msg->mUsername; 20 | return info; 21 | } 22 | 23 | void JoinGameRspInfo::Serialize(uint8_t *buffer) const 24 | { 25 | JoinGameRspMsg *msg = reinterpret_cast(buffer); 26 | msg->mType = MsgType::JOIN_GAME_RSP; 27 | 28 | std::string usernames{}; 29 | std::for_each(mUsernames.begin(), mUsernames.end(), 30 | [&usernames](const std::string &username) { 31 | usernames.append(username).push_back(' '); 32 | } 33 | ); 34 | msg->mLen = sizeof(int) + usernames.size(); 35 | 36 | msg->mPlayerNum = mPlayerNum; 37 | std::strcpy(msg->mUsernames, usernames.c_str()); 38 | } 39 | 40 | std::unique_ptr JoinGameRspInfo::Deserialize(const uint8_t *buffer) 41 | { 42 | const JoinGameRspMsg *msg = reinterpret_cast(buffer); 43 | std::unique_ptr info = std::make_unique(); 44 | info->mPlayerNum = msg->mPlayerNum; 45 | std::string usernames(msg->mUsernames); 46 | while (!usernames.empty()) { 47 | int pos = usernames.find(' '); 48 | info->mUsernames.emplace_back(usernames, 0, pos); 49 | usernames.erase(0, pos + 1); 50 | } 51 | return info; 52 | } 53 | 54 | void GameStartInfo::Serialize(uint8_t *buffer) const 55 | { 56 | GameStartMsg *msg = reinterpret_cast(buffer); 57 | msg->mType = MsgType::GAME_START; 58 | 59 | std::string usernames{}; 60 | std::for_each(mUsernames.begin(), mUsernames.end(), 61 | [&usernames](const std::string &username) { 62 | // ' ' as delimiter of usernames 63 | usernames.append(username).push_back(' '); 64 | } 65 | ); 66 | msg->mLen = sizeof(Card) * 8 + sizeof(int) + usernames.size(); 67 | 68 | std::copy(mInitHandCards.begin(), mInitHandCards.end(), msg->mInitHandCards); 69 | msg->mFlippedCard = mFlippedCard; 70 | msg->mFirstPlayer = mFirstPlayer; 71 | std::strcpy(msg->mUsernames, usernames.c_str()); 72 | } 73 | 74 | std::unique_ptr GameStartInfo::Deserialize(const uint8_t *buffer) 75 | { 76 | const GameStartMsg *msg = reinterpret_cast(buffer); 77 | std::unique_ptr info = std::make_unique(); 78 | 79 | std::copy(std::begin(msg->mInitHandCards), std::end(msg->mInitHandCards), 80 | info->mInitHandCards.begin()); 81 | info->mFlippedCard = msg->mFlippedCard; 82 | info->mFirstPlayer = msg->mFirstPlayer; 83 | std::string usernames(msg->mUsernames); 84 | while (!usernames.empty()) { 85 | int pos = usernames.find(' '); 86 | info->mUsernames.emplace_back(usernames, 0, pos); 87 | usernames.erase(0, pos + 1); 88 | } 89 | 90 | return info; 91 | } 92 | 93 | void ActionInfo::Serialize(uint8_t *buffer) const 94 | { 95 | ActionMsg *msg = reinterpret_cast(buffer); 96 | msg->mType = MsgType::ACTION; 97 | msg->mLen = sizeof(ActionMsg) - sizeof(Msg); 98 | msg->mActionType = mActionType; 99 | msg->mPlayerIndex = mPlayerIndex; 100 | } 101 | 102 | std::unique_ptr ActionInfo::Deserialize(const uint8_t *buffer) 103 | { 104 | const ActionMsg *msg = reinterpret_cast(buffer); 105 | // info here is polymorphic 106 | std::unique_ptr info; 107 | switch (msg->mActionType) { 108 | case ActionType::DRAW: 109 | info.reset(dynamic_cast(DrawInfo::Deserialize(buffer).release())); 110 | break; 111 | case ActionType::SKIP: 112 | info.reset(dynamic_cast(SkipInfo::Deserialize(buffer).release())); 113 | break; 114 | case ActionType::PLAY: 115 | info.reset(dynamic_cast(PlayInfo::Deserialize(buffer).release())); 116 | break; 117 | default: 118 | assert(0); 119 | } 120 | info->mActionType = msg->mActionType; 121 | info->mPlayerIndex = msg->mPlayerIndex; 122 | return info; 123 | } 124 | 125 | void DrawInfo::Serialize(uint8_t *buffer) const 126 | { 127 | ActionInfo::Serialize(buffer); 128 | DrawMsg *msg = reinterpret_cast(buffer); 129 | msg->mLen = sizeof(DrawMsg) - sizeof(Msg); 130 | msg->mNumber = mNumber; 131 | } 132 | 133 | std::unique_ptr DrawInfo::Deserialize(const uint8_t *buffer) 134 | { 135 | const DrawMsg *msg = reinterpret_cast(buffer); 136 | std::unique_ptr info = std::make_unique(); 137 | info->mNumber = msg->mNumber; 138 | return info; 139 | } 140 | 141 | void SkipInfo::Serialize(uint8_t *buffer) const 142 | { 143 | ActionInfo::Serialize(buffer); 144 | SkipMsg *msg = reinterpret_cast(buffer); 145 | msg->mLen = sizeof(SkipMsg) - sizeof(Msg); 146 | } 147 | 148 | std::unique_ptr SkipInfo::Deserialize(const uint8_t *buffer) 149 | { 150 | const SkipMsg *msg = reinterpret_cast(buffer); 151 | std::unique_ptr info = std::make_unique(); 152 | return info; 153 | } 154 | 155 | void PlayInfo::Serialize(uint8_t *buffer) const 156 | { 157 | ActionInfo::Serialize(buffer); 158 | PlayMsg *msg = reinterpret_cast(buffer); 159 | msg->mLen = sizeof(PlayMsg) - sizeof(Msg); 160 | msg->mCard = mCard; 161 | msg->mNextColor = mNextColor; 162 | } 163 | 164 | std::unique_ptr PlayInfo::Deserialize(const uint8_t *buffer) 165 | { 166 | const PlayMsg *msg = reinterpret_cast(buffer); 167 | std::unique_ptr info = std::make_unique(); 168 | info->mCard = msg->mCard; 169 | info->mNextColor = msg->mNextColor; 170 | return info; 171 | } 172 | 173 | void DrawRspInfo::Serialize(uint8_t *buffer) const 174 | { 175 | DrawRspMsg *msg = reinterpret_cast(buffer); 176 | msg->mType = MsgType::DRAW_RSP; 177 | 178 | msg->mLen = sizeof(int) + mNumber * sizeof(Card); 179 | msg->mNumber = mNumber; 180 | 181 | std::copy(mCards.begin(), mCards.end(), msg->mCards); 182 | } 183 | 184 | std::unique_ptr DrawRspInfo::Deserialize(const uint8_t *buffer) 185 | { 186 | const DrawRspMsg *msg = reinterpret_cast(buffer); 187 | std::unique_ptr info = std::make_unique(); 188 | info->mNumber = msg->mNumber; 189 | info->mCards.resize(info->mNumber); 190 | std::copy(msg->mCards, msg->mCards + msg->mNumber, info->mCards.begin()); 191 | return info; 192 | } 193 | 194 | void GameEndInfo::Serialize(uint8_t *buffer) const 195 | { 196 | GameEndMsg *msg = reinterpret_cast(buffer); 197 | msg->mType = MsgType::GAME_END; 198 | msg->mLen = sizeof(int); 199 | msg->mWinner = mWinner; 200 | } 201 | 202 | std::unique_ptr GameEndInfo::Deserialize(const uint8_t *buffer) 203 | { 204 | const GameEndMsg *msg = reinterpret_cast(buffer); 205 | std::unique_ptr info = std::make_unique(); 206 | info->mWinner = msg->mWinner; 207 | return info; 208 | } 209 | 210 | bool JoinGameInfo::operator==(const JoinGameInfo &info) const 211 | { 212 | return mUsername == info.mUsername; 213 | } 214 | 215 | bool JoinGameRspInfo::operator==(const JoinGameRspInfo &info) const 216 | { 217 | return (mPlayerNum == info.mPlayerNum) && (mUsernames == info.mUsernames); 218 | } 219 | 220 | bool GameStartInfo::operator==(const GameStartInfo &info) const 221 | { 222 | return (mInitHandCards == info.mInitHandCards) && (mFlippedCard == info.mFlippedCard) 223 | && (mFirstPlayer == info.mFirstPlayer) && (mUsernames == info.mUsernames); 224 | } 225 | 226 | bool ActionInfo::operator==(const ActionInfo &info) const 227 | { 228 | return (mActionType == info.mActionType) && (mPlayerIndex == info.mPlayerIndex); 229 | } 230 | 231 | bool DrawInfo::operator==(const DrawInfo &info) const 232 | { 233 | return (mNumber == info.mNumber) 234 | && (dynamic_cast(*this) == dynamic_cast(info)); 235 | } 236 | 237 | bool SkipInfo::operator==(const SkipInfo &info) const 238 | { 239 | return dynamic_cast(*this) == dynamic_cast(info); 240 | } 241 | 242 | bool PlayInfo::operator==(const PlayInfo &info) const 243 | { 244 | return (mCard == info.mCard) && (mNextColor == info.mNextColor) 245 | && (dynamic_cast(*this) == dynamic_cast(info)); 246 | } 247 | 248 | bool DrawRspInfo::operator==(const DrawRspInfo &info) const 249 | { 250 | return (mNumber == info.mNumber) && (mCards == info.mCards); 251 | } 252 | 253 | bool GameEndInfo::operator==(const GameEndInfo &info) const 254 | { 255 | return mWinner == info.mWinner; 256 | } 257 | 258 | std::ostream& operator<<(std::ostream& os, const JoinGameInfo& info) 259 | { 260 | os << "JoinGameInfo Received: " << std::endl; 261 | os << "\t mUsername: " << info.mUsername << std::endl; 262 | return os; 263 | } 264 | 265 | std::ostream& operator<<(std::ostream& os, const JoinGameRspInfo& info) 266 | { 267 | os << "JoinGameRspInfo Received: " << std::endl; 268 | os << "\t mPlayerNum: " << info.mPlayerNum << std::endl; 269 | os << "\t mUsernames: ["; 270 | assert(!info.mUsernames.empty()); 271 | for (int i = 0; i < info.mUsernames.size() - 1; i++) { 272 | os << info.mUsernames[i] << ", "; 273 | } 274 | os << info.mUsernames.back() << "]" << std::endl; 275 | return os; 276 | } 277 | 278 | std::ostream& operator<<(std::ostream& os, const GameStartInfo& info) 279 | { 280 | os << "GameStartInfo Received: " << std::endl; 281 | os << "\t mInitHandCards: ["; 282 | for (int i = 0; i < 6; i++) { 283 | os << info.mInitHandCards[i] << ", "; 284 | } 285 | os << info.mInitHandCards[6] << "]" << std::endl; 286 | 287 | os << "\t mFlippedCard: " << info.mFlippedCard << std::endl; 288 | os << "\t mFirstPlayer: " << info.mFirstPlayer << std::endl; 289 | 290 | os << "\t mUsernames: ["; 291 | assert(!info.mUsernames.empty()); 292 | for (int i = 0; i < info.mUsernames.size() - 1; i++) { 293 | os << info.mUsernames[i] << ", "; 294 | } 295 | os << info.mUsernames.back() << "]" << std::endl; 296 | 297 | return os; 298 | } 299 | 300 | std::ostream& operator<<(std::ostream& os, const ActionInfo& info) 301 | { 302 | os << "\t mActionType: " << info.mActionType << std::endl; 303 | os << "\t mPlayerIndex: " << info.mPlayerIndex << std::endl; 304 | return os; 305 | } 306 | 307 | std::ostream& operator<<(std::ostream& os, const DrawInfo& info) 308 | { 309 | os << "DrawInfo Received: " << std::endl; 310 | os << dynamic_cast(info); 311 | os << "\t mNumber: " << info.mNumber << std::endl; 312 | return os; 313 | } 314 | 315 | std::ostream& operator<<(std::ostream& os, const SkipInfo& info) 316 | { 317 | os << "SkipInfo Received: " << std::endl; 318 | os << dynamic_cast(info); 319 | return os; 320 | } 321 | 322 | std::ostream& operator<<(std::ostream& os, const PlayInfo& info) 323 | { 324 | os << "PlayInfo Received: " << std::endl; 325 | os << dynamic_cast(info); 326 | os << "\t mCard: " << info.mCard << std::endl; 327 | os << "\t mNextColor: " << info.mNextColor << std::endl; 328 | return os; 329 | } 330 | 331 | std::ostream& operator<<(std::ostream& os, const DrawRspInfo& info) 332 | { 333 | os << "DrawRspInfo Received: " << std::endl; 334 | os << "\t mNumber: " << info.mNumber << std::endl; 335 | os << "\t mCards: ["; 336 | assert(!info.mCards.empty()); 337 | for (int i = 0; i < info.mCards.size() - 1; i++) { 338 | os << info.mCards[i] << ", "; 339 | } 340 | os << info.mCards.back() << "]" << std::endl; 341 | return os; 342 | } 343 | }} -------------------------------------------------------------------------------- /src/game/info.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../network/msg.h" 11 | 12 | namespace UNO { namespace Game { 13 | 14 | using namespace Network; 15 | 16 | struct Info { 17 | // enable polymorphism 18 | virtual ~Info() {} 19 | }; 20 | 21 | struct JoinGameInfo : public Info { 22 | std::string mUsername; 23 | 24 | JoinGameInfo() {} 25 | JoinGameInfo(const std::string &username) : mUsername(username) {} 26 | 27 | void Serialize(uint8_t *buffer) const; 28 | static std::unique_ptr Deserialize(const uint8_t *buffer); 29 | 30 | bool operator==(const JoinGameInfo &info) const; 31 | friend std::ostream &operator<<(std::ostream &os, const JoinGameInfo &info); 32 | }; 33 | 34 | struct JoinGameRspInfo : public Info { 35 | int mPlayerNum; 36 | std::vector mUsernames; 37 | 38 | JoinGameRspInfo() {} 39 | JoinGameRspInfo(int playerNum, const std::vector &usernames) 40 | : mPlayerNum(playerNum), mUsernames(usernames) {} 41 | 42 | void Serialize(uint8_t *buffer) const; 43 | static std::unique_ptr Deserialize(const uint8_t *buffer); 44 | 45 | bool operator==(const JoinGameRspInfo &info) const; 46 | friend std::ostream &operator<<(std::ostream &os, const JoinGameRspInfo &info); 47 | }; 48 | 49 | struct GameStartInfo : public Info { 50 | std::array mInitHandCards; 51 | Card mFlippedCard; 52 | int mFirstPlayer; 53 | std::vector mUsernames; 54 | 55 | GameStartInfo() {} 56 | GameStartInfo(const std::array &initHandCards, 57 | Card flippedCard, int firstPlayer, 58 | const std::vector &usernames) 59 | : mInitHandCards(initHandCards), mFlippedCard(flippedCard), 60 | mFirstPlayer(firstPlayer), mUsernames(usernames) {} 61 | 62 | void Serialize(uint8_t *buffer) const; 63 | static std::unique_ptr Deserialize(const uint8_t *buffer); 64 | 65 | bool operator==(const GameStartInfo &info) const; 66 | friend std::ostream& operator<<(std::ostream& os, const GameStartInfo& info); 67 | }; 68 | 69 | struct ActionInfo : public Info { 70 | ActionType mActionType; 71 | int mPlayerIndex{-1}; 72 | 73 | ActionInfo() {} 74 | ActionInfo(ActionType actionType) : mActionType(actionType) {} 75 | 76 | void Serialize(uint8_t *buffer) const; 77 | static std::unique_ptr Deserialize(const uint8_t *buffer); 78 | 79 | bool operator==(const ActionInfo &info) const; 80 | friend std::ostream& operator<<(std::ostream& os, const ActionInfo& info); 81 | 82 | // enable polymorphism 83 | virtual ~ActionInfo() {} 84 | }; 85 | 86 | struct DrawInfo : public ActionInfo { 87 | int mNumber; 88 | 89 | DrawInfo() : ActionInfo(ActionType::DRAW) {} 90 | DrawInfo(int number) : ActionInfo(ActionType::DRAW), mNumber(number) {} 91 | 92 | void Serialize(uint8_t *buffer) const; 93 | static std::unique_ptr Deserialize(const uint8_t *buffer); 94 | 95 | bool operator==(const DrawInfo &info) const; 96 | friend std::ostream& operator<<(std::ostream& os, const DrawInfo& info); 97 | }; 98 | 99 | struct SkipInfo : public ActionInfo { 100 | SkipInfo() : ActionInfo(ActionType::SKIP) {} 101 | 102 | void Serialize(uint8_t *buffer) const; 103 | static std::unique_ptr Deserialize(const uint8_t *buffer); 104 | 105 | bool operator==(const SkipInfo &info) const; 106 | friend std::ostream& operator<<(std::ostream& os, const SkipInfo& info); 107 | }; 108 | 109 | struct PlayInfo : public ActionInfo { 110 | Card mCard; 111 | CardColor mNextColor; 112 | 113 | PlayInfo() : ActionInfo(ActionType::PLAY) {} 114 | PlayInfo(Card card) : PlayInfo(card, card.mColor) {} 115 | PlayInfo(Card card, CardColor nextColor) 116 | : ActionInfo(ActionType::PLAY), mCard(card), mNextColor(nextColor) {} 117 | 118 | void Serialize(uint8_t *buffer) const; 119 | static std::unique_ptr Deserialize(const uint8_t *buffer); 120 | 121 | bool operator==(const PlayInfo &info) const; 122 | friend std::ostream& operator<<(std::ostream& os, const PlayInfo& info); 123 | }; 124 | 125 | struct DrawRspInfo : public Info { 126 | int mNumber; 127 | std::vector mCards; 128 | 129 | DrawRspInfo() {} 130 | DrawRspInfo(int number, const std::vector &cards) 131 | : mNumber(number), mCards(cards) {} 132 | 133 | void Serialize(uint8_t *buffer) const; 134 | static std::unique_ptr Deserialize(const uint8_t *buffer); 135 | 136 | bool operator==(const DrawRspInfo &info) const; 137 | friend std::ostream& operator<<(std::ostream& os, const DrawRspInfo& info); 138 | }; 139 | 140 | struct GameEndInfo : public Info { 141 | int mWinner; 142 | 143 | GameEndInfo() {} 144 | GameEndInfo(int winner) : mWinner(winner) {} 145 | 146 | void Serialize(uint8_t *buffer) const; 147 | static std::unique_ptr Deserialize(const uint8_t *buffer); 148 | 149 | bool operator==(const GameEndInfo &info) const; 150 | }; 151 | }} -------------------------------------------------------------------------------- /src/game/player.cpp: -------------------------------------------------------------------------------- 1 | #include "player.h" 2 | 3 | namespace UNO { namespace Game { 4 | 5 | Player::Player(std::string username, std::shared_ptr clientSp) 6 | : mUsername(username), mClient(clientSp) 7 | { 8 | mClient->RegisterConnectCallback([this] { JoinGame(); }); 9 | } 10 | 11 | void Player::Start() 12 | { 13 | mClient->Connect(); 14 | } 15 | 16 | std::shared_ptr Player::CreateClient(const std::string &host, const std::string &port) 17 | { 18 | return std::make_shared(host, port); 19 | } 20 | 21 | void Player::ResetGame() 22 | { 23 | mClient->Reset(); 24 | mPlayerStats.clear(); 25 | } 26 | 27 | void Player::JoinGame() 28 | { 29 | // std::cout << "connect success, sending username to server" << std::endl; 30 | Common::Util::Deliver(mClient, mUsername); 31 | 32 | /// TODO: If the room is full and another player wants to join in, he will block here 33 | /// maybe use a clock to detect if the room is full. 34 | auto joinRsp = Common::Util::Receive(mClient); 35 | // std::cout << *joinRsp << std::endl; 36 | auto initUsernames = joinRsp->mUsernames; 37 | auto initSize = initUsernames.size(); 38 | // don't forget to update common config 39 | Common::Common::mPlayerNum = joinRsp->mPlayerNum; 40 | // UIManager should be initialized after common config being loaded 41 | mUIManager = std::make_unique(mGameStat, mPlayerStats, mHandCards); 42 | mUIManager->RenderWhenInitWaiting(initUsernames, true); 43 | for (auto i = 0; i < Common::Common::mPlayerNum - initSize; i++) { 44 | auto joinInfo = Common::Util::Receive(mClient); 45 | initUsernames.push_back(joinInfo->mUsername); 46 | mUIManager->RenderWhenInitWaiting(initUsernames, false); 47 | } 48 | 49 | // wait for game start 50 | auto info = Common::Util::Receive(mClient); 51 | // std::cout << *info << std::endl; 52 | 53 | mHandCards.reset(new HandCards(info->mInitHandCards)); 54 | mGameStat.reset(new GameStat(*info)); 55 | std::for_each(info->mUsernames.begin(), info->mUsernames.end(), 56 | [this](const std::string &username) { 57 | mPlayerStats.emplace_back(username, 7); 58 | } 59 | ); 60 | 61 | mUIManager->RunTimerThread(); 62 | GameLoop(); 63 | } 64 | 65 | void Player::GameLoop() 66 | { 67 | while (!mGameStat->DoesGameEnd()) { 68 | if (mGameStat->IsMyTurn()) { 69 | // when it's my turn, reset the cursor for a better ui, 70 | // except the condition that having a chance to play immediately after draw 71 | if (!mPlayerStats[0].HasChanceToPlayAfterDraw()) { 72 | mUIManager->NextTurn(); 73 | } 74 | bool actionSuccess = false; 75 | bool lastCardCanBePlayed = true; 76 | while (!actionSuccess) { 77 | auto [action, cardIndex] = mUIManager->GetAction(lastCardCanBePlayed, 78 | mPlayerStats[0].HasChanceToPlayAfterDraw()); 79 | switch (action) { 80 | case InputAction::PASS: { 81 | if (mPlayerStats[0].HasChanceToPlayAfterDraw() || mGameStat->IsSkipped()) { 82 | HandleSelfSkip(); 83 | } 84 | else { 85 | HandleSelfDraw(); 86 | } 87 | actionSuccess = true; 88 | break; 89 | } 90 | case InputAction::PLAY: { 91 | actionSuccess = HandleSelfPlay(cardIndex); 92 | // if action succeeded, ok, nothing happens. 93 | // while if failure, lastCardCanBePlayed will be set to false, 94 | // which will affect the hint text. 95 | lastCardCanBePlayed = actionSuccess; 96 | break; 97 | } 98 | default: 99 | assert(0); 100 | } 101 | } 102 | } 103 | else { 104 | if (!mPlayerStats[mGameStat->GetCurrentPlayer()].HasChanceToPlayAfterDraw()) { 105 | mUIManager->NextTurn(); 106 | } 107 | mUIManager->Render(); 108 | // wait for gameboard state update from server 109 | auto info = Common::Util::Receive(mClient); 110 | switch (info->mActionType) { 111 | case ActionType::DRAW: { 112 | auto drawInfo = Common::Util::DynamicCast(info); 113 | // std::cout << *drawInfo << std::endl; 114 | UpdateStateAfterDraw(drawInfo->mPlayerIndex, drawInfo->mNumber); 115 | break; 116 | } 117 | case ActionType::SKIP: { 118 | auto skipInfo = Common::Util::DynamicCast(info); 119 | // std::cout << *skipInfo << std::endl; 120 | UpdateStateAfterSkip(skipInfo->mPlayerIndex); 121 | break; 122 | } 123 | case ActionType::PLAY: { 124 | auto playInfo = Common::Util::DynamicCast(info); 125 | // std::cout << *playInfo << std::endl; 126 | UpdateStateAfterPlay(playInfo->mPlayerIndex, playInfo->mCard); 127 | break; 128 | } 129 | default: 130 | assert(0); 131 | } 132 | } 133 | } 134 | // show one more frame after win 135 | mUIManager->Render(); 136 | GameEnds(); 137 | } 138 | 139 | void Player::GameEnds() 140 | { 141 | // let the server resets game first 142 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 143 | if (!mUIManager->WantToPlayAgain(mWinner)) { 144 | std::exit(0); 145 | } 146 | ResetGame(); 147 | } 148 | 149 | void Player::HandleSelfDraw() 150 | { 151 | Common::Util::Deliver(mClient, mGameStat->GetCardsNumToDraw()); 152 | // wait for draw rsp msg 153 | auto info = Common::Util::Receive(mClient); 154 | auto handcardsBeforeDraw = *mHandCards; 155 | int indexOfNewlyDrawn = -1; 156 | mHandCards->Draw(info->mCards); 157 | if (info->mNumber == 1) { 158 | indexOfNewlyDrawn = mHandCards->GetIndexOfNewlyDrawn(handcardsBeforeDraw); 159 | } 160 | 161 | UpdateStateAfterDraw(0, mGameStat->GetCardsNumToDraw(), indexOfNewlyDrawn); 162 | if (!mPlayerStats[0].HasChanceToPlayAfterDraw()) { 163 | // draw penalty due to a +2 / +4, cannot play immediately 164 | HandleSelfSkip(); 165 | } 166 | else { 167 | // a common draw, move the cursor to the card just drawn 168 | assert(info->mCards.size() == 1); 169 | Card cardDrawn = info->mCards.front(); 170 | int cursorIndex = mHandCards->GetIndex(cardDrawn); 171 | mUIManager->MoveCursorTo(cursorIndex); 172 | } 173 | } 174 | 175 | void Player::HandleSelfSkip() 176 | { 177 | Common::Util::Deliver(mClient); 178 | UpdateStateAfterSkip(0); 179 | } 180 | 181 | bool Player::HandleSelfPlay(int cardIndex) 182 | { 183 | Card cardToPlay = mHandCards->At(cardIndex); 184 | 185 | if (mHandCards->CanBePlayedAfter(cardIndex, mGameStat->GetLastPlayedCard())) { 186 | // the card to play should be erased **after** specifying next color if it's wild card 187 | CardColor nextColor = (cardToPlay.mColor != CardColor::BLACK) ? 188 | cardToPlay.mColor : mUIManager->SpecifyNextColor(); 189 | Common::Util::Deliver(mClient, cardToPlay, nextColor); 190 | cardToPlay.mColor = nextColor; 191 | UpdateStateAfterPlay(0, cardToPlay); 192 | mHandCards->Erase(cardIndex); 193 | return true; 194 | } 195 | return false; 196 | } 197 | 198 | void Player::UpdateStateAfterDraw(int playerIndex, int number, int indexOfNewlyDrawn) 199 | { 200 | mPlayerStats[playerIndex].UpdateAfterDraw(number, indexOfNewlyDrawn); 201 | mGameStat->UpdateAfterDraw(); 202 | } 203 | 204 | void Player::UpdateStateAfterSkip(int playerIndex) 205 | { 206 | mPlayerStats[playerIndex].UpdateAfterSkip(); 207 | mGameStat->UpdateAfterSkip(); 208 | } 209 | 210 | void Player::UpdateStateAfterPlay(int playerIndex, Card cardPlayed) 211 | { 212 | PlayerStat &stat = mPlayerStats[playerIndex]; 213 | stat.UpdateAfterPlay(cardPlayed); 214 | if (stat.GetRemainingHandCardsNum() == 0) { 215 | Win(playerIndex); 216 | } 217 | 218 | mGameStat->UpdateAfterPlay(cardPlayed); 219 | } 220 | 221 | void Player::Win(int playerIndex) 222 | { 223 | mUIManager->StopTimerThread(); 224 | mGameStat->GameEnds(); 225 | mWinner = (playerIndex == 0) ? "You" : mPlayerStats[playerIndex].GetUsername(); 226 | } 227 | 228 | void Player::PrintLocalState() 229 | { 230 | std::cout << "Local State: " << std::endl; 231 | std::cout << "\t " << *mHandCards << std::endl; 232 | std::cout << "\t mLastPlayedCard: " << mGameStat->GetLastPlayedCard() << std::endl; 233 | std::cout << "\t mCurrentPlayer: " << mGameStat->GetCurrentPlayer() << std::endl; 234 | std::cout << "\t mIsInClockwise: " << mGameStat->IsInClockwise() << std::endl; 235 | std::cout << "\t mCardsNumToDraw: " << mGameStat->GetCardsNumToDraw() << std::endl; 236 | 237 | std::cout << "\t mPlayerStats: [" << std::endl; 238 | for (const auto &stat : mPlayerStats) { 239 | std::cout << " " << stat << std::endl; 240 | } 241 | std::cout << "\t ]" << std::endl; 242 | } 243 | 244 | }} 245 | -------------------------------------------------------------------------------- /src/game/player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "stat.h" 8 | #include "../network/client.h" 9 | #include "../ui/ui_manager.h" 10 | 11 | namespace UNO { namespace Game { 12 | 13 | using namespace Network; 14 | using namespace UI; 15 | 16 | class Player { 17 | public: 18 | explicit Player(std::string username, std::shared_ptr clientSp); 19 | 20 | void Start(); 21 | 22 | static std::shared_ptr CreateClient(const std::string &host, 23 | const std::string &port); 24 | 25 | private: 26 | /** 27 | * Connection succeeded. Prepare game state and wait for others joining. 28 | */ 29 | void JoinGame(); 30 | 31 | /** 32 | * Main game loop, 33 | * 1) in player's turn, get his action and deliver to server; 34 | * 2) in others' turn, receive info and render ui. 35 | */ 36 | void GameLoop(); 37 | 38 | /** 39 | * Deliver info after player does a draw action. 40 | */ 41 | void HandleSelfDraw(); 42 | 43 | /** 44 | * Deliver info after player does a skip action. 45 | */ 46 | void HandleSelfSkip(); 47 | 48 | /** 49 | * Deliver info after player does a play action. 50 | */ 51 | bool HandleSelfPlay(int cardIndex); 52 | 53 | /** 54 | * Update state after a draw action either from player or others. 55 | */ 56 | void UpdateStateAfterDraw(int playerIndex, int number, int indexOfNewlyDrawn = -1); 57 | 58 | /** 59 | * Update state after a skip action either from player or others. 60 | */ 61 | void UpdateStateAfterSkip(int playerIndex); 62 | 63 | /** 64 | * Update state after a play action either from player or others. 65 | */ 66 | void UpdateStateAfterPlay(int playerIndex, Card cardPlayed); 67 | 68 | /** 69 | * Someone has won, end game. 70 | */ 71 | void Win(int playerIndex); 72 | 73 | /** 74 | * Wait for the player to decide whether want to play again. 75 | */ 76 | void GameEnds(); 77 | 78 | /** 79 | * Reset the game state and prepare for reconnection. 80 | */ 81 | void ResetGame(); 82 | 83 | void PrintLocalState(); 84 | 85 | private: 86 | const std::string mUsername; 87 | std::string mWinner; 88 | std::shared_ptr mClient; 89 | 90 | std::unique_ptr mUIManager; 91 | std::unique_ptr mHandCards; 92 | 93 | // state of game board 94 | std::unique_ptr mGameStat; 95 | 96 | // state of all players 97 | std::vector mPlayerStats; 98 | }; 99 | }} -------------------------------------------------------------------------------- /src/game/stat.cpp: -------------------------------------------------------------------------------- 1 | #include "stat.h" 2 | 3 | namespace UNO { namespace Game { 4 | 5 | GameStat::GameStat(const GameStartInfo &info) 6 | : mCurrentPlayer(info.mFirstPlayer), 7 | mIsInClockwise(info.mFlippedCard.mText != CardText::REVERSE), 8 | mLastPlayedCard(info.mFlippedCard) {} 9 | 10 | GameStat::GameStat(int firstPlayer, Card flippedCard) 11 | : mCurrentPlayer(firstPlayer), 12 | mIsInClockwise(flippedCard.mText != CardText::REVERSE) {} 13 | 14 | void GameStat::NextPlayer() 15 | { 16 | mCurrentPlayer = mIsInClockwise ? 17 | Common::Util::WrapWithPlayerNum(mCurrentPlayer + 1) : 18 | Common::Util::WrapWithPlayerNum(mCurrentPlayer - 1); 19 | mTimeElapsed = 0; 20 | } 21 | 22 | void GameStat::UpdateAfterDraw() 23 | { 24 | if (CardSet::DrawTexts.count(mLastPlayedCard.mText)) { 25 | // last played card will become EMPTY after the draw penalty is consumed 26 | mLastPlayedCard.mText = CardText::EMPTY; 27 | } 28 | // the number of cards to draw falls back to 1 29 | mCardsNumToDraw = 1; 30 | 31 | // no need to invoke NextPlayer() here 32 | // because a draw action is always followed by a skip or play action 33 | } 34 | 35 | void GameStat::UpdateAfterSkip() 36 | { 37 | if (mLastPlayedCard.mText == CardText::SKIP) { 38 | // last played card will become EMPTY after the skip penalty is consumed 39 | mLastPlayedCard.mText = CardText::EMPTY; 40 | } 41 | NextPlayer(); 42 | } 43 | 44 | void GameStat::UpdateAfterPlay(Card card) 45 | { 46 | if (card.mText == CardText::WILD) { 47 | // if just a common wild card (not +4), don't affect the number text 48 | mLastPlayedCard.mColor = card.mColor; 49 | } 50 | else { 51 | mLastPlayedCard = card; 52 | } 53 | 54 | if (card.mText == CardText::REVERSE) { 55 | mIsInClockwise = !mIsInClockwise; 56 | } 57 | if (card.mText == CardText::DRAW_TWO) { 58 | // in the normal state, mCardsNumToDraw is equal to 1 59 | // once a player plays a `Draw` card, the effect is gonna accumulate 60 | mCardsNumToDraw = (mCardsNumToDraw == 1) ? 2 : (mCardsNumToDraw + 2); 61 | } 62 | if (card.mText == CardText::DRAW_FOUR) { 63 | mCardsNumToDraw = (mCardsNumToDraw == 1) ? 4 : (mCardsNumToDraw + 4); 64 | } 65 | 66 | if (!mGameEnds) { 67 | NextPlayer(); 68 | } 69 | } 70 | 71 | void GameStat::Tick() 72 | { 73 | mTimeElapsed++; 74 | } 75 | 76 | PlayerStat::PlayerStat(const std::string &username, int remainingHandCardsNum) 77 | : mUsername(username), mRemainingHandCardsNum(remainingHandCardsNum) {} 78 | 79 | void PlayerStat::UpdateAfterDraw(int number, int indexOfNewlyDrawn) 80 | { 81 | mRemainingHandCardsNum += number; 82 | mDoPlayInLastRound = false; 83 | // only common draw (rather than draw penalty due to +2 / +4) 84 | // has the chance to play the card just drawn immediately 85 | mHasChanceToPlayAfterDraw = (number == 1); 86 | mIndexOfNewlyDrawn = indexOfNewlyDrawn; 87 | } 88 | 89 | void PlayerStat::UpdateAfterSkip() 90 | { 91 | mDoPlayInLastRound = false; 92 | mHasChanceToPlayAfterDraw = false; 93 | } 94 | 95 | void PlayerStat::UpdateAfterPlay(Card card) 96 | { 97 | mRemainingHandCardsNum--; 98 | mDoPlayInLastRound = true; 99 | mLastPlayedCard = card; 100 | mHasChanceToPlayAfterDraw = false; 101 | } 102 | 103 | std::ostream& operator<<(std::ostream& os, const PlayerStat& stat) 104 | { 105 | os << "\t { " << stat.mUsername << ", " << stat.mRemainingHandCardsNum; 106 | if (stat.mDoPlayInLastRound) { 107 | os << ", " << stat.mLastPlayedCard; 108 | } 109 | os << " }"; 110 | return os; 111 | } 112 | 113 | }} -------------------------------------------------------------------------------- /src/game/stat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "info.h" 4 | #include "../common/util.h" 5 | 6 | namespace UNO { namespace Game { 7 | 8 | using namespace Network; 9 | 10 | class GameStat { 11 | public: 12 | /// constructor for \c Player 13 | GameStat(const GameStartInfo &info); 14 | 15 | /// constructor for \c GameBoard 16 | GameStat(int firstPlayer, Card flippedCard); 17 | 18 | void NextPlayer(); 19 | 20 | void UpdateAfterDraw(); 21 | 22 | void UpdateAfterSkip(); 23 | 24 | void UpdateAfterPlay(Card card); 25 | 26 | void Tick(); 27 | 28 | bool IsMyTurn() const { return mCurrentPlayer == 0; } 29 | 30 | bool IsSkipped() const { return mLastPlayedCard.mText == CardText::SKIP; } 31 | 32 | int GetCurrentPlayer() const { return mCurrentPlayer; } 33 | 34 | bool IsInClockwise() const { return mIsInClockwise; } 35 | 36 | bool DoesGameEnd() const { return mGameEnds; } 37 | 38 | int GetTimeElapsed() const { return mTimeElapsed; } 39 | 40 | Card GetLastPlayedCard() const { return mLastPlayedCard; } 41 | 42 | int GetCardsNumToDraw() const { return mCardsNumToDraw; } 43 | 44 | void GameEnds() { mGameEnds = true; mCurrentPlayer = -1; } 45 | 46 | void Reverse() { mIsInClockwise = !mIsInClockwise; } 47 | 48 | // for tests 49 | void SetCurrentPlayer(int currentPlayer) { mCurrentPlayer = currentPlayer; } 50 | 51 | void SetIsInClockwise(bool isInClockwise) { mIsInClockwise = isInClockwise; } 52 | 53 | void SetLastPlayedCard(Card lastPlayedCard) { mLastPlayedCard = lastPlayedCard; } 54 | 55 | void SetCardsNumToDraw(int cardsNumToDraw) { mCardsNumToDraw = cardsNumToDraw; } 56 | 57 | private: 58 | int mCurrentPlayer; 59 | bool mIsInClockwise; 60 | bool mGameEnds{false}; 61 | int mTimeElapsed{0}; 62 | 63 | // currently the two fields below are not used by GameStat of GameBoard 64 | Card mLastPlayedCard{}; 65 | int mCardsNumToDraw{1}; // +2 and +4 can accumulate 66 | }; 67 | 68 | class PlayerStat { 69 | public: 70 | PlayerStat() {} 71 | explicit PlayerStat(const std::string &username, int remainingHandCardsNum); 72 | 73 | void UpdateAfterDraw(int number, int indexOfNewlyDrawn = -1); 74 | 75 | void UpdateAfterSkip(); 76 | 77 | void UpdateAfterPlay(Card card); 78 | 79 | std::string GetUsername() const { return mUsername; } 80 | 81 | int GetRemainingHandCardsNum() const { return mRemainingHandCardsNum; } 82 | 83 | bool DoPlayInLastRound() const { return mDoPlayInLastRound; } 84 | 85 | Card GetLastPlayedCard() const { return mLastPlayedCard; } 86 | 87 | bool HasChanceToPlayAfterDraw() const { return mHasChanceToPlayAfterDraw; } 88 | 89 | int GetIndexOfNewlyDrawn() const { return mIndexOfNewlyDrawn; } 90 | 91 | // for test 92 | void SetLastPlayedCard(Card lastPlayedCard) { mLastPlayedCard = lastPlayedCard; } 93 | 94 | private: 95 | const std::string mUsername; 96 | 97 | int mRemainingHandCardsNum; 98 | bool mDoPlayInLastRound{false}; 99 | Card mLastPlayedCard{}; 100 | bool mHasChanceToPlayAfterDraw{false}; 101 | int mIndexOfNewlyDrawn{-1}; 102 | 103 | friend std::ostream& operator<<(std::ostream& os, const PlayerStat& stat); 104 | }; 105 | 106 | }} -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #ifdef ENABLE_LOG 5 | #include 6 | #include 7 | #endif 8 | 9 | #include "network/server.h" 10 | #include "network/client.h" 11 | #include "game/game_board.h" 12 | #include "game/player.h" 13 | #include "common/util.h" 14 | #include "common/config.h" 15 | 16 | using namespace UNO; 17 | 18 | int main(int argc, char **argv) 19 | { 20 | auto configInfo = Common::Config(argc, const_cast(argv)).Parse(); 21 | 22 | #ifdef ENABLE_LOG 23 | spdlog::set_level(spdlog::level::info); 24 | spdlog::set_default_logger(spdlog::basic_logger_mt("UNO", configInfo->mLogPath)); 25 | spdlog::default_logger()->flush_on(spdlog::level::info); 26 | spdlog::info("hello, spdlog"); 27 | #endif 28 | 29 | if (configInfo->mIsServer) { 30 | auto serverSp = Game::GameBoard::CreateServer(configInfo->mPort); 31 | Game::GameBoard gameBoard(serverSp); 32 | gameBoard.Start(); 33 | } 34 | else { 35 | auto clientSp = Game::Player::CreateClient(configInfo->mHost, configInfo->mPort); 36 | Game::Player player(configInfo->mUsername, clientSp); 37 | player.Start(); 38 | } 39 | 40 | return 0; 41 | } -------------------------------------------------------------------------------- /src/network/client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "client.h" 5 | 6 | namespace UNO { namespace Network { 7 | 8 | using asio::ip::tcp; 9 | 10 | Client::Client(std::string host, std::string port) 11 | : mHost(host), mPort(port) 12 | {} 13 | 14 | void Client::Connect() 15 | { 16 | tcp::resolver resolver(mContext); 17 | auto endpoints = resolver.resolve(mHost, mPort); 18 | 19 | while (mShouldReset) { 20 | mShouldReset = false; 21 | tcp::socket socket(mContext); 22 | asio::async_connect(socket, endpoints, 23 | [this, &socket](std::error_code ec, tcp::endpoint) { 24 | if (!ec) { 25 | mSession = std::make_unique(std::move(socket)); 26 | } 27 | } 28 | ); 29 | 30 | mContext.run(); 31 | if (!mSession) { 32 | // connection failure 33 | std::cout << "Service not found, connection failure." << std::endl; 34 | std::exit(-1); 35 | } 36 | OnConnect(); 37 | } 38 | } 39 | 40 | void Client::Reset() 41 | { 42 | mShouldReset = true; 43 | mContext.restart(); 44 | } 45 | 46 | std::unique_ptr Client::ReceiveInfo(const std::type_info *infoType) 47 | { 48 | using funcType = std::function()>; 49 | static std::map mapping{ 50 | {&typeid(JoinGameInfo), [this] { return ReceiveInfoImpl(); }}, 51 | {&typeid(JoinGameRspInfo), [this] { return ReceiveInfoImpl(); }}, 52 | {&typeid(GameStartInfo), [this] { return ReceiveInfoImpl(); }}, 53 | {&typeid(ActionInfo), [this] { return ReceiveInfoImpl(); }}, 54 | {&typeid(DrawInfo), [this] { return ReceiveInfoImpl(); }}, 55 | {&typeid(SkipInfo), [this] { return ReceiveInfoImpl(); }}, 56 | {&typeid(PlayInfo), [this] { return ReceiveInfoImpl(); }}, 57 | {&typeid(DrawRspInfo), [this] { return ReceiveInfoImpl(); }} 58 | }; 59 | 60 | auto it = mapping.find(infoType); 61 | assert(it != mapping.end()); 62 | std::unique_ptr info; 63 | try { 64 | info = it->second(); 65 | } 66 | catch (const std::exception &e) { 67 | /// TODO: handle the condition that server has shutdown 68 | std::cout << "oops, server has shutdown" << std::endl; 69 | std::exit(-1); 70 | } 71 | return info; 72 | } 73 | 74 | void Client::DeliverInfo(const std::type_info *infoType, const Info &info) 75 | { 76 | using funcType = std::function; 77 | static std::map mapping{ 78 | {&typeid(JoinGameInfo), [this](const Info &info) { 79 | DeliverInfoImpl(dynamic_cast(info)); 80 | }}, 81 | {&typeid(JoinGameRspInfo), [this](const Info &info) { 82 | DeliverInfoImpl(dynamic_cast(info)); 83 | }}, 84 | {&typeid(GameStartInfo), [this](const Info &info) { 85 | DeliverInfoImpl(dynamic_cast(info)); 86 | }}, 87 | {&typeid(ActionInfo), [this](const Info &info) { 88 | DeliverInfoImpl(dynamic_cast(info)); 89 | }}, 90 | {&typeid(DrawInfo), [this](const Info &info) { 91 | DeliverInfoImpl(dynamic_cast(info)); 92 | }}, 93 | {&typeid(SkipInfo), [this](const Info &info) { 94 | DeliverInfoImpl(dynamic_cast(info)); 95 | }}, 96 | {&typeid(PlayInfo), [this](const Info &info) { 97 | DeliverInfoImpl(dynamic_cast(info)); 98 | }}, 99 | {&typeid(DrawRspInfo), [this](const Info &info) { 100 | DeliverInfoImpl(dynamic_cast(info)); 101 | }} 102 | }; 103 | auto it = mapping.find(infoType); 104 | assert(it != mapping.end()); 105 | return it->second(info); 106 | } 107 | }} -------------------------------------------------------------------------------- /src/network/client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "session.h" 7 | 8 | namespace UNO { namespace Network { 9 | 10 | using asio::ip::tcp; 11 | using namespace Game; 12 | 13 | class IClient { 14 | public: 15 | virtual void Connect() = 0; 16 | 17 | virtual void Reset() = 0; 18 | 19 | virtual void RegisterConnectCallback(const std::function &callback) = 0; 20 | 21 | virtual std::unique_ptr ReceiveInfo(const std::type_info *infoType) = 0; 22 | 23 | virtual void DeliverInfo(const std::type_info *infoType, const Info &info) = 0; 24 | }; 25 | 26 | class Client : public IClient { 27 | public: 28 | explicit Client(std::string host, std::string port); 29 | 30 | void Connect() override; 31 | 32 | void Reset() override; 33 | 34 | void RegisterConnectCallback(const std::function &callback) override { 35 | OnConnect = callback; 36 | } 37 | 38 | std::unique_ptr ReceiveInfo(const std::type_info *infoType) override; 39 | 40 | void DeliverInfo(const std::type_info *infoType, const Info &info) override; 41 | 42 | private: 43 | template 44 | std::unique_ptr ReceiveInfoImpl() { 45 | return mSession->ReceiveInfo(); 46 | } 47 | 48 | template 49 | void DeliverInfoImpl(const InfoT &info) { 50 | mSession->DeliverInfo(info); 51 | } 52 | 53 | private: 54 | std::function OnConnect; 55 | 56 | private: 57 | const std::string mHost; 58 | const std::string mPort; 59 | 60 | asio::io_context mContext; 61 | std::unique_ptr mSession; 62 | 63 | bool mShouldReset{true}; 64 | }; 65 | }} -------------------------------------------------------------------------------- /src/network/msg.cpp: -------------------------------------------------------------------------------- 1 | #include "msg.h" 2 | 3 | namespace UNO { namespace Network { 4 | 5 | std::ostream& operator<<(std::ostream& os, const ActionType& type) 6 | { 7 | std::string typeStr; 8 | switch (type) { 9 | case ActionType::DRAW: typeStr = "DRAW"; break; 10 | case ActionType::SKIP: typeStr = "SKIP"; break; 11 | case ActionType::PLAY: typeStr = "PLAY"; break; 12 | default: assert(0); 13 | } 14 | 15 | os << typeStr; 16 | return os; 17 | } 18 | }} -------------------------------------------------------------------------------- /src/network/msg.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../game/cards.h" 4 | 5 | namespace UNO { namespace Network { 6 | 7 | using namespace Game; 8 | 9 | enum class MsgType : uint8_t { 10 | JOIN_GAME, 11 | JOIN_GAME_RSP, 12 | GAME_START, 13 | ACTION, 14 | DRAW_RSP, 15 | GAME_END 16 | }; 17 | 18 | struct Msg { 19 | MsgType mType; 20 | int mLen; // **not** include the mType and mLen itself 21 | }; 22 | 23 | struct JoinGameMsg : public Msg { 24 | char mUsername[]; 25 | }; 26 | 27 | struct JoinGameRspMsg : public Msg { 28 | int mPlayerNum; 29 | // including player himself 30 | char mUsernames[]; 31 | }; 32 | 33 | struct GameStartMsg : public Msg { 34 | Card mInitHandCards[7]; 35 | Card mFlippedCard; // indicating the first card that should be played 36 | int mFirstPlayer; // the index of the first player to play a card 37 | // usernames of all players, not including player himself, ' ' as delimiter 38 | // and the order is from left side of the player to right side 39 | char mUsernames[]; 40 | }; 41 | 42 | enum class ActionType : uint8_t { 43 | DRAW, 44 | SKIP, 45 | PLAY 46 | }; 47 | std::ostream& operator<<(std::ostream& os, const ActionType& type); 48 | 49 | struct ActionMsg : public Msg { 50 | ActionType mActionType; 51 | int mPlayerIndex; 52 | }; 53 | 54 | struct DrawMsg : public ActionMsg { 55 | int mNumber; // the number of cards to draw 56 | }; 57 | 58 | struct SkipMsg : public ActionMsg { 59 | }; 60 | 61 | struct PlayMsg : public ActionMsg { 62 | Card mCard; 63 | CardColor mNextColor; // valid only if mCard is black 64 | }; 65 | 66 | struct DrawRspMsg : public Msg { 67 | int mNumber; 68 | Card mCards[]; 69 | }; 70 | 71 | struct GameEndMsg : public Msg { 72 | int mWinner; 73 | }; 74 | }} -------------------------------------------------------------------------------- /src/network/server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "server.h" 5 | 6 | namespace UNO { namespace Network { 7 | 8 | using asio::ip::tcp; 9 | 10 | Server::Server(std::string port) : mPort(port) 11 | {} 12 | 13 | void Server::Run() 14 | { 15 | tcp::endpoint endpoint(tcp::v4(), std::atoi(mPort.c_str())); 16 | 17 | mAcceptor = std::make_unique(mContext, endpoint); 18 | while (mShouldReset) { 19 | mShouldReset = false; 20 | Accept(); 21 | mContext.run(); 22 | 23 | std::cout << "All players have joined. Game Start!" << std::endl; 24 | OnAllPlayersJoined(); 25 | Close(); 26 | } 27 | } 28 | 29 | void Server::Accept() 30 | { 31 | mAcceptor->async_accept([this](std::error_code ec, tcp::socket socket) { 32 | if (!ec) { 33 | // a new player joins in 34 | int index = mSessions.size(); 35 | std::cout << "a new player joins in, index : " << index << std::endl; 36 | 37 | mSessions.push_back(std::make_unique(std::move(socket))); 38 | std::unique_ptr info = mSessions.back()->ReceiveInfo(); 39 | OnReceiveJoinGameInfo(index, *info); 40 | } 41 | if (mSessions.size() < Common::Common::mPlayerNum) { 42 | Accept(); 43 | } 44 | }); 45 | } 46 | 47 | void Server::Close() 48 | { 49 | mAcceptor->cancel(); 50 | mSessions.clear(); 51 | } 52 | 53 | void Server::Reset() 54 | { 55 | mShouldReset = true; 56 | // a invokation to restart is needed for subsequent run 57 | mContext.restart(); 58 | } 59 | 60 | std::unique_ptr Server::ReceiveInfo(const std::type_info *infoType, int index) 61 | { 62 | using funcType = std::function(int)>; 63 | static std::map mapping{ 64 | {&typeid(JoinGameInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 65 | {&typeid(JoinGameRspInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 66 | {&typeid(GameStartInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 67 | {&typeid(ActionInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 68 | {&typeid(DrawInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 69 | {&typeid(SkipInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 70 | {&typeid(PlayInfo), [this](int index) { return ReceiveInfoImpl(index); }}, 71 | {&typeid(DrawRspInfo), [this](int index) { return ReceiveInfoImpl(index); }} 72 | }; 73 | auto it = mapping.find(infoType); 74 | assert(it != mapping.end()); 75 | return it->second(index); 76 | } 77 | 78 | void Server::DeliverInfo(const std::type_info *infoType, int index, const Info &info) 79 | { 80 | using funcType = std::function; 81 | static std::map mapping{ 82 | {&typeid(JoinGameInfo), [this](int index, const Info &info) { 83 | DeliverInfoImpl(index, dynamic_cast(info)); 84 | }}, 85 | {&typeid(JoinGameRspInfo), [this](int index, const Info &info) { 86 | DeliverInfoImpl(index, dynamic_cast(info)); 87 | }}, 88 | {&typeid(GameStartInfo), [this](int index, const Info &info) { 89 | DeliverInfoImpl(index, dynamic_cast(info)); 90 | }}, 91 | {&typeid(ActionInfo), [this](int index, const Info &info) { 92 | DeliverInfoImpl(index, dynamic_cast(info)); 93 | }}, 94 | {&typeid(DrawInfo), [this](int index, const Info &info) { 95 | DeliverInfoImpl(index, dynamic_cast(info)); 96 | }}, 97 | {&typeid(SkipInfo), [this](int index, const Info &info) { 98 | DeliverInfoImpl(index, dynamic_cast(info)); 99 | }}, 100 | {&typeid(PlayInfo), [this](int index, const Info &info) { 101 | DeliverInfoImpl(index, dynamic_cast(info)); 102 | }}, 103 | {&typeid(DrawRspInfo), [this](int index, const Info &info) { 104 | DeliverInfoImpl(index, dynamic_cast(info)); 105 | }} 106 | }; 107 | auto it = mapping.find(infoType); 108 | assert(it != mapping.end()); 109 | return it->second(index, info); 110 | } 111 | 112 | }} -------------------------------------------------------------------------------- /src/network/server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "session.h" 7 | 8 | namespace UNO { namespace Network { 9 | 10 | using asio::ip::tcp; 11 | using namespace Game; 12 | 13 | class IServer { 14 | public: 15 | virtual void Run() = 0; 16 | 17 | virtual void Close() = 0; 18 | 19 | virtual void Reset() = 0; 20 | 21 | virtual void RegisterReceiveJoinGameInfoCallback( 22 | const std::function &callback) = 0; 23 | 24 | virtual void RegisterAllPlayersJoinedCallback(const std::function &callback) = 0; 25 | 26 | virtual std::unique_ptr ReceiveInfo(const std::type_info *infoType, int index) = 0; 27 | 28 | virtual void DeliverInfo(const std::type_info *infoType, int index, const Info &info) = 0; 29 | }; 30 | 31 | class Server : public IServer { 32 | public: 33 | explicit Server(std::string port); 34 | 35 | void Run() override; 36 | 37 | void Close() override; 38 | 39 | void Reset() override; 40 | 41 | void RegisterReceiveJoinGameInfoCallback( 42 | const std::function &callback) override { 43 | OnReceiveJoinGameInfo = callback; 44 | } 45 | 46 | void RegisterAllPlayersJoinedCallback(const std::function &callback) override { 47 | OnAllPlayersJoined = callback; 48 | } 49 | 50 | std::unique_ptr ReceiveInfo(const std::type_info *infoType, int index) override; 51 | 52 | void DeliverInfo(const std::type_info *infoType, int index, const Info &info) override; 53 | 54 | private: 55 | void Accept(); 56 | 57 | template 58 | std::unique_ptr ReceiveInfoImpl(int index) { 59 | return mSessions[index]->ReceiveInfo(); 60 | } 61 | 62 | template 63 | void DeliverInfoImpl(int index, const InfoT &info) { 64 | mSessions[index]->DeliverInfo(info); 65 | } 66 | 67 | private: 68 | // callbacks in server side should always take index of session as the first parameter 69 | std::function OnReceiveJoinGameInfo; 70 | 71 | std::function OnAllPlayersJoined; 72 | 73 | private: 74 | const std::string mPort; 75 | 76 | asio::io_context mContext; 77 | std::unique_ptr mAcceptor; 78 | std::vector> mSessions; 79 | 80 | bool mShouldReset{true}; 81 | }; 82 | }} -------------------------------------------------------------------------------- /src/network/session.cpp: -------------------------------------------------------------------------------- 1 | #include "session.h" 2 | 3 | namespace UNO { namespace Network { 4 | 5 | Session::Session(tcp::socket socket) : mSocket(std::move(socket)) {} 6 | 7 | /** 8 | * Read will throw end-of-file exception if the corresponding client has disconnected 9 | */ 10 | void Session::Read() 11 | { 12 | std::memset(mReadBuffer, 0, MAX_BUFFER_SIZE); 13 | // read header 14 | asio::read(mSocket, asio::buffer(mReadBuffer, sizeof(Msg))); 15 | 16 | // read body 17 | int len = reinterpret_cast(mReadBuffer)->mLen; 18 | asio::read(mSocket, asio::buffer(mReadBuffer + sizeof(Msg), len)); 19 | } 20 | 21 | void Session::Write() 22 | { 23 | Msg *msg = reinterpret_cast(mWriteBuffer); 24 | int len = sizeof(Msg) + msg->mLen; 25 | asio::async_write(mSocket, asio::buffer(msg, len), 26 | [this](std::error_code, std::size_t) { 27 | std::memset(mWriteBuffer, 0, MAX_BUFFER_SIZE); 28 | } 29 | ); 30 | } 31 | 32 | }} -------------------------------------------------------------------------------- /src/network/session.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../game/info.h" 7 | 8 | namespace UNO { 9 | 10 | namespace Test { 11 | class SessionFixture; 12 | } 13 | 14 | namespace Network { 15 | 16 | using asio::ip::tcp; 17 | 18 | class Session { 19 | public: 20 | explicit Session(tcp::socket socket); 21 | 22 | template 23 | std::unique_ptr ReceiveInfo() { 24 | Read(); 25 | return InfoT::Deserialize(mReadBuffer); 26 | } 27 | 28 | template 29 | void DeliverInfo(const InfoT &info) { 30 | info.Serialize(mWriteBuffer); 31 | Write(); 32 | } 33 | 34 | // for test 35 | template 36 | void DeliverInfo(Types&&... args) { 37 | InfoT info(args...); 38 | info.Serialize(mWriteBuffer); 39 | Write(); 40 | } 41 | 42 | private: 43 | // read from mSocket to mReadBuffer 44 | void Read(); 45 | 46 | // write from mWriteBuffer to mSocket 47 | void Write(); 48 | 49 | private: 50 | constexpr static int MAX_BUFFER_SIZE = 256; 51 | 52 | tcp::socket mSocket; 53 | uint8_t mReadBuffer[MAX_BUFFER_SIZE]; 54 | uint8_t mWriteBuffer[MAX_BUFFER_SIZE]; 55 | 56 | friend class Test::SessionFixture; 57 | }; 58 | }} -------------------------------------------------------------------------------- /src/ui/inputter.cpp: -------------------------------------------------------------------------------- 1 | #include "inputter.h" 2 | 3 | namespace UNO { namespace UI { 4 | 5 | InputAction Inputter::GetAction(int timeout) 6 | { 7 | while (true) { 8 | char ch; 9 | try { 10 | ch = Common::Util::GetCharWithTimeout(timeout, true); 11 | } 12 | catch (std::exception &e) { 13 | // timeout 14 | return InputAction::PASS; 15 | } 16 | 17 | switch (ch) { 18 | case ',': return InputAction::CURSOR_MOVE_LEFT; 19 | case '.': return InputAction::CURSOR_MOVE_RIGHT; 20 | #if defined(__unix__) || defined(__APPLE__) 21 | case '\n': return InputAction::PLAY; 22 | #elif defined(_WIN32) 23 | case '\r': return InputAction::PLAY; 24 | #endif 25 | case ' ': return InputAction::PASS; 26 | case 'q': case 'Q': return InputAction::QUIT; 27 | } 28 | } 29 | } 30 | 31 | Game::CardColor Inputter::SpecifyNextColor(int timeout) 32 | { 33 | while (true) { 34 | char ch; 35 | try { 36 | ch = Common::Util::GetCharWithTimeout(timeout, true); 37 | } 38 | catch (std::exception &e) { 39 | // timeout, red is default 40 | return Game::CardColor::RED; 41 | } 42 | 43 | switch (ch) { 44 | case 'R': case 'r': return Game::CardColor::RED; 45 | case 'Y': case 'y': return Game::CardColor::YELLOW; 46 | case 'G': case 'g': return Game::CardColor::GREEN; 47 | case 'B': case 'b': return Game::CardColor::BLUE; 48 | } 49 | } 50 | } 51 | 52 | }} -------------------------------------------------------------------------------- /src/ui/inputter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../common/util.h" 7 | #include "../game/cards.h" 8 | 9 | namespace UNO { namespace UI { 10 | 11 | enum class InputAction : uint8_t { 12 | CURSOR_MOVE_LEFT, 13 | CURSOR_MOVE_RIGHT, 14 | PLAY, 15 | PASS, // skip or draw 16 | QUIT 17 | }; 18 | 19 | class Inputter { 20 | public: 21 | Inputter() {} 22 | 23 | /** 24 | * Wait for player to input an action with a time limit \param timeout. 25 | */ 26 | InputAction GetAction(int timeout); 27 | 28 | /** 29 | * After the player plays a 'W' or '+4', he needs to specify the next color to turn to. 30 | */ 31 | Game::CardColor SpecifyNextColor(int timeout); 32 | 33 | private: 34 | }; 35 | 36 | }} -------------------------------------------------------------------------------- /src/ui/outputter.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #include 3 | #endif 4 | 5 | #include "outputter.h" 6 | 7 | namespace UNO { namespace UI { 8 | 9 | std::string ColorEscape::RESET; 10 | std::string ColorEscape::RED; 11 | std::string ColorEscape::YELLOW; 12 | std::string ColorEscape::GREEN; 13 | std::string ColorEscape::BLUE; 14 | // const std::string ColorEscape::BLACK = "\033[30m"; 15 | 16 | Outputter::Outputter(std::unique_ptr &gameStat, 17 | std::vector &playerStats, std::unique_ptr &handCards) 18 | : mGameStat(gameStat), mPlayerStats(playerStats), mHandCards(handCards) 19 | { 20 | ColorEscape::RESET = "\033[0m"; 21 | ColorEscape::RED = Common::Common::mRedEscape; 22 | ColorEscape::YELLOW = Common::Common::mYellowEscape; 23 | ColorEscape::GREEN = Common::Common::mGreenEscape; 24 | ColorEscape::BLUE = Common::Common::mBlueEscape; 25 | } 26 | 27 | void Outputter::PrintRawView(const View &view) const 28 | { 29 | ClearScreen(); 30 | auto [height, width] = ViewFormatter::GetBaseScaleOfView(); 31 | for (int row = 0; row < height; row++) { 32 | for (int col = 0; col < width; col++) { 33 | std::cout << view.At(row, col); 34 | } 35 | std::cout << std::endl; 36 | } 37 | } 38 | 39 | void Outputter::PrintView(const View &view, bool useCls) const 40 | { 41 | ClearScreen(useCls); 42 | auto [baseHeight, width] = ViewFormatter::GetBaseScaleOfView(); 43 | int height = baseHeight + view.GetExtraRowNum(); 44 | 45 | auto renderInfos = GetRenderInfos(); 46 | int curRenderIndex = 0; 47 | int charsLeftToReset = 0; 48 | for (int row = 0; row < height; row++) { 49 | for (int col = 0; col < width; col++) { 50 | if (curRenderIndex < renderInfos.size()) { 51 | auto curRenderInfo = renderInfos[curRenderIndex]; 52 | if (row == curRenderInfo.mPos.first && 53 | col == curRenderInfo.mPos.second) 54 | { 55 | std::cout << ToColorEscape(curRenderInfo.mCard.mColor); 56 | charsLeftToReset = curRenderInfo.mCard.Length(); 57 | curRenderIndex++; 58 | } 59 | } 60 | std::cout << view.At(row, col); 61 | charsLeftToReset--; 62 | if (charsLeftToReset == 0) { 63 | std::cout << ColorEscape::RESET; 64 | } 65 | } 66 | std::cout << std::endl; 67 | } 68 | } 69 | 70 | void Outputter::PrintHintText(bool isSpecifyingNextColor, bool lastCardCanBePlayed, 71 | bool hasChanceToPlayAfterDraw) const 72 | { 73 | if (isSpecifyingNextColor) { 74 | std::cout << "Specify the next color (" 75 | << ToColorEscape(CardColor::RED) << "R" << ColorEscape::RESET << "/" 76 | << ToColorEscape(CardColor::YELLOW) << "Y" << ColorEscape::RESET << "/" 77 | << ToColorEscape(CardColor::GREEN) << "G" << ColorEscape::RESET << "/" 78 | << ToColorEscape(CardColor::BLUE) << "B" << ColorEscape::RESET << ")." << std::endl; 79 | } 80 | else if (!lastCardCanBePlayed) { 81 | auto lastPlayedCard = mGameStat->GetLastPlayedCard(); 82 | std::cout << "This card cannot be played. Last played card is " 83 | << ToColorEscape(lastPlayedCard.mColor) 84 | << lastPlayedCard << ColorEscape::RESET << std::endl; 85 | std::cout << "Press , and . to move the cursor and Enter to play the card." << std::endl; 86 | std::cout << "Or press Space to draw cards / skip." << std::endl; 87 | } 88 | else if (!hasChanceToPlayAfterDraw) { 89 | std::cout << "Now it's your turn." << std::endl; 90 | std::cout << "Press , and . to move the cursor and Enter to play the card." << std::endl; 91 | std::cout << "Or press Space to draw cards / skip." << std::endl; 92 | } 93 | else { 94 | std::cout << "Press Enter to play the card just drawn immediately." << std::endl; 95 | std::cout << "Or press Space to turn to the next player." << std::endl; 96 | } 97 | } 98 | 99 | std::string Outputter::ToColorEscape(CardColor color) const 100 | { 101 | switch (color) { 102 | case CardColor::RED: return ColorEscape::RED; 103 | case CardColor::YELLOW: return ColorEscape::YELLOW; 104 | case CardColor::GREEN: return ColorEscape::GREEN; 105 | case CardColor::BLUE: return ColorEscape::BLUE; 106 | // case CardColor::BLACK: return ColorEscape::BLACK; 107 | default: return ColorEscape::RESET; 108 | } 109 | } 110 | 111 | std::vector Outputter::GetRenderInfos() const 112 | { 113 | std::vector renderInfos; 114 | for (int i = 1; i < Common::Common::mPlayerNum; i++) { 115 | renderInfos.emplace_back(ViewFormatter::GetPosOfPlayerLastPlayedCard(i), 116 | mPlayerStats[i].GetLastPlayedCard()); 117 | } 118 | renderInfos.emplace_back(ViewFormatter::GetPosOfLastPlayedCard(), 119 | mGameStat->GetLastPlayedCard()); 120 | for (int i = 0; i < mHandCards->Number(); i++) { 121 | renderInfos.emplace_back(ViewFormatter::GetPosOfHandCard(i, *mHandCards), 122 | mHandCards->At(i)); 123 | } 124 | // 'U' is red, 'N' is yellow, 'O' is green and '!' is blue. 125 | // only color matters, text can be anything here 126 | renderInfos.emplace_back(ViewFormatter::GetPosOfUNOText('U'), 127 | Game::Card(Game::CardColor::RED, Game::CardText::ZERO)); 128 | renderInfos.emplace_back(ViewFormatter::GetPosOfUNOText('N'), 129 | Game::Card(Game::CardColor::YELLOW, Game::CardText::ZERO)); 130 | renderInfos.emplace_back(ViewFormatter::GetPosOfUNOText('O'), 131 | Game::Card(Game::CardColor::GREEN, Game::CardText::ZERO)); 132 | renderInfos.emplace_back(ViewFormatter::GetPosOfUNOText('!'), 133 | Game::Card(Game::CardColor::BLUE, Game::CardText::ZERO)); 134 | 135 | std::sort(renderInfos.begin(), renderInfos.end()); 136 | return renderInfos; 137 | } 138 | 139 | void Outputter::ClearScreen(bool useCls) const 140 | { 141 | #ifdef _WIN32 142 | if (useCls) { 143 | system("cls"); 144 | } 145 | else { 146 | // not use system('cls') for better player experience 147 | static HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); 148 | static COORD coord = {0, 0}; 149 | SetConsoleCursorPosition(hOutput, coord); 150 | } 151 | #else 152 | system("clear"); 153 | #endif 154 | } 155 | 156 | }} -------------------------------------------------------------------------------- /src/ui/outputter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "view.h" 6 | 7 | namespace UNO { namespace UI { 8 | 9 | using namespace Game; 10 | 11 | struct ColorEscape { 12 | static std::string RESET; 13 | static std::string RED; 14 | static std::string YELLOW; 15 | static std::string GREEN; 16 | static std::string BLUE; 17 | // const static std::string BLACK; 18 | }; 19 | 20 | class Outputter { 21 | public: 22 | Outputter(std::unique_ptr &gameStat, 23 | std::vector &playerStats, 24 | std::unique_ptr &handCards); 25 | 26 | /** 27 | * Print view without color rendering. 28 | * Used when waiting for other playes to join. 29 | */ 30 | void PrintRawView(const View &view) const; 31 | 32 | /** 33 | * Print view with color rendering. 34 | */ 35 | void PrintView(const View &view, bool useCls = true) const; 36 | 37 | /** 38 | * Print the hint text shown below the view. Only appear in player's turn. 39 | */ 40 | void PrintHintText(bool isSpecifyingNextColor, bool lastCardCanBePlayed, 41 | bool hasChanceToPlayAfterDraw) const; 42 | 43 | private: 44 | std::vector GetRenderInfos() const; 45 | 46 | std::string ToColorEscape(CardColor color) const; 47 | 48 | void ClearScreen(bool useCls = true) const; 49 | 50 | private: 51 | std::unique_ptr &mGameStat; 52 | std::vector &mPlayerStats; 53 | std::unique_ptr &mHandCards; 54 | }; 55 | }} -------------------------------------------------------------------------------- /src/ui/ui_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "ui_manager.h" 2 | 3 | namespace UNO { namespace UI { 4 | 5 | UIManager::UIManager(std::unique_ptr &gameStat, 6 | std::vector &playerStats, std::unique_ptr &handCards) 7 | : mGameStat(gameStat), mPlayerStats(playerStats), mHandCards(handCards) 8 | { 9 | // ViewFormatter should be init first 10 | ViewFormatter::Init(); 11 | mView.reset(new View()); 12 | mInputter.reset(new Inputter()); 13 | mOutputter.reset(new Outputter(gameStat, playerStats, handCards)); 14 | Common::Util::HideTerminalCursor(); 15 | } 16 | 17 | void UIManager::RunTimerThread() 18 | { 19 | mTimerThreadShouldStop = false; 20 | mTimerThread.reset(new std::thread([this]() { TimerThreadLoop(); })); 21 | } 22 | 23 | void UIManager::StopTimerThread() 24 | { 25 | mTimerThreadShouldStop = true; 26 | // don't forget to join the thread 27 | mTimerThread->join(); 28 | } 29 | 30 | void UIManager::TimerThreadLoop() 31 | { 32 | while (!mTimerThreadShouldStop) { 33 | mGameStat->Tick(); 34 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 35 | 36 | std::lock_guard lock(mMutex); 37 | mView->DrawTimeIndicator(mGameStat->GetCurrentPlayer(), mGameStat->GetTimeElapsed()); 38 | /// XXX: race condition. it may print before main thread prints first time, 39 | /// in which case some states are not initialized yet such as mExtraRowNum of View. 40 | /// current workaround: init mExtraRowNum with 0 41 | Print(false); 42 | } 43 | } 44 | 45 | void UIManager::RenderWhenInitWaiting(const std::vector &usernames, bool isFirstTime) 46 | { 47 | mView->Clear(true); 48 | mView->DrawWhenInitWaiting(usernames, isFirstTime); 49 | mOutputter->PrintRawView(*mView); 50 | } 51 | 52 | void UIManager::Render(bool useCls) 53 | { 54 | std::lock_guard lock(mMutex); 55 | // before render, mView should be cleared first 56 | if (mGameStat->DoesGameEnd()) { 57 | mView->Clear(true); 58 | } 59 | else { 60 | mView->Clear(false, mGameStat->GetCurrentPlayer()); 61 | } 62 | mView->DrawSelfBox(*mGameStat, mPlayerStats[0], *mHandCards, mCursorIndex); 63 | for (int i = 1; i < mPlayerStats.size(); i++) { 64 | mView->DrawOtherBox(i, *mGameStat, mPlayerStats[i]); 65 | } 66 | mView->DrawLastPlayedCard(mGameStat->GetLastPlayedCard()); 67 | 68 | Print(useCls); 69 | } 70 | 71 | void UIManager::NextTurn() 72 | { 73 | if (mGameStat->IsMyTurn()) { 74 | ResetCursor(); 75 | ResetTimeLeft(); 76 | Common::Terminal::ClearStdInBuffer(); 77 | } 78 | 79 | std::lock_guard lock(mMutex); 80 | mView->Clear(true); 81 | } 82 | 83 | void UIManager::Print(bool useCls) const 84 | { 85 | // get value only once, for atomicity 86 | auto isMyTurn = mGameStat->IsMyTurn(); 87 | if (isMyTurn) { 88 | mView->DrawSelfTimeIndicatorIfNot(); 89 | } 90 | mOutputter->PrintView(*mView, useCls); 91 | 92 | if (isMyTurn) { 93 | mOutputter->PrintHintText(mIsSpecifyingNextColor, mLastCardCanBePlayed, 94 | mHasChanceToPlayAfterDraw); 95 | } 96 | } 97 | 98 | std::pair UIManager::GetAction(bool lastCardCanBePlayed, 99 | bool hasChanceToPlayAfterDraw) 100 | { 101 | mLastCardCanBePlayed = lastCardCanBePlayed; 102 | mHasChanceToPlayAfterDraw = hasChanceToPlayAfterDraw; 103 | bool isFirstRender = true; 104 | while (true) { 105 | Render(isFirstRender); 106 | isFirstRender = false; 107 | 108 | InputAction action; 109 | ExecuteWithTimePassing([this, &action] { 110 | action = mInputter->GetAction(mTimeLeft); 111 | }); 112 | 113 | switch (action) { 114 | case InputAction::CURSOR_MOVE_LEFT: { 115 | if (!hasChanceToPlayAfterDraw) { 116 | mCursorIndex = Common::Util::Wrap(mCursorIndex - 1, 117 | mPlayerStats[0].GetRemainingHandCardsNum()); 118 | } 119 | break; 120 | } 121 | case InputAction::CURSOR_MOVE_RIGHT: { 122 | if (!hasChanceToPlayAfterDraw) { 123 | mCursorIndex = Common::Util::Wrap(mCursorIndex + 1, 124 | mPlayerStats[0].GetRemainingHandCardsNum()); 125 | } 126 | break; 127 | } 128 | case InputAction::PLAY: { 129 | int cardIndex = (!hasChanceToPlayAfterDraw) ? mCursorIndex : 130 | (mPlayerStats[0].GetIndexOfNewlyDrawn()); 131 | return std::make_pair(InputAction::PLAY, cardIndex); 132 | } 133 | case InputAction::PASS: { 134 | return std::make_pair(InputAction::PASS, -1); 135 | } 136 | case InputAction::QUIT: { 137 | std::cout << "Bye." << std::endl; 138 | std::exit(0); 139 | } 140 | default: assert(0); 141 | } 142 | } 143 | } 144 | 145 | CardColor UIManager::SpecifyNextColor() 146 | { 147 | mIsSpecifyingNextColor = true; 148 | { 149 | std::lock_guard lock(mMutex); 150 | Print(); 151 | } 152 | 153 | CardColor nextColor; 154 | ExecuteWithTimePassing([this, &nextColor] { 155 | nextColor = mInputter->SpecifyNextColor(mTimeLeft); 156 | }); 157 | 158 | mIsSpecifyingNextColor = false; 159 | return nextColor; 160 | } 161 | 162 | bool UIManager::WantToPlayAgain(const std::string &winner) 163 | { 164 | std::cout << winner << " won. Want to play again? (Y/n) " << std::endl; 165 | while (true) { 166 | char ch; 167 | try { 168 | ch = Common::Util::GetCharWithTimeout(30000, true); 169 | } 170 | catch (const std::exception &e) { 171 | // timeout, regard it as 'N' 172 | ch = 'N'; 173 | } 174 | 175 | switch (ch) { 176 | case 'y': case 'Y': return true; 177 | case 'n': case 'N': return false; 178 | } 179 | } 180 | } 181 | 182 | void UIManager::ExecuteWithTimePassing(const std::function &func) 183 | { 184 | auto startTime = std::chrono::system_clock::now(); 185 | func(); 186 | auto endTime = std::chrono::system_clock::now(); 187 | std::chrono::duration secondsElapsed = endTime - startTime; 188 | mTimeLeft -= static_cast(secondsElapsed.count() * 1000); 189 | // mTimeLeft shouldn't be negative 190 | mTimeLeft = std::max(mTimeLeft, 0); 191 | } 192 | 193 | }} 194 | 195 | -------------------------------------------------------------------------------- /src/ui/ui_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "view.h" 10 | #include "inputter.h" 11 | #include "outputter.h" 12 | 13 | namespace UNO { namespace UI { 14 | 15 | using namespace Game; 16 | 17 | class UIManager { 18 | public: 19 | UIManager(std::unique_ptr &gameStat, 20 | std::vector &playerStats, 21 | std::unique_ptr &handCards); 22 | 23 | // ~UIManager() { mTimerThread->join(); } 24 | 25 | /** 26 | * Start the timer thread. 27 | */ 28 | void RunTimerThread(); 29 | 30 | /** 31 | * Stop the timer thread and wait for joining. 32 | */ 33 | void StopTimerThread(); 34 | 35 | /** 36 | * Render view when waiting for other players to join. 37 | */ 38 | void RenderWhenInitWaiting(const std::vector &usernames, bool isFirstTime); 39 | 40 | /** 41 | * Render view in the main game loop. 42 | */ 43 | void Render(bool useCls = true); 44 | 45 | /** 46 | * Get the player's action. 47 | */ 48 | std::pair GetAction(bool lastCardCanBePlayed, 49 | bool hasChanceToPlayAfterDraw); 50 | 51 | /** 52 | * Get the specified next color when a 'W' or '+4' is played. 53 | */ 54 | CardColor SpecifyNextColor(); 55 | 56 | /** 57 | * Ask player whether want to play again. 58 | */ 59 | bool WantToPlayAgain(const std::string &winner); 60 | 61 | /** 62 | * Hook invoked when the game enters next turn. 63 | */ 64 | void NextTurn(); 65 | 66 | /** 67 | * Move the cursor in handcard to the position indicated by \param index. 68 | */ 69 | void MoveCursorTo(int index) { mCursorIndex = index; } 70 | 71 | private: 72 | void TimerThreadLoop(); 73 | 74 | void Print(bool useCls = true) const; 75 | 76 | void ResetCursor() { MoveCursorTo(0); } 77 | 78 | void ResetTimeLeft() { mTimeLeft = Common::Common::mTimeoutPerTurn * 1000 + 500; } 79 | 80 | int PlayerNum() const { return mPlayerStats.size(); } 81 | 82 | void ExecuteWithTimePassing(const std::function &func); 83 | 84 | private: 85 | std::unique_ptr mView; 86 | std::unique_ptr mInputter; 87 | std::unique_ptr mOutputter; 88 | 89 | std::unique_ptr &mGameStat; 90 | std::vector &mPlayerStats; 91 | std::unique_ptr &mHandCards; 92 | int mCursorIndex{0}; 93 | int mTimeLeft; // in milliseconds 94 | 95 | std::unique_ptr mTimerThread; 96 | 97 | bool mLastCardCanBePlayed; 98 | bool mHasChanceToPlayAfterDraw; 99 | bool mIsSpecifyingNextColor{false}; 100 | 101 | bool mTimerThreadShouldStop{false}; 102 | // when printing view, it shouldn't be modified 103 | std::mutex mMutex; 104 | }; 105 | }} -------------------------------------------------------------------------------- /src/ui/view.cpp: -------------------------------------------------------------------------------- 1 | #include "view.h" 2 | 3 | namespace UNO { namespace UI { 4 | 5 | const std::string View::CARDS_REMAINED_STR = "cards remained: "; 6 | const std::string View::LAST_PLAYED_STR = "last played: "; 7 | const std::string View::HAND_CARDS_STR = "handcards: "; 8 | const std::string View::UNO_STR = "UNO!"; 9 | 10 | View::View() 11 | { 12 | auto [height, width] = ViewFormatter::GetMaxScaleOfView(); 13 | mView.resize(height); 14 | for (auto &row : mView) { 15 | row.resize(width); 16 | } 17 | 18 | Clear(true); 19 | } 20 | 21 | void View::Clear(bool doClearIndicator, int currentPlayer) 22 | { 23 | int rowNumNotToClear = -1; 24 | if (!doClearIndicator) { 25 | if (currentPlayer != 0) { 26 | rowNumNotToClear = ViewFormatter::GetPosOfPlayerBox(currentPlayer).first + 6; 27 | } 28 | else { 29 | rowNumNotToClear = ViewFormatter::GetPosOfPlayerBox(0).first + 5 + mExtraRowNum; 30 | } 31 | } 32 | 33 | for (int row = 0; row < mView.size(); row++) { 34 | if (row != rowNumNotToClear) { 35 | for (auto &c : mView[row]) { 36 | c = ' '; 37 | } 38 | } 39 | } 40 | } 41 | 42 | void View::DrawWhenInitWaiting(const std::vector &usernames, bool isFirstTime) 43 | { 44 | if (isFirstTime) { 45 | mMyIndex = usernames.size() - 1; 46 | } 47 | for (int playerIndex = 0; playerIndex < Common::Common::mPlayerNum; playerIndex++) { 48 | auto [row, col] = ViewFormatter::GetPosOfPlayerBox(playerIndex); 49 | auto [height, width] = ViewFormatter::GetBaseScaleOfBox(playerIndex); 50 | int curNum = usernames.size(); 51 | int absoluteIndex = Common::Util::WrapWithPlayerNum(playerIndex + mMyIndex); 52 | if (absoluteIndex >= curNum) { 53 | // username unknown yet 54 | DrawBorder(row, col, width, height - 2); 55 | } 56 | else { 57 | DrawBorderAndUsername(row, col, width, height - 2, usernames[absoluteIndex]); 58 | } 59 | if (playerIndex == 0) { 60 | AlignCenter(row + 3, col, width, "Waiting for players to join..."); 61 | } 62 | else { 63 | Copy(row + 3, col + 2, CARDS_REMAINED_STR); 64 | Copy(row + 4, col + 2, LAST_PLAYED_STR); 65 | } 66 | } 67 | } 68 | 69 | void View::DrawOtherBox(int playerIndex, const GameStat &gameStat, const PlayerStat &playerStat) 70 | { 71 | bool isCurrentPlayer = false; 72 | auto [row, col] = ViewFormatter::GetPosOfPlayerBox(playerIndex); 73 | auto [height, width] = ViewFormatter::GetBaseScaleOfBox(playerIndex); 74 | DrawBorderAndUsername(row, col, width, height - 2, playerStat.GetUsername()); 75 | if (gameStat.GetCurrentPlayer() == playerIndex) { 76 | isCurrentPlayer = true; 77 | } 78 | // if (isCurrentPlayer) { 79 | // mView[row + 1][col + OTHER_BOX_WIDTH - 3] = '*'; 80 | // } 81 | 82 | Copy(row + 3, col + 2, CARDS_REMAINED_STR); 83 | auto remainingHandCardsNum = playerStat.GetRemainingHandCardsNum(); 84 | Copy(row + 3, col + 2 + CARDS_REMAINED_STR.size(), std::to_string(remainingHandCardsNum)); 85 | if (remainingHandCardsNum == 1) { 86 | DrawUNO(); 87 | } 88 | 89 | Copy(row + 4, col + 2, LAST_PLAYED_STR); 90 | if (playerStat.DoPlayInLastRound()) { 91 | Copy(row + 4, col + 2 + LAST_PLAYED_STR.size(), playerStat.GetLastPlayedCard().ToString()); 92 | } 93 | } 94 | 95 | void View::DrawSelfBox(const GameStat &gameStat, const PlayerStat &playerStat, 96 | const HandCards &handcards, int cursorIndex) 97 | { 98 | // update mExtraRowNum first 99 | mExtraRowNum = Common::Util::GetSegmentNum(handcards.Number()) - 1; 100 | auto [row, col] = ViewFormatter::GetPosOfPlayerBox(0); 101 | auto [height, width] = ViewFormatter::GetBaseScaleOfBox(0); 102 | // int height = GetSelfBoxHeight(); 103 | DrawBorderAndUsername(row, col, width, height - 2 + mExtraRowNum, playerStat.GetUsername()); 104 | DrawHandCards(row, col, width, handcards); 105 | if (handcards.Number() == 1) { 106 | DrawUNO(); 107 | } 108 | 109 | if (gameStat.IsMyTurn()) { 110 | // mView[row + 1][col + width - 3] = '*'; 111 | // show cursor only in the turn of player himself 112 | auto [cardRow, cardCol] = ViewFormatter::GetPosOfHandCard(cursorIndex, handcards); 113 | mView[cardRow][cardCol - 1] = '>'; 114 | } 115 | } 116 | 117 | void View::DrawSelfTimeIndicatorIfNot() 118 | { 119 | auto [row, col] = ViewFormatter::GetPosOfPlayerBox(0); 120 | if (mView[row + 5 + mExtraRowNum][col] != '[') { 121 | DrawTimeIndicator(0, 0); 122 | } 123 | } 124 | 125 | void View::DrawLastPlayedCard(Card lastPlayedCard) 126 | { 127 | auto [row, col] = ViewFormatter::GetPosOfLastPlayedCard(); 128 | Copy(row, col, lastPlayedCard.ToString()); 129 | } 130 | 131 | void View::DrawUNO() 132 | { 133 | auto [row, col] = ViewFormatter::GetPosOfUNOText('U'); 134 | Copy(row, col, UNO_STR); 135 | } 136 | 137 | void View::DrawTimeIndicator(int currentPlayer, int timeElapsed) 138 | { 139 | auto [row, col] = ViewFormatter::GetPosOfPlayerBox(currentPlayer); 140 | std::string indicator(18, ' '); 141 | indicator.front() = '['; 142 | indicator.back() = ']'; 143 | indicator.replace(1, timeElapsed, timeElapsed, '='); 144 | indicator[timeElapsed + 1] = '>'; 145 | 146 | if (currentPlayer != 0) { 147 | Copy(row + 6, col, indicator); 148 | } 149 | else { 150 | Copy(row + 5 + mExtraRowNum, col, indicator); 151 | } 152 | } 153 | 154 | void View::DrawHandCards(int row, int col, int width, const HandCards &handcards) 155 | { 156 | if (handcards.Number() > 0) { 157 | for (int i = 0; i < Common::Util::GetSegmentNum(handcards.Number()); i++) { 158 | AlignCenter(row + 3 + i, col, width, handcards.ToStringBySegment(i)); 159 | } 160 | } 161 | else { 162 | // you have won 163 | AlignCenter(row + 3, col, width, "You win!"); 164 | } 165 | } 166 | 167 | void View::DrawBorder(int row, int col, int width, int height) 168 | { 169 | DrawHorizontalBorder(row, col, width); 170 | DrawHorizontalBorder(row + height + 1, col, width); 171 | DrawVerticalBorder(row + 1, col, height); 172 | DrawVerticalBorder(row + 1, col + width - 1, height); 173 | DrawHorizontalBorder(row + 2, col, width); 174 | } 175 | 176 | /** 177 | * draw the border and username of a box, whose top left corner is located by \p row and \p col, 178 | * with width (including '+' at both sides) of \p width, height (not including '+' at both sides) 179 | * of \p height and username of \p username. 180 | */ 181 | void View::DrawBorderAndUsername(int row, int col, int width, int height, const std::string &username) 182 | { 183 | DrawBorder(row, col, width, height); 184 | AlignCenter(row + 1, col, width, username); 185 | } 186 | 187 | /** 188 | * draw a horizontal border which starts at the position located by \p row and \p col, 189 | * and has a length of \p length, including '+' at both sides 190 | */ 191 | void View::DrawHorizontalBorder(int row, int col, int length) 192 | { 193 | assert(length > 2); 194 | std::string border = "++"; 195 | border.insert(1, length - 2, '-'); 196 | Copy(row, col, border); 197 | } 198 | 199 | /** 200 | * draw a vertical border which starts at the position located by \p row and \p col, 201 | * and has a height of \p height, not including '+' at both sides 202 | */ 203 | void View::DrawVerticalBorder(int row, int col, int height) 204 | { 205 | for (int i = 0; i < height; i++) { 206 | mView[row + i][col] = '|'; 207 | } 208 | } 209 | 210 | void View::AlignCenter(int row, int col, int width, const std::string &src) 211 | { 212 | int indent = (width - src.size()) / 2; 213 | Copy(row, col + indent, src); 214 | } 215 | 216 | /** 217 | * copy the string \p src to the position in view located by \p row and \p col. 218 | * note that we use `strncpy` here instead of `strcpy`, that is because `strcpy` 219 | * copys the terminated-null too, which will cover the space following the string. 220 | * with `strncpy`, we can copy the string without the terminated-null. 221 | */ 222 | void View::Copy(int row, int col, const std::string &src) 223 | { 224 | std::strncpy(mView[row].data() + col, src.c_str(), src.size()); 225 | } 226 | 227 | std::ostream& operator<<(std::ostream& os, const View& view) 228 | { 229 | int rowNum = ViewFormatter::GetBaseScaleOfView().first + view.mExtraRowNum; 230 | std::for_each(view.mView.begin(), std::next(view.mView.begin(), rowNum), 231 | [&os](const std::vector &row) { 232 | for (auto c : row) { 233 | os << c; 234 | } 235 | os << std::endl; 236 | } 237 | ); 238 | return os; 239 | } 240 | 241 | }} -------------------------------------------------------------------------------- /src/ui/view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../game/stat.h" 8 | #include "view_formatter.h" 9 | 10 | namespace UNO { namespace UI { 11 | 12 | using namespace Game; 13 | 14 | class View { 15 | public: 16 | View(); 17 | 18 | /** 19 | * Clear the view. If \param doClearIndicator is true, 20 | * do not clear the line where \param currentPlayer 's time indicator positioned. 21 | */ 22 | void Clear(bool doClearIndicator, int currentPlayer = 0); 23 | 24 | /** 25 | * Draw the whole view when waiting for other players to join. 26 | */ 27 | void DrawWhenInitWaiting(const std::vector &usernames, bool isFirstTime); 28 | 29 | /** 30 | * Draw a box of other player. 31 | */ 32 | void DrawOtherBox(int playerIndex, const GameStat &gameStat, const PlayerStat &playerStat); 33 | 34 | /** 35 | * Draw the box of player himself. 36 | */ 37 | void DrawSelfBox(const GameStat &gameStat, const PlayerStat &playerStat, 38 | const HandCards &handcards, int cursorIndex); 39 | 40 | /** 41 | * Draw the last played card near the center of game board. 42 | */ 43 | void DrawLastPlayedCard(Card lastPlayedCard); 44 | 45 | /** 46 | * Draw the time indicator of \param currentPlayer, which has passed \param timeElapsed seconds. 47 | */ 48 | void DrawTimeIndicator(int currentPlayer, int timeElapsed); 49 | 50 | /** 51 | * Make sure that if it's my turn, there must be a self time indicator. 52 | * i.e. self time indicator and hint text should always appear at the same time. 53 | */ 54 | void DrawSelfTimeIndicatorIfNot(); 55 | 56 | /** 57 | * The handcard may occupy more than one row, in which condition, there is an extra row number. 58 | */ 59 | int GetExtraRowNum() const { return mExtraRowNum; } 60 | 61 | /** 62 | * Get the char in view positioned at \param row in row and \param col in column. 63 | */ 64 | char At(int row, int col) const { return mView[row][col]; } 65 | 66 | friend std::ostream& operator<<(std::ostream& os, const View& view); 67 | 68 | private: 69 | void DrawBorder(int row, int col, int width, int height); 70 | 71 | void DrawBorderAndUsername(int row, int col, int width, int height, const std::string &username); 72 | 73 | void DrawHorizontalBorder(int row, int col, int length); 74 | 75 | void DrawVerticalBorder(int row, int col, int height); 76 | 77 | void DrawHandCards(int row, int col, int width, const HandCards &handcards); 78 | 79 | void DrawUNO(); 80 | 81 | int GetSelfBoxHeight(); 82 | 83 | void Copy(int row, int col, const std::string &src); 84 | 85 | void AlignCenter(int row, int col, int width, const std::string &src); 86 | 87 | private: 88 | using ViewT = std::vector>; 89 | 90 | const static std::string CARDS_REMAINED_STR; 91 | const static std::string LAST_PLAYED_STR; 92 | const static std::string HAND_CARDS_STR; 93 | const static std::string UNO_STR; 94 | 95 | ViewT mView; 96 | 97 | int mExtraRowNum{0}; // due to more than one handcard segment 98 | int mMyIndex; 99 | }; 100 | }} -------------------------------------------------------------------------------- /src/ui/view_formatter.cpp: -------------------------------------------------------------------------------- 1 | #include "view_formatter.h" 2 | 3 | namespace UNO { namespace UI { 4 | 5 | std::vector> ViewFormatter::mPosOfPlayerBox; 6 | std::vector ViewFormatter::mPosOfLastPlayedCard; 7 | std::vector ViewFormatter::mBaseScaleOfView; 8 | std::vector ViewFormatter::mPosOfUNOText; 9 | 10 | void ViewFormatter::Init() 11 | { 12 | // player num is 0 13 | mPosOfPlayerBox.emplace_back(std::vector()); 14 | mPosOfLastPlayedCard.emplace_back(PosT{}); 15 | mBaseScaleOfView.emplace_back(ScaleT{}); 16 | mPosOfUNOText.emplace_back(PosT{}); 17 | 18 | // player num is 1 19 | mPosOfPlayerBox.emplace_back(std::vector()); 20 | mPosOfLastPlayedCard.emplace_back(PosT{}); 21 | mBaseScaleOfView.emplace_back(ScaleT{}); 22 | mPosOfUNOText.emplace_back(PosT{}); 23 | 24 | /** 25 | * player num is 2 26 | * +--------------------+ 27 | * | a2 | 28 | * +--------------------+ 29 | * | cards remained: 6 | 30 | * | last played: R8 | 31 | * +--------------------+ 32 | * ======> 33 | * 34 | * Y2 UNO! 35 | * 36 | * +----------------------------------------+ 37 | * | a1 | 38 | * +----------------------------------------+ 39 | * | R+2 R+2 Y2 W B+2 G5 G+2 G+2 | 40 | * +----------------------------------------+ 41 | */ 42 | mPosOfPlayerBox.emplace_back(std::vector{ 43 | PosT{10, 0}, PosT{0, 10} 44 | }); 45 | mPosOfLastPlayedCard.emplace_back(PosT{8, 20}); 46 | mBaseScaleOfView.emplace_back(ScaleT{16, 42}); 47 | mPosOfUNOText.emplace_back(PosT{8, 24}); 48 | 49 | /** 50 | * player num is 3 51 | * +--------------------+ +--------------------+ 52 | * | a2 | | a2 | 53 | * +--------------------+ +--------------------+ 54 | * | cards remained: 6 | | cards remained: 6 | 55 | * | last played: R8 | | last played: R8 | 56 | * +--------------------+ +--------------------+ 57 | * ======> 58 | * 59 | * Y3 UNO! 60 | * 61 | * +----------------------------------------+ 62 | * | a1 | 63 | * +----------------------------------------+ 64 | * | R+2 R+2 Y2 W B+2 G5 G+2 G+2 | 65 | * +----------------------------------------+ 66 | */ 67 | mPosOfPlayerBox.emplace_back(std::vector{ 68 | PosT{10, 6}, PosT{0, 0}, PosT{0, 32} 69 | }); 70 | mPosOfLastPlayedCard.emplace_back(PosT{8, 26}); 71 | mBaseScaleOfView.emplace_back(ScaleT{16, 54}); 72 | mPosOfUNOText.emplace_back(PosT{8, 30}); 73 | 74 | /** 75 | * player num is 4 76 | * +--------------------+ 77 | * | a2 | 78 | * +--------------------+ 79 | * | cards remained: 6 | 80 | * | last played: R8 | 81 | * +--------------------+ 82 | * 83 | * +--------------------+ +--------------------+ 84 | * | a2 | | a2 | 85 | * +--------------------+ +--------------------+ 86 | * | cards remained: 6 | Y4 | cards remained: 6 | 87 | * | last played: R8 | UNO! | last played: R8 | 88 | * +--------------------+ +--------------------+ 89 | * ======> 90 | * 91 | * +----------------------------------------+ 92 | * | a1 | 93 | * +----------------------------------------+ 94 | * | R+2 R+2 Y2 W B+2 G5 G+2 G+2 | 95 | * +----------------------------------------+ 96 | */ 97 | mPosOfPlayerBox.emplace_back(std::vector{ 98 | PosT{15, 10}, PosT{7, 0}, PosT{0, 20}, PosT{7, 40} 99 | }); 100 | mPosOfLastPlayedCard.emplace_back(PosT{10, 30}); 101 | mBaseScaleOfView.emplace_back(ScaleT{21, 62}); 102 | mPosOfUNOText.emplace_back(PosT{11, 29}); 103 | } 104 | 105 | ViewFormatter::PosT ViewFormatter::GetPosOfPlayerBox(int player) 106 | { 107 | return mPosOfPlayerBox[Common::Common::mPlayerNum][player]; 108 | } 109 | 110 | ViewFormatter::PosT ViewFormatter::GetPosOfLastPlayedCard() 111 | { 112 | return mPosOfLastPlayedCard[Common::Common::mPlayerNum]; 113 | } 114 | 115 | ViewFormatter::PosT ViewFormatter::GetPosOfUNOText(char c) 116 | { 117 | int offset = -1; 118 | switch (c) { 119 | case 'U': offset = 0; break; 120 | case 'N': offset = 1; break; 121 | case 'O': offset = 2; break; 122 | case '!': offset = 3; break; 123 | default: assert(0); 124 | } 125 | auto startPos = mPosOfUNOText[Common::Common::mPlayerNum]; 126 | return PosT{startPos.first, startPos.second + offset}; 127 | } 128 | 129 | ViewFormatter::PosT ViewFormatter::GetPosOfPlayerLastPlayedCard(int playerIndex) 130 | { 131 | auto [row, col] = GetPosOfPlayerBox(playerIndex); 132 | return ViewFormatter::PosT{row + 4, col + 15}; 133 | } 134 | 135 | ViewFormatter::PosT ViewFormatter::GetPosOfHandCard(int handcardIndex, 136 | const Game::HandCards &handcards) 137 | { 138 | int segIndex = Common::Util::GetSegmentIndex(handcardIndex); 139 | int length = handcards.ToStringBySegment(segIndex).size(); 140 | int indent = (42 - length) / 2; 141 | int indexInSeg = Common::Util::GetIndexInSegment(handcardIndex); 142 | auto [row, col] = GetPosOfPlayerBox(0); 143 | row += (3 + segIndex); 144 | col += (indent + handcards.LengthBeforeIndexInSegment(segIndex, indexInSeg) + 1); 145 | return ViewFormatter::PosT{row, col}; 146 | } 147 | 148 | ViewFormatter::ScaleT ViewFormatter::GetBaseScaleOfView() 149 | { 150 | return mBaseScaleOfView[Common::Common::mPlayerNum]; 151 | } 152 | 153 | ViewFormatter::ScaleT ViewFormatter::GetMaxScaleOfView() 154 | { 155 | auto [height, width] = GetBaseScaleOfView(); 156 | return ViewFormatter::ScaleT{height + 6, width}; 157 | } 158 | 159 | ViewFormatter::ScaleT ViewFormatter::GetBaseScaleOfBox(int playerIndex) 160 | { 161 | if (playerIndex == 0) { 162 | return ViewFormatter::ScaleT{5, 42}; 163 | } 164 | else { 165 | return ViewFormatter::ScaleT{6, 22}; 166 | } 167 | } 168 | 169 | }} -------------------------------------------------------------------------------- /src/ui/view_formatter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../common/util.h" 7 | #include "../game/cards.h" 8 | 9 | namespace UNO { namespace UI { 10 | 11 | class ViewFormatter { 12 | public: 13 | using PosT = std::pair; 14 | using ScaleT = std::pair; 15 | 16 | public: 17 | static void Init(); 18 | 19 | static PosT GetPosOfPlayerBox(int playerIndex); 20 | 21 | static PosT GetPosOfLastPlayedCard(); 22 | 23 | static PosT GetPosOfUNOText(char c); 24 | 25 | static PosT GetPosOfPlayerLastPlayedCard(int playerIndex); 26 | 27 | static PosT GetPosOfHandCard(int handcardIndex, const Game::HandCards &handcards); 28 | 29 | static ScaleT GetBaseScaleOfView(); 30 | 31 | static ScaleT GetMaxScaleOfView(); 32 | 33 | static ScaleT GetBaseScaleOfBox(int playerIndex); 34 | 35 | private: 36 | static std::vector> mPosOfPlayerBox; 37 | static std::vector mPosOfLastPlayedCard; 38 | static std::vector mBaseScaleOfView; 39 | static std::vector mPosOfUNOText; 40 | }; 41 | 42 | struct RenderInfo { 43 | ViewFormatter::PosT mPos; 44 | Game::Card mCard; 45 | 46 | RenderInfo(const ViewFormatter::PosT &pos, Game::Card card) 47 | : mPos(pos), mCard(card) {} 48 | 49 | bool operator<(const RenderInfo &rhs) const { 50 | return (mPos.first < rhs.mPos.first) || 51 | (mPos.first == rhs.mPos.first && mPos.second < rhs.mPos.second); 52 | } 53 | }; 54 | }} -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | aux_source_directory(. TEST_DIR) 2 | aux_source_directory(network TEST_DIR) 3 | aux_source_directory(game TEST_DIR) 4 | aux_source_directory(../src/common TEST_DIR) 5 | aux_source_directory(../src/game TEST_DIR) 6 | aux_source_directory(../src/network TEST_DIR) 7 | aux_source_directory(../src/ui TEST_DIR) 8 | 9 | add_executable(uno_test ${TEST_DIR}) 10 | target_link_libraries(uno_test PRIVATE asio) 11 | target_link_libraries(uno_test PRIVATE cxxopts) 12 | target_link_libraries(uno_test PRIVATE gtest) 13 | target_link_libraries(uno_test PRIVATE gmock) 14 | target_link_libraries(uno_test PRIVATE yaml-cpp) 15 | 16 | if(ENABLE_LOG) 17 | target_link_libraries(uno_test PRIVATE spdlog::spdlog) 18 | endif() -------------------------------------------------------------------------------- /test/bot_test.py: -------------------------------------------------------------------------------- 1 | import os, sys, time 2 | from subprocess import Popen, PIPE 3 | 4 | proj_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | sys.path.append(proj_dir + "/bot") 6 | from bot import Bot 7 | 8 | player_num = 2 9 | if len(sys.argv) > 1: 10 | player_num = int(sys.argv[1]) 11 | 12 | start_time = time.time() 13 | 14 | if os.name == 'nt': 15 | # windows 16 | uno_path = proj_dir + "\\build\\src\\Debug\\uno.exe" 17 | else: 18 | # unix 19 | uno_path = proj_dir + "/build/src/uno" 20 | 21 | server = Popen([uno_path, "-l", "9091", "-n", str(player_num)], stdout=PIPE) 22 | time.sleep(0.2) 23 | 24 | bots = [] 25 | 26 | for i in range(player_num): 27 | _username = "bot" + str(i) 28 | bots.append(Bot("localhost:9091", username=_username, player_num=player_num, debug=True)) 29 | time.sleep(0.2) 30 | 31 | for i in range(player_num): 32 | for j in range(player_num - i + 1): 33 | bots[i].single_loop() 34 | 35 | cur_player = -1 36 | 37 | for i in range(player_num): 38 | if bots[i].hint_stat == 1: 39 | cur_player = i 40 | break 41 | assert cur_player != -1 42 | 43 | def wrap_with(num_to_wrap, wrap_range): 44 | # -1 % 3 == 2 in Python 45 | return num_to_wrap % wrap_range 46 | 47 | def update_cur_player(last_played_card, cur_player, is_in_clockwise): 48 | if last_played_card[1:] == 'R': 49 | is_in_clockwise = not is_in_clockwise 50 | if is_in_clockwise: 51 | cur_player = wrap_with(cur_player + 1, player_num) 52 | else: 53 | cur_player = wrap_with(cur_player - 1, player_num) 54 | return cur_player, is_in_clockwise 55 | 56 | # 0 -> 1 -> 2 -> 3 -> 0 57 | # 0 -> 1 -> 2 -> 0 58 | # 0 -> 1 -> 3 -> 0 59 | # 0 -> 1 -> 0 60 | 61 | def test_end(): 62 | global start_time, server 63 | end_time = time.time() 64 | print("time consumed: " + format(end_time - start_time, '.2f') + " seconds") 65 | print("\033[92mtest success\033[0m") 66 | server.kill() 67 | exit(0) 68 | 69 | old_last_played_card = bots[cur_player].last_played_card 70 | last_played_card = bots[cur_player].last_played_card 71 | is_in_clockwise = (last_played_card[1:] != 'R') 72 | while True: 73 | print("cur_player:", cur_player) 74 | has_done_action = (bots[cur_player].hint_stat > 0) 75 | while True: 76 | bots[cur_player].single_loop() 77 | has_done_action = has_done_action or bots[cur_player].hint_stat > 0 78 | if (has_done_action and bots[cur_player].hint_stat == 0): 79 | print("[TURN ENDS]") 80 | # turn ends 81 | print("old last played card:", last_played_card) 82 | old_last_played_card = last_played_card 83 | last_played_card = bots[cur_player].last_played_card 84 | print("new last played card:", last_played_card) 85 | cur_player, is_in_clockwise = \ 86 | update_cur_player(last_played_card, cur_player, is_in_clockwise) 87 | for i in range(player_num): 88 | while True: 89 | print("[loop in]", i) 90 | bots[i].single_loop() 91 | if bots[i].hint_stat == 4: 92 | test_end() 93 | assert bots[i].last_played_card == old_last_played_card \ 94 | or bots[i].last_played_card == last_played_card 95 | if bots[i].last_played_card == last_played_card: 96 | break 97 | print("bots", i, "last_played_card", bots[i].last_played_card) 98 | assert bots[i].last_played_card == last_played_card 99 | break 100 | -------------------------------------------------------------------------------- /test/game/card_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/game/stat.h" 5 | 6 | namespace UNO { namespace Test { 7 | 8 | using namespace testing; 9 | using namespace Game; 10 | 11 | class CardFixture : public ::testing::Test { 12 | public: 13 | CardFixture() {} 14 | 15 | void SetUp() {} 16 | 17 | void TearDown() {} 18 | }; 19 | 20 | TEST_F(CardFixture, CanBePlayedAfter) { 21 | // special cards can not be played as the last one 22 | EXPECT_FALSE(Card{"RS"}.CanBePlayedAfter("R0", true)); 23 | EXPECT_FALSE(Card{"YS"}.CanBePlayedAfter("GS", true)); 24 | EXPECT_FALSE(Card{"BR"}.CanBePlayedAfter("B+2", true)); 25 | EXPECT_FALSE(Card{"G+2"}.CanBePlayedAfter("B+2", true)); 26 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("R2", true)); 27 | EXPECT_FALSE(Card{"+4"}.CanBePlayedAfter("Y6", true)); 28 | 29 | // if the last played card is skip, you can only play a skip 30 | EXPECT_FALSE(Card{"+4"}.CanBePlayedAfter("RS")); 31 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("GS")); 32 | EXPECT_FALSE(Card{"R2"}.CanBePlayedAfter("YS")); 33 | EXPECT_TRUE(Card{"YS"}.CanBePlayedAfter("BS")); 34 | 35 | // if the last played card is draw two, you can only play a draw two or draw four 36 | EXPECT_FALSE(Card{"R2"}.CanBePlayedAfter("Y+2")); 37 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("B+2")); 38 | EXPECT_TRUE(Card{"Y+2"}.CanBePlayedAfter("G+2")); 39 | EXPECT_TRUE(Card{"+4"}.CanBePlayedAfter("R+2")); 40 | 41 | // if the last played card is draw four, you can only play a draw four 42 | EXPECT_FALSE(Card{"R9"}.CanBePlayedAfter("R+4")); 43 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("G+4")); 44 | EXPECT_FALSE(Card{"Y+2"}.CanBePlayedAfter("Y+4")); 45 | EXPECT_TRUE(Card{"+4"}.CanBePlayedAfter("B+4")); 46 | 47 | // wild card can always be played except above conditions 48 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("R0", true)); 49 | EXPECT_FALSE(Card{"+4"}.CanBePlayedAfter("BS")); 50 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("B+2")); 51 | EXPECT_FALSE(Card{"W"}.CanBePlayedAfter("G+4")); 52 | EXPECT_TRUE(Card{"W"}.CanBePlayedAfter("RR")); 53 | EXPECT_TRUE(Card{"+4"}.CanBePlayedAfter("G5")); 54 | EXPECT_TRUE(Card{"W"}.CanBePlayedAfter("Y0")); 55 | 56 | // if not wild card, only cards with the same num or color can be played 57 | EXPECT_FALSE(Card{"RS"}.CanBePlayedAfter("GR")); 58 | EXPECT_TRUE(Card{"RS"}.CanBePlayedAfter("RR")); 59 | EXPECT_TRUE(Card{"RS"}.CanBePlayedAfter("R0")); 60 | EXPECT_FALSE(Card{"BR"}.CanBePlayedAfter("Y2")); 61 | EXPECT_TRUE(Card{"BR"}.CanBePlayedAfter("BR")); 62 | EXPECT_TRUE(Card{"BR"}.CanBePlayedAfter("YR")); 63 | EXPECT_FALSE(Card{"G4"}.CanBePlayedAfter("Y6")); 64 | EXPECT_TRUE(Card{"G3"}.CanBePlayedAfter("Y3")); 65 | EXPECT_TRUE(Card{"G2"}.CanBePlayedAfter("G0")); 66 | EXPECT_FALSE(Card{"Y+2"}.CanBePlayedAfter("G6")); 67 | EXPECT_TRUE(Card{"Y0"}.CanBePlayedAfter("Y3")); 68 | EXPECT_TRUE(Card{"Y9"}.CanBePlayedAfter("G9")); 69 | 70 | // handcards 71 | HandCards cards{{"Y0", "W", "RS", "BR", "Y+2", "G3", "+4"}}; 72 | EXPECT_FALSE(cards.CanBePlayedAfter(cards.GetIndex("Y0"), "G8")); 73 | EXPECT_TRUE(cards.CanBePlayedAfter(cards.GetIndex("BR"), "B5")); 74 | EXPECT_TRUE(cards.CanBePlayedAfter(cards.GetIndex("W"), "G3")); 75 | EXPECT_FALSE(cards.CanBePlayedAfter(cards.GetIndex("Y+2"), "R0")); 76 | EXPECT_FALSE(cards.CanBePlayedAfter(cards.GetIndex("+4"), "RS")); 77 | EXPECT_TRUE(cards.CanBePlayedAfter(cards.GetIndex("+4"), "G+2")); 78 | } 79 | 80 | TEST_F(CardFixture, Length) { 81 | // single card 82 | EXPECT_EQ(Card{"R2"}.Length(), 2); 83 | EXPECT_EQ(Card{"Y+2"}.Length(), 3); 84 | EXPECT_EQ(Card{"W"}.Length(), 1); 85 | EXPECT_EQ(Card{"+4"}.Length(), 2); 86 | 87 | // handcards 88 | HandCards cards{{"RS", "Y0", "Y+2", "G3", "BR", "W", "+4"}}; 89 | EXPECT_EQ(cards.LengthBeforeIndex(0), 0); 90 | EXPECT_EQ(cards.LengthBeforeIndex(1), 4); 91 | EXPECT_EQ(cards.LengthBeforeIndex(2), 8); 92 | EXPECT_EQ(cards.LengthBeforeIndex(3), 13); 93 | EXPECT_EQ(cards.LengthBeforeIndex(4), 17); 94 | EXPECT_EQ(cards.LengthBeforeIndex(5), 21); 95 | EXPECT_EQ(cards.LengthBeforeIndex(6), 24); 96 | EXPECT_EQ(cards.LengthBeforeIndex(7), 28); 97 | EXPECT_EQ(cards.Length(), 28); 98 | 99 | cards.Draw({"R4", "W"}); 100 | EXPECT_EQ(cards.Length(), 35); 101 | 102 | cards.Erase(cards.GetIndex("+4")); 103 | EXPECT_EQ(cards.Length(), 31); 104 | 105 | cards.Erase(cards.GetIndex("W")); 106 | EXPECT_EQ(cards.Length(), 28); 107 | } 108 | 109 | TEST_F(CardFixture, Pile) { 110 | DiscardPile discardPile; 111 | Deck deck{discardPile}; 112 | 113 | deck.Init(); 114 | EXPECT_EQ(deck.GetPile().size(), 108); 115 | 116 | auto initHandCards = deck.DealInitHandCards(3); 117 | EXPECT_EQ(initHandCards.size(), 3); 118 | EXPECT_EQ(deck.GetPile().size(), 87); 119 | 120 | auto cards = deck.Draw(87); 121 | EXPECT_EQ(cards.size(), 87); 122 | EXPECT_EQ(deck.GetPile().size(), 0); 123 | EXPECT_EQ(discardPile.GetPile().size(), 0); 124 | 125 | deck.PutToBottom("+4"); 126 | EXPECT_EQ(deck.GetPile().size(), 1); 127 | EXPECT_EQ(discardPile.GetPile().size(), 0); 128 | 129 | std::set cardset{"R0", "G+2", "BS", "YR", "W", "+4"}; 130 | for (auto card : cardset) { 131 | discardPile.Add(card); 132 | } 133 | 134 | EXPECT_EQ(deck.GetPile().size(), 1); 135 | EXPECT_EQ(discardPile.GetPile().size(), 6); 136 | 137 | cards = deck.Draw(2); 138 | EXPECT_EQ(cards.size(), 2); 139 | EXPECT_TRUE(cardset.count(cards[0])); 140 | EXPECT_TRUE(cardset.count(cards[1])); 141 | EXPECT_EQ(deck.GetPile().size(), 5); 142 | EXPECT_EQ(discardPile.GetPile().size(), 0); 143 | } 144 | }} -------------------------------------------------------------------------------- /test/game/game_board_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../mock.h" 5 | #include "../../src/game/game_board.h" 6 | 7 | namespace UNO { namespace Test { 8 | 9 | using namespace testing; 10 | using namespace Game; 11 | 12 | class GameBoardFixture : public ::testing::Test { 13 | public: 14 | GameBoardFixture() {} 15 | 16 | void SetUp() { 17 | mMockServer = std::make_shared>(); 18 | mGameBoard = std::make_unique(mMockServer); 19 | } 20 | 21 | void TearDown() {} 22 | 23 | // use NickMock to ignore warnings of uninterested mock methods 24 | std::shared_ptr> mMockServer; 25 | std::unique_ptr mGameBoard; 26 | }; 27 | 28 | TEST_F(GameBoardFixture, ReceiveUsername) { 29 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameRspInfo), 0, _)).Times(1); 30 | mGameBoard->ReceiveUsername(0, "tbc"); 31 | auto stats1 = mGameBoard->GetPlayerStats(); 32 | EXPECT_EQ(stats1.size(), 1); 33 | EXPECT_EQ(stats1[0].GetUsername(), "tbc"); 34 | 35 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameRspInfo), 1, _)).Times(1); 36 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameInfo), 0, _)).Times(1); 37 | mGameBoard->ReceiveUsername(1, "cbt"); 38 | auto stats2 = mGameBoard->GetPlayerStats(); 39 | EXPECT_EQ(stats2.size(), 2); 40 | EXPECT_EQ(stats2[1].GetUsername(), "cbt"); 41 | 42 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameRspInfo), 2, _)).Times(1); 43 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameInfo), 0, _)).Times(1); 44 | EXPECT_CALL(*mMockServer, DeliverInfo(&typeid(JoinGameInfo), 1, _)).Times(1); 45 | mGameBoard->ReceiveUsername(2, "bct"); 46 | auto stats3 = mGameBoard->GetPlayerStats(); 47 | EXPECT_EQ(stats3.size(), 3); 48 | EXPECT_EQ(stats3[2].GetUsername(), "bct"); 49 | } 50 | }} -------------------------------------------------------------------------------- /test/game/game_stat_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/game/stat.h" 5 | 6 | namespace UNO { namespace Test { 7 | 8 | using namespace testing; 9 | using namespace Game; 10 | 11 | class GameStatFixture : public ::testing::Test { 12 | public: 13 | GameStatFixture() {} 14 | 15 | void SetUp() { 16 | mGameStat = std::make_unique(mDefaultFirstPlayer, mDefaultFlippedCard); 17 | mGameStat->SetLastPlayedCard(mDefaultLastPlayedCard); 18 | } 19 | 20 | void TearDown() {} 21 | 22 | std::unique_ptr mGameStat; 23 | 24 | const int mDefaultFirstPlayer = 1; 25 | const Card mDefaultFlippedCard = "R3"; 26 | const bool mDefaultIsInClockwise = true; 27 | const Card mDefaultLastPlayedCard = "B6"; 28 | }; 29 | 30 | #define EXPECT_GAME_STAT_EQ(currentPlayer, isInClockwise, lastPlayedCard, cardsNumToDraw) \ 31 | EXPECT_EQ(mGameStat->GetCurrentPlayer(), (currentPlayer)); \ 32 | EXPECT_EQ(mGameStat->IsInClockwise(), (isInClockwise)); \ 33 | EXPECT_EQ(mGameStat->GetLastPlayedCard(), (lastPlayedCard)); \ 34 | EXPECT_EQ(mGameStat->GetCardsNumToDraw(), (cardsNumToDraw)); 35 | 36 | TEST_F(GameStatFixture, Draw_Common) { 37 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 38 | mGameStat->UpdateAfterDraw(); 39 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 40 | } 41 | 42 | TEST_F(GameStatFixture, Draw_ConsumePenalty_Plus2) { 43 | Card lastPlayedCard = "Y+2"; 44 | mGameStat->SetLastPlayedCard(lastPlayedCard); 45 | mGameStat->SetCardsNumToDraw(2); 46 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, lastPlayedCard, 2); 47 | mGameStat->UpdateAfterDraw(); 48 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, "Y", 1); 49 | } 50 | 51 | TEST_F(GameStatFixture, Draw_ConsumePenalty_Plus4) { 52 | Card lastPlayedCard = "G+4"; 53 | mGameStat->SetLastPlayedCard(lastPlayedCard); 54 | mGameStat->SetCardsNumToDraw(4); 55 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, lastPlayedCard, 4); 56 | mGameStat->UpdateAfterDraw(); 57 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, "G", 1); 58 | } 59 | 60 | TEST_F(GameStatFixture, Skip_Common) { 61 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 62 | mGameStat->UpdateAfterSkip(); 63 | // currentPlayer may be wrapped here 64 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 65 | } 66 | 67 | TEST_F(GameStatFixture, Skip_ConsumePenalty) { 68 | Card lastPlayedCard = "RS"; 69 | mGameStat->SetLastPlayedCard(lastPlayedCard); 70 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, lastPlayedCard, 1); 71 | mGameStat->UpdateAfterSkip(); 72 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, "R", 1); 73 | } 74 | 75 | TEST_F(GameStatFixture, Play_Common) { 76 | Card cardToPlay = "G2"; 77 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 78 | mGameStat->UpdateAfterPlay(cardToPlay); 79 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 1); 80 | } 81 | 82 | TEST_F(GameStatFixture, Play_Wild) { 83 | Card cardToPlay = "BW"; 84 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 85 | mGameStat->UpdateAfterPlay(cardToPlay); 86 | Card lastPlayedCard{CardColor::BLUE, mDefaultLastPlayedCard.mText}; 87 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, lastPlayedCard, 1); 88 | } 89 | 90 | TEST_F(GameStatFixture, Play_Reverse) { 91 | Card cardToPlay = "YR"; 92 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 93 | mGameStat->UpdateAfterPlay(cardToPlay); 94 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer - 1, !mDefaultIsInClockwise, cardToPlay, 1); 95 | } 96 | 97 | TEST_F(GameStatFixture, Play_Plus2) { 98 | Card cardToPlay = "R+2"; 99 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 100 | mGameStat->UpdateAfterPlay(cardToPlay); 101 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 2); 102 | 103 | mGameStat->SetCurrentPlayer(mDefaultFirstPlayer); 104 | mGameStat->UpdateAfterPlay(cardToPlay); 105 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 4); 106 | } 107 | 108 | TEST_F(GameStatFixture, Play_Plus4) { 109 | Card cardToPlay = "B+4"; 110 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer, mDefaultIsInClockwise, mDefaultLastPlayedCard, 1); 111 | mGameStat->UpdateAfterPlay(cardToPlay); 112 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 4); 113 | 114 | mGameStat->SetCurrentPlayer(mDefaultFirstPlayer); 115 | cardToPlay = "Y+4"; 116 | mGameStat->UpdateAfterPlay(cardToPlay); 117 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 8); 118 | 119 | mGameStat->SetCurrentPlayer(mDefaultFirstPlayer); 120 | cardToPlay = "Y+2"; 121 | mGameStat->UpdateAfterPlay(cardToPlay); 122 | EXPECT_GAME_STAT_EQ(mDefaultFirstPlayer + 1, mDefaultIsInClockwise, cardToPlay, 10); 123 | } 124 | 125 | }} -------------------------------------------------------------------------------- /test/game/info_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/game/stat.h" 5 | 6 | namespace UNO { namespace Test { 7 | 8 | using namespace testing; 9 | using namespace Game; 10 | 11 | class InfoFixture : public ::testing::Test { 12 | public: 13 | InfoFixture() {} 14 | 15 | void SetUp() {} 16 | 17 | void TearDown() {} 18 | 19 | template 20 | void Harness(Types&&... args) { 21 | InfoT info(args...); 22 | info.Serialize(mBuffer); 23 | auto newInfo = *InfoT::Deserialize(mBuffer); 24 | EXPECT_EQ(info, newInfo); 25 | } 26 | 27 | uint8_t mBuffer[100]; 28 | }; 29 | 30 | TEST_F(InfoFixture, JoinGameInfo) { 31 | Harness("tbc"); 32 | } 33 | 34 | TEST_F(InfoFixture, GameStartInfo) { 35 | std::array initHandCards{"R1", "R2", "R3", "R4", "+4", "W", "B5"}; 36 | std::vector usernames{"tbc", "tyq"}; 37 | Harness(initHandCards, "R0", 0, usernames); 38 | } 39 | 40 | TEST_F(InfoFixture, ActionInfo) { 41 | Harness(ActionType::DRAW); 42 | } 43 | 44 | TEST_F(InfoFixture, DrawInfo) { 45 | Harness(2); 46 | } 47 | 48 | TEST_F(InfoFixture, SkipInfo) { 49 | Harness(); 50 | } 51 | 52 | TEST_F(InfoFixture, PlayInfo) { 53 | Harness("Y5", CardColor::GREEN); 54 | } 55 | 56 | TEST_F(InfoFixture, DrawRspInfo) { 57 | std::vector cards{"RS", "G+2"}; 58 | Harness(2, cards); 59 | } 60 | 61 | TEST_F(InfoFixture, GameEndInfo) { 62 | Harness(0); 63 | } 64 | 65 | }} -------------------------------------------------------------------------------- /test/game/player_stat_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/game/stat.h" 5 | 6 | namespace UNO { namespace Test { 7 | 8 | using namespace testing; 9 | using namespace Game; 10 | 11 | class PlayerStatFixture : public ::testing::Test { 12 | public: 13 | PlayerStatFixture() {} 14 | 15 | void SetUp() { 16 | mPlayerStat = std::make_unique("tbc", mDefaultRemainingHandCardsNum); 17 | mPlayerStat->SetLastPlayedCard(mDefaultLastPlayedCard); 18 | } 19 | 20 | void TearDown() {} 21 | 22 | std::unique_ptr mPlayerStat; 23 | 24 | const int mDefaultRemainingHandCardsNum = 7; 25 | const bool mDefaultDoPlayInLastRound = false; 26 | const Card mDefaultLastPlayedCard = "B6"; 27 | const bool mDefaultHasChanceToPlayAfterDraw = false; 28 | const int mDefaultIndexOfNewlyDrawn = -1; 29 | }; 30 | 31 | #define EXPECT_PLAYER_STAT_EQ(remainingHandCardsNum, doPlayInLastRound, \ 32 | lastPlayedCard, hasChanceToPlayAfterDraw, indexOfNewlyDrawn) \ 33 | EXPECT_EQ(mPlayerStat->GetRemainingHandCardsNum(), remainingHandCardsNum); \ 34 | EXPECT_EQ(mPlayerStat->DoPlayInLastRound(), doPlayInLastRound); \ 35 | EXPECT_EQ(mPlayerStat->GetLastPlayedCard(), lastPlayedCard); \ 36 | EXPECT_EQ(mPlayerStat->HasChanceToPlayAfterDraw(), hasChanceToPlayAfterDraw); \ 37 | EXPECT_EQ(mPlayerStat->GetIndexOfNewlyDrawn(), indexOfNewlyDrawn); 38 | 39 | TEST_F(PlayerStatFixture, Draw_SingleCard) { 40 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum, mDefaultDoPlayInLastRound, 41 | mDefaultLastPlayedCard, mDefaultHasChanceToPlayAfterDraw, mDefaultIndexOfNewlyDrawn); 42 | mPlayerStat->UpdateAfterDraw(1, 0); 43 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum + 1, false, mDefaultLastPlayedCard, true, 0); 44 | } 45 | 46 | TEST_F(PlayerStatFixture, Draw_MultiCards) { 47 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum, mDefaultDoPlayInLastRound, 48 | mDefaultLastPlayedCard, mDefaultHasChanceToPlayAfterDraw, mDefaultIndexOfNewlyDrawn); 49 | mPlayerStat->UpdateAfterDraw(4, 0); 50 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum + 4, false, mDefaultLastPlayedCard, false, 0); 51 | } 52 | 53 | TEST_F(PlayerStatFixture, Skip) { 54 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum, mDefaultDoPlayInLastRound, 55 | mDefaultLastPlayedCard, mDefaultHasChanceToPlayAfterDraw, mDefaultIndexOfNewlyDrawn); 56 | mPlayerStat->UpdateAfterSkip(); 57 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum, false, 58 | mDefaultLastPlayedCard, false, mDefaultIndexOfNewlyDrawn); 59 | } 60 | 61 | TEST_F(PlayerStatFixture, Play) { 62 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum, mDefaultDoPlayInLastRound, 63 | mDefaultLastPlayedCard, mDefaultHasChanceToPlayAfterDraw, mDefaultIndexOfNewlyDrawn); 64 | Card cardToPlay = "R5"; 65 | mPlayerStat->UpdateAfterPlay(cardToPlay); 66 | EXPECT_PLAYER_STAT_EQ(mDefaultRemainingHandCardsNum - 1, true, 67 | cardToPlay, false, mDefaultIndexOfNewlyDrawn); 68 | } 69 | 70 | }} -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../src/common/common.h" 3 | 4 | int main(int argc, char **argv) { 5 | UNO::Common::Common::mPlayerNum = 3; 6 | UNO::Common::Common::mTimeoutPerTurn = 15; 7 | UNO::Common::Common::mHandCardsNumPerRow = 8; 8 | ::testing::InitGoogleTest(&argc, argv); 9 | return RUN_ALL_TESTS(); 10 | } -------------------------------------------------------------------------------- /test/mock.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../src/network/server.h" 7 | #include "../src/network/client.h" 8 | 9 | namespace UNO { namespace Test { 10 | 11 | using namespace testing; 12 | using namespace Game; 13 | 14 | class MockServer : public IServer { 15 | public: 16 | MOCK_METHOD(void, Run, (), (override)); 17 | MOCK_METHOD(void, Close, (), (override)); 18 | MOCK_METHOD(void, Reset, (), (override)); 19 | MOCK_METHOD(void, RegisterReceiveJoinGameInfoCallback, 20 | (const std::function &), (override)); 21 | MOCK_METHOD(void, RegisterAllPlayersJoinedCallback, (const std::function &), (override)); 22 | MOCK_METHOD(std::unique_ptr, ReceiveInfo, (const std::type_info *, int), (override)); 23 | MOCK_METHOD(void, DeliverInfo, (const std::type_info *, int, const Info &), (override)); 24 | }; 25 | 26 | class MockClient : public IClient { 27 | public: 28 | MOCK_METHOD(void, Connect, (), (override)); 29 | MOCK_METHOD(void, Reset, (), (override)); 30 | MOCK_METHOD(void, RegisterConnectCallback, (const std::function &), (override)); 31 | MOCK_METHOD(std::unique_ptr, ReceiveInfo, (const std::type_info *), (override)); 32 | MOCK_METHOD(void, DeliverInfo, (const std::type_info *, const Info &), (override)); 33 | }; 34 | 35 | }} -------------------------------------------------------------------------------- /test/network/session_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "asio.hpp" 6 | #include "../../src/network/session.h" 7 | 8 | namespace UNO { namespace Test { 9 | 10 | using namespace testing; 11 | using namespace Network; 12 | using namespace Game; 13 | using asio::ip::tcp; 14 | 15 | class SessionFixture : public ::testing::Test { 16 | public: 17 | SessionFixture() {} 18 | 19 | void SetUp() { 20 | mServerThread.reset(new std::thread([this]() { SetUpServer(); })); 21 | // it's necessary to **sleep** to ensure that connect is after accept 22 | std::this_thread::sleep_for(std::chrono::milliseconds(200)); 23 | SetUpClient(); 24 | } 25 | 26 | void TearDown() {} 27 | 28 | void SetUpServer() { 29 | tcp::acceptor acceptor(mServerContext, tcp::endpoint(tcp::v4(), PORT_NUM)); 30 | tcp::socket socket(mServerContext); 31 | acceptor.accept(socket); 32 | 33 | mServerSession.reset(new Session(std::move(socket))); 34 | } 35 | 36 | void SetUpClient() { 37 | tcp::resolver resolver(mClientContext); 38 | auto endpoints = resolver.resolve("localhost", std::to_string(PORT_NUM)); 39 | tcp::socket socket(mClientContext); 40 | asio::connect(socket, endpoints); 41 | 42 | mClientSession.reset(new Session(std::move(socket))); 43 | } 44 | 45 | constexpr static short PORT_NUM = 6012; 46 | asio::io_context mServerContext; 47 | asio::io_context mClientContext; 48 | std::unique_ptr mServerSession; 49 | std::unique_ptr mClientSession; 50 | std::unique_ptr mServerThread; 51 | }; 52 | 53 | TEST_F(SessionFixture, JoinGameInfo) { 54 | mClientSession->DeliverInfo("tbc"); 55 | // here the invokation of run ensures that write will finish before receive 56 | mClientContext.run(); 57 | 58 | // make sure that mServerSession has been reset before invoking ReceiveInfo 59 | mServerThread->join(); 60 | std::unique_ptr info = mServerSession->ReceiveInfo(); 61 | // only async operation needs invokation of run, while read here is blocking 62 | // mServerContext.run(); 63 | 64 | EXPECT_EQ(info->mUsername, "tbc"); 65 | } 66 | 67 | TEST_F(SessionFixture, GameStartInfo) { 68 | std::array initHandCards{"R1", "W", "+4", "R+2", "YS", "GR", "BW"}; 69 | std::vector usernames{"fred", "daniel", "greg", "cissie"}; 70 | mClientSession->DeliverInfo(initHandCards, "B0", 1, usernames); 71 | mClientContext.run(); 72 | 73 | mServerThread->join(); 74 | std::unique_ptr info = mServerSession->ReceiveInfo(); 75 | 76 | EXPECT_EQ(info->mInitHandCards, initHandCards); 77 | EXPECT_EQ(info->mFlippedCard, "B0"); 78 | EXPECT_EQ(info->mFirstPlayer, 1); 79 | EXPECT_EQ(info->mUsernames, usernames); 80 | } 81 | 82 | TEST_F(SessionFixture, ActionInfo) { 83 | mClientSession->DeliverInfo(ActionType::DRAW); 84 | mClientContext.run(); 85 | 86 | mServerThread->join(); 87 | std::unique_ptr info = mServerSession->ReceiveInfo(); 88 | 89 | EXPECT_EQ(info->mActionType, ActionType::DRAW); 90 | EXPECT_EQ(info->mPlayerIndex, -1); 91 | } 92 | 93 | TEST_F(SessionFixture, DrawInfo) { 94 | mClientSession->DeliverInfo(2); 95 | mClientContext.run(); 96 | 97 | mServerThread->join(); 98 | std::unique_ptr info = mServerSession->ReceiveInfo(); 99 | 100 | EXPECT_EQ(info->mActionType, ActionType::DRAW); 101 | EXPECT_EQ(info->mPlayerIndex, -1); 102 | EXPECT_EQ(info->mNumber, 2); 103 | } 104 | 105 | TEST_F(SessionFixture, SkipInfo) { 106 | mClientSession->DeliverInfo(); 107 | mClientContext.run(); 108 | 109 | mServerThread->join(); 110 | std::unique_ptr info = mServerSession->ReceiveInfo(); 111 | 112 | EXPECT_EQ(info->mActionType, ActionType::SKIP); 113 | EXPECT_EQ(info->mPlayerIndex, -1); 114 | } 115 | 116 | TEST_F(SessionFixture, PlayInfoWithoutNextColor) { 117 | mClientSession->DeliverInfo("R6"); 118 | mClientContext.run(); 119 | 120 | mServerThread->join(); 121 | std::unique_ptr info = mServerSession->ReceiveInfo(); 122 | 123 | EXPECT_EQ(info->mActionType, ActionType::PLAY); 124 | EXPECT_EQ(info->mPlayerIndex, -1); 125 | EXPECT_EQ(info->mCard, "R6"); 126 | EXPECT_EQ(info->mNextColor, CardColor::RED); 127 | } 128 | 129 | TEST_F(SessionFixture, PlayInfoWithNextColor) { 130 | mClientSession->DeliverInfo("W", CardColor::BLUE); 131 | mClientContext.run(); 132 | 133 | mServerThread->join(); 134 | std::unique_ptr info = mServerSession->ReceiveInfo(); 135 | 136 | EXPECT_EQ(info->mActionType, ActionType::PLAY); 137 | EXPECT_EQ(info->mPlayerIndex, -1); 138 | EXPECT_EQ(info->mCard, "W"); 139 | EXPECT_EQ(info->mNextColor, CardColor::BLUE); 140 | } 141 | 142 | TEST_F(SessionFixture, DrawRspInfo) { 143 | mServerThread->join(); 144 | std::vector cards{"R2", "+4"}; 145 | mServerSession->DeliverInfo(2, cards); 146 | mServerContext.run(); 147 | 148 | std::unique_ptr info = mClientSession->ReceiveInfo(); 149 | 150 | EXPECT_EQ(info->mNumber, 2); 151 | EXPECT_EQ(info->mCards, cards); 152 | } 153 | }} --------------------------------------------------------------------------------