├── AgariIndex.java ├── CMakeLists.txt ├── README.md ├── c_src ├── export.cpp ├── mctree.cpp ├── mctree.h ├── player.cpp └── player.h ├── complete.pickle ├── controller.py ├── graphics.py ├── montecarlo.py ├── players.py ├── pyenv.py ├── resource.py ├── sprites.py ├── test.py └── tiles ├── MJd1.png ├── MJd2.png ├── MJd3.png ├── MJf1.png ├── MJf2.png ├── MJf3.png ├── MJf4.png ├── MJs1.png ├── MJs2.png ├── MJs3.png ├── MJs4.png ├── MJs5.png ├── MJs6.png ├── MJs7.png ├── MJs8.png ├── MJs9.png ├── MJt1.png ├── MJt2.png ├── MJt3.png ├── MJt4.png ├── MJt5.png ├── MJt6.png ├── MJt7.png ├── MJt8.png ├── MJt9.png ├── MJw1.png ├── MJw2.png ├── MJw3.png ├── MJw4.png ├── MJw5.png ├── MJw6.png ├── MJw7.png ├── MJw8.png ├── MJw9.png └── hidden.png /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.12) 2 | project(clib) 3 | set (CMAKE_CXX_STANDARD 14) 4 | 5 | find_package(pybind11 REQUIRED) 6 | pybind11_add_module(clib ./c_src/export.cpp ./c_src/mctree.cpp ./c_src/player.cpp) 7 | # if(MSVC) 8 | # target_compile_options(env PRIVATE /utf-8) 9 | # endif() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mahjong 2 | 3 | ## Python Mahjong game with monte-carlo AI 4 | ### to run pygame with GUI, run controller.py 5 | ### to run pure terminal Python game, run pyenv.py 6 | -------------------------------------------------------------------------------- /c_src/export.cpp: -------------------------------------------------------------------------------- 1 | #include "mctree.h" 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | using namespace pybind11::literals; 10 | unordered_set complete_keys; 11 | 12 | vector mc_search(STATE_ID id, TILE_TYPE last_tile, vector> handcards, 13 | vector deck, int current_idx, int control_idx, int n_threads, int n, float c = 1.f, float temp = 0.5f) { 14 | State* root = new State(id, last_tile, handcards, deck, current_idx); 15 | // cout << static_cast(id) << endl; 16 | // cout << static_cast(last_tile) << endl; 17 | // for (auto& hc : handcards) { 18 | // for (auto c : hc) { 19 | // cout << static_cast(c) << ", "; 20 | // } 21 | // cout << endl; 22 | // } 23 | // for (auto c : deck) { 24 | // cout << static_cast(c) << ", "; 25 | // } 26 | // cout << endl; 27 | // cout << current_idx << endl << control_idx << endl << n_threads << endl << n << endl; 28 | 29 | MCTree tree(root, control_idx, c); 30 | tree.search(n_threads, n); 31 | auto res = tree.predict(temp); 32 | // for (auto r : res) { 33 | // cout << static_cast(r) << ", "; 34 | // } 35 | // cout << endl; 36 | return res; 37 | } 38 | 39 | int main() { 40 | // mc_search(STATE_ID::DISCARD, TILE_TYPE::NONE, {{TILE_TYPE::BAMBOO_ONE, TILE_TYPE::BAMBOO_ONE}, {TILE_TYPE::BAMBOO_ONE, TILE_TYPE::BAMBOO_ONE}, {TILE_TYPE::BAMBOO_ONE, TILE_TYPE::BAMBOO_ONE}}, 41 | // {TILE_TYPE::BAMBOO_ONE, TILE_TYPE::BAMBOO_ONE}, 0, 0, 4, 1000); 42 | // FILE f("../../complete.bin"); 43 | // int values[9362]; 44 | // fread(values, sizeof(int), 9362, ) 45 | ifstream fs ("../../complete.bin", ios::in | ios::binary); 46 | 47 | if (fs.is_open()) { 48 | fs.seekg(0, ios::end); 49 | int f_size = fs.tellg(); 50 | fs.seekg(0); 51 | vector nums(f_size / sizeof(int), 0); 52 | fs.read((char*)&nums[0], f_size); 53 | cout << nums.size() << endl; 54 | for (auto i : nums) { 55 | complete_keys.insert(i); 56 | } 57 | } else { 58 | cout << "sth wrong." << endl; 59 | } 60 | Player p({TILE_TYPE::BAMBOO_ONE, TILE_TYPE::BAMBOO_ONE}); 61 | cout << p.can_complete() << endl; 62 | } 63 | 64 | PYBIND11_MODULE(clib, m) { 65 | m.def("mc_search", &mc_search, py::arg("state_id"), py::arg("last_tile"), 66 | py::arg("handcards"), py::arg("deck"), 67 | py::arg("current_idx"), py::arg("control_idx"), 68 | py::arg("n_threads"), py::arg("n"), 69 | py::arg("c") = 1.f, py::arg("temp") = 0.5f); 70 | py::enum_(m, "TILE_TYPE") 71 | .value("BAMBOO_ONE", TILE_TYPE::BAMBOO_ONE) 72 | .value("BAMBOO_TWO", TILE_TYPE::BAMBOO_TWO) 73 | .value("BAMBOO_THREE", TILE_TYPE::BAMBOO_THREE) 74 | .value("BAMBOO_FOUR", TILE_TYPE::BAMBOO_FOUR) 75 | .value("BAMBOO_FIVE", TILE_TYPE::BAMBOO_FIVE) 76 | .value("BAMBOO_SIX", TILE_TYPE::BAMBOO_SIX) 77 | .value("BAMBOO_SEVEN", TILE_TYPE::BAMBOO_SEVEN) 78 | .value("BAMBOO_EIGHT", TILE_TYPE::BAMBOO_EIGHT) 79 | .value("BAMBOO_NINE", TILE_TYPE::BAMBOO_NINE) 80 | .value("CIRCLE_ONE", TILE_TYPE::CIRCLE_ONE) 81 | .value("CIRCLE_TWO", TILE_TYPE::CIRCLE_TWO) 82 | .value("CIRCLE_THREE", TILE_TYPE::CIRCLE_THREE) 83 | .value("CIRCLE_FOUR", TILE_TYPE::CIRCLE_FOUR) 84 | .value("CIRCLE_FIVE", TILE_TYPE::CIRCLE_FIVE) 85 | .value("CIRCLE_SIX", TILE_TYPE::CIRCLE_SIX) 86 | .value("CIRCLE_SEVEN", TILE_TYPE::CIRCLE_SEVEN) 87 | .value("CIRCLE_EIGHT", TILE_TYPE::CIRCLE_EIGHT) 88 | .value("CIRCLE_NINE", TILE_TYPE::CIRCLE_NINE) 89 | .value("WAN_ONE", TILE_TYPE::WAN_ONE) 90 | .value("WAN_TWO", TILE_TYPE::WAN_TWO) 91 | .value("WAN_THREE", TILE_TYPE::WAN_THREE) 92 | .value("WAN_FOUR", TILE_TYPE::WAN_FOUR) 93 | .value("WAN_FIVE", TILE_TYPE::WAN_FIVE) 94 | .value("WAN_SIX", TILE_TYPE::WAN_SIX) 95 | .value("WAN_SEVEN", TILE_TYPE::WAN_SEVEN) 96 | .value("WAN_EIGHT", TILE_TYPE::WAN_EIGHT) 97 | .value("WAN_NINE", TILE_TYPE::WAN_NINE) 98 | .value("SPECIAL_RED_DRAGON", TILE_TYPE::SPECIAL_RED_DRAGON) 99 | .value("SPECIAL_GREEN_DRAGON", TILE_TYPE::SPECIAL_GREEN_DRAGON) 100 | .value("SPECIAL_WHITE_DRAGON", TILE_TYPE::SPECIAL_WHITE_DRAGON) 101 | .value("SPECIAL_EAST", TILE_TYPE::SPECIAL_EAST) 102 | .value("SPECIAL_SOUTH", TILE_TYPE::SPECIAL_SOUTH) 103 | .value("SPECIAL_WEST", TILE_TYPE::SPECIAL_WEST) 104 | .value("SPECIAL_NORTH", TILE_TYPE::SPECIAL_NORTH) 105 | .value("NONE", TILE_TYPE::NONE); 106 | py::enum_(m, "STATE_ID") 107 | .value("CHOW", STATE_ID::CHOW) 108 | .value("DISCARD", STATE_ID::DISCARD) 109 | .value("PUNG1", STATE_ID::PUNG1) 110 | .value("PUNG2", STATE_ID::PUNG2) 111 | .value("PUNG3", STATE_ID::PUNG3); 112 | } -------------------------------------------------------------------------------- /c_src/mctree.cpp: -------------------------------------------------------------------------------- 1 | #include "mctree.h" 2 | #include 3 | #include 4 | #include 5 | 6 | extern unordered_set complete_keys; 7 | static vector seeds; 8 | 9 | State::State(STATE_ID id, TILE_TYPE last_tile, const vector>& handcards, 10 | const vector& deck, int idx) { 11 | this->id = id; 12 | this->last_tile = last_tile; 13 | for (int i = 0; i < handcards.size(); i++) { 14 | this->players.push_back(new Player(handcards[i])); 15 | } 16 | this->remain_cards = deck; 17 | this->idx = idx; 18 | } 19 | 20 | State::State(const State& s) : last_tile(s.last_tile), idx(s.idx), winner(s.winner), id(s.id), remain_cards(s.remain_cards) { 21 | for (int i = 0; i < s.players.size(); i++) { 22 | this->players.push_back(new Player(*s.players[i])); 23 | } 24 | } 25 | 26 | State::~State() { 27 | for (auto p : this->players) { 28 | if (p) { 29 | delete p; 30 | } 31 | } 32 | } 33 | 34 | vector> State::get_action_space() { 35 | if (id == STATE_ID::CHOW) { 36 | vector> sols = {{}}; 37 | bool can_chow = players[idx]->can_chow(last_tile, sols); 38 | return sols; 39 | } else if (id == STATE_ID::DISCARD) { 40 | vector> sols; 41 | for (const auto& t : players[idx]->tiles) { 42 | sols.push_back({t}); 43 | } 44 | return sols; 45 | } else if (STATE_ID::PUNG1 <= id && id <= STATE_ID::PUNG3) { 46 | bool can_pung = players[(idx + 1 + static_cast(id) - static_cast(STATE_ID::PUNG1)) % 4]->can_pung(last_tile); 47 | vector> sols = {{}}; 48 | if (can_pung) { 49 | sols.push_back({last_tile, last_tile}); 50 | } 51 | return sols; 52 | } else if (id == STATE_ID::FINISHED) { 53 | return {}; 54 | } else { 55 | throw std::runtime_error("invalid state id"); 56 | } 57 | } 58 | 59 | 60 | Node::Node(Edge* src, State* st, vector priors) { 61 | this->st = st; 62 | this->actions = st->get_action_space(); 63 | if (priors.empty() && !this->actions.empty()) { 64 | priors = vector(this->actions.size(), 1.f / this->actions.size()); 65 | } 66 | this->src = src; 67 | for (int i = 0; i < this->actions.size(); i++) { 68 | this->edges.push_back(new Edge(this, this->actions[i], priors[i])); 69 | } 70 | } 71 | 72 | Node::~Node() { 73 | if (this->st) { 74 | delete this->st; 75 | } 76 | for (auto e : this->edges) { 77 | if (e) { 78 | delete e; 79 | } 80 | } 81 | } 82 | 83 | Edge::Edge(Node* src, const vector& action, float prior) { 84 | this->action = action; 85 | this->p = prior; 86 | this->src = src; 87 | } 88 | 89 | 90 | Edge::~Edge() { 91 | if (this->dest) { 92 | delete this->dest; 93 | } 94 | } 95 | 96 | 97 | Edge* Node::choose(float c) { 98 | float sum = 0.f; 99 | size_t e_size = edges.size(); 100 | // vector n(e_size, 0); 101 | // vector p(e_size, 0); 102 | // vector q(e_size, 0); 103 | for (int i = 0; i < edges.size(); i++) { 104 | // std::shared_lock lock(edges[i]->mu); 105 | // n[i] = edges[i]->n; 106 | sum += edges[i]->n; 107 | // p[i] = edges[i]->p; 108 | // q[i] = edges[i]->q; 109 | } 110 | 111 | float nsum_sqrt = sqrtf(sum); 112 | int best_idx = -1; 113 | float best = -100.f; 114 | for (int i = 0; i < e_size; i++) { 115 | float cand = edges[i]->q + c * edges[i]->p * nsum_sqrt / (1.f + edges[i]->n); 116 | if (cand > best) { 117 | best_idx = i; 118 | best = cand; 119 | } 120 | } 121 | return edges[best_idx]; 122 | } 123 | 124 | 125 | MCTree::MCTree(State* st, int idx, float c) { 126 | if (complete_keys.empty()) { 127 | ifstream fs ("./complete.bin", ios::in | ios::binary); 128 | if (fs.is_open()) { 129 | fs.seekg(0, ios::end); 130 | int f_size = fs.tellg(); 131 | fs.seekg(0); 132 | vector nums(f_size / sizeof(int), 0); 133 | fs.read((char*)&nums[0], f_size); 134 | cout << nums.size() << endl; 135 | for (auto i : nums) { 136 | complete_keys.insert(i); 137 | } 138 | } else { 139 | cout << "sth wrong." << endl; 140 | } 141 | } else { 142 | // cout << "already exists" << endl; 143 | } 144 | 145 | this->root = new Node(nullptr, st); 146 | this->idx = idx; 147 | this->counter = 0; 148 | this->c = c; 149 | } 150 | 151 | 152 | MCTree::~MCTree() { 153 | if (root) { 154 | delete root; 155 | } 156 | } 157 | 158 | void MCTree::search(int n_threads, int n) { 159 | if (seeds.empty()) { 160 | for (int i = 0; i < n_threads; i++) { 161 | seeds.push_back(i); 162 | } 163 | } 164 | counter = n; 165 | vector threads; 166 | for (int i = 0; i < n_threads; i++) { 167 | 168 | threads.push_back(std::move(std::thread(&MCTree::search_thread, this, &seeds[i]))); 169 | } 170 | for (auto& t : threads) { 171 | t.join(); 172 | } 173 | } 174 | 175 | void MCTree::search_thread(unsigned int* seed) { 176 | while (true) { 177 | { 178 | std::lock_guard lock(counter_mu); 179 | if (counter == 0) { 180 | break; 181 | } else { 182 | counter--; 183 | } 184 | } 185 | float val = 0.f; 186 | // cout << "explore" << endl; 187 | Node* leaf = explore(root, val, seed); 188 | // cout << val << endl; 189 | backup(leaf, val); 190 | } 191 | } 192 | 193 | 194 | // TODO: change node lock to per 195 | Node* MCTree::explore(Node* node, float& val, unsigned int* seed) { 196 | std::unique_lock lock(node->mu); 197 | srand(*seed); 198 | auto edge = node->choose(this->c); 199 | if (edge->dest) { 200 | if (edge->terminiated) { 201 | val = edge->r; 202 | lock.unlock(); 203 | return edge->dest; 204 | } else { 205 | lock.unlock(); 206 | return explore(edge->dest, val, seed); 207 | } 208 | } else { 209 | // cout << node->st->idx << ": " << static_cast(node->st->id) << ", "; 210 | // cout << node->st->get_action_space().size() << endl; 211 | State* sprime = step(*node->st, edge->action); 212 | while (sprime->idx != this->idx && sprime->id != STATE_ID::FINISHED) { 213 | auto last_s = sprime; 214 | auto actions = sprime->get_action_space(); 215 | // cout << sprime->idx << ": " << static_cast(sprime->id) << ", "; 216 | // cout << actions.size() << endl; 217 | sprime = step(*sprime, actions[rand() % actions.size()]); 218 | if (last_s) { 219 | delete last_s; 220 | } 221 | } 222 | edge->dest = new Node(edge, sprime); 223 | 224 | if (sprime->id == STATE_ID::FINISHED) { 225 | if (sprime->winner == idx) { 226 | edge->r = 1; 227 | } else if (sprime->winner >= 0) { 228 | edge->r = -1; 229 | } else { 230 | edge->r = 0; 231 | } 232 | edge->terminiated = true; 233 | val = edge->r; 234 | return edge->dest; 235 | } 236 | lock.unlock(); 237 | // cout << "rollout "; 238 | val = rollout(edge->dest, seed); 239 | // cout << val << endl; 240 | // if (val != 0) { 241 | // cout << val << ", "; 242 | // } 243 | 244 | 245 | return edge->dest; 246 | } 247 | } 248 | 249 | void MCTree::backup(Node* node, float val) { 250 | while (node->src) { 251 | auto edge = node->src; 252 | { 253 | std::lock_guard lock(edge->src->mu); 254 | edge->n++; 255 | edge->w += val; 256 | edge->q = edge->w / edge->n; 257 | } 258 | node = edge->src; 259 | } 260 | } 261 | 262 | float MCTree::rollout(Node* node, unsigned int* seed) { 263 | auto st = node->st; 264 | bool first_time = true; 265 | srand(*seed); 266 | while (st->id != STATE_ID::FINISHED) { 267 | auto actions = st->get_action_space(); 268 | // cout << st->idx << ": " << static_cast(st->id) << ", "; 269 | // cout << actions.size() << endl; 270 | auto last_st = st; 271 | st = step(*st, actions[rand() % actions.size()]); 272 | if (!first_time) { 273 | delete last_st; 274 | } 275 | if (first_time) { 276 | first_time = false; 277 | } 278 | } 279 | float r = 0; 280 | // cout << st->winner << endl; 281 | if (st->winner == idx) { 282 | r = 1.f; 283 | } else if (st->winner >= 0) { 284 | r = -1.f; 285 | } else { 286 | r = 0; 287 | } 288 | delete st; 289 | return r; 290 | } 291 | 292 | vector MCTree::predict(float temp) { 293 | int max_n = 0; 294 | int max_id = 0; 295 | for (int i = 0; i < root->edges.size(); i++) { 296 | // cout << root->edges[i]->q << ", "; 297 | if (root->edges[i]->n > max_n) { 298 | max_n = root->edges[i]->n; 299 | max_id = i; 300 | } 301 | } 302 | // cout << endl; 303 | return root->edges[max_id]->action; 304 | } 305 | 306 | 307 | State* step(const State& s, const vector& a) { 308 | State* sprime = new State(s); 309 | 310 | if (sprime->id == STATE_ID::CHOW) { 311 | sprime->id = STATE_ID::DISCARD; 312 | if (a.size() > 0) { 313 | assert(a.size() == 2); 314 | sprime->players[sprime->idx]->remove(a); 315 | } else { 316 | // cout << sprime->remain_cards.size() << endl; 317 | sprime->players[sprime->idx]->add({sprime->remain_cards.back()}); 318 | sprime->remain_cards.pop_back(); 319 | // for (int i = 0; i < sprime->players[sprime->idx]->cnt.size(); i++) { 320 | // cout << sprime->players[sprime->idx]->cnt[i] << ","; 321 | // } 322 | // cout << endl; 323 | if (sprime->players[sprime->idx]->can_complete()) { 324 | // cout << sprime->idx << endl; 325 | sprime->id = STATE_ID::FINISHED; 326 | sprime->winner = sprime->idx; 327 | } 328 | } 329 | } else if (sprime->id == STATE_ID::DISCARD) { 330 | sprime->last_tile = a[0]; 331 | sprime->players[sprime->idx]->remove(a); 332 | sprime->id = STATE_ID::PUNG1; 333 | for (int i = sprime->idx + 1; i < sprime->idx + 4; i++) { 334 | if (sprime->players[i % 4]->can_complete(sprime->last_tile)) { 335 | sprime->id = STATE_ID::FINISHED; 336 | sprime->winner = i % 4; 337 | break; 338 | } 339 | } 340 | if (sprime->remain_cards.empty() && sprime->id != STATE_ID::FINISHED) { 341 | sprime->id = STATE_ID::FINISHED; 342 | sprime->winner = -1; 343 | } 344 | } else if (STATE_ID::PUNG1 <= sprime->id && sprime->id <= STATE_ID::PUNG3) { 345 | if (a.size() > 0) { 346 | sprime->idx = (sprime->idx + 1 + static_cast(sprime->id) - static_cast(STATE_ID::PUNG1)) % 4; 347 | sprime->players[sprime->idx]->remove(a); 348 | sprime->id = STATE_ID::DISCARD; 349 | } else { 350 | if (sprime->id == STATE_ID::PUNG3) { 351 | sprime->idx = (sprime->idx + 1) % 4; 352 | sprime->id = STATE_ID::CHOW; 353 | } else { 354 | sprime->id = static_cast(static_cast(sprime->id) + 1); 355 | } 356 | } 357 | } 358 | return sprime; 359 | } 360 | -------------------------------------------------------------------------------- /c_src/mctree.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "player.h" 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace std; 12 | 13 | 14 | enum class STATE_ID { 15 | CHOW = 0, 16 | DISCARD = 1, 17 | PUNG1 = 2, 18 | PUNG2 = 3, 19 | PUNG3 = 4, 20 | FINISHED = 5 21 | }; 22 | 23 | 24 | 25 | class State { 26 | public: 27 | TILE_TYPE last_tile; 28 | int idx = 0; 29 | int winner = 10; 30 | STATE_ID id; 31 | vector players; 32 | vectorremain_cards; 33 | 34 | State(STATE_ID id, TILE_TYPE last_tile, 35 | const vector>& handcards, 36 | const vector& deck, int idx); 37 | State(const State&); 38 | 39 | ~State(); 40 | 41 | vector> get_action_space(); 42 | 43 | }; 44 | 45 | 46 | class Edge; 47 | 48 | class Node { 49 | public: 50 | State* st = nullptr; 51 | vector> actions; 52 | Edge* src = nullptr; 53 | vector edges; 54 | std::mutex mu; 55 | 56 | Node(Edge* src, State* st, vector priors = vector()); 57 | 58 | ~Node(); 59 | 60 | Edge* choose(float c); 61 | }; 62 | 63 | 64 | class Edge { 65 | public: 66 | vector action; 67 | int n = 0; 68 | float w = 0.f; 69 | float q = 0.f; 70 | bool terminiated = false; 71 | std::shared_timed_mutex mu; 72 | float r = 0.f; 73 | float p = 0.f; 74 | Node* src = nullptr; 75 | Node* dest = nullptr; 76 | 77 | Edge(Node* src, const vector& action, float prior); 78 | 79 | ~Edge(); 80 | }; 81 | 82 | 83 | class MCTree { 84 | public: 85 | Node* root = nullptr; 86 | int idx = -1; 87 | int counter = 0; 88 | float c = 0; 89 | std::mutex counter_mu; 90 | 91 | MCTree(State*, int idx, float c); 92 | 93 | ~MCTree(); 94 | 95 | void search(int n_threads, int n); 96 | void search_thread(unsigned int*); 97 | Node* explore(Node* node, float& val, unsigned int*); 98 | void backup(Node* node, float val); 99 | float rollout(Node* node, unsigned int*); 100 | vector predict(float temp); 101 | }; 102 | 103 | 104 | State* step(const State& s, const vector& a); -------------------------------------------------------------------------------- /c_src/player.cpp: -------------------------------------------------------------------------------- 1 | #include "player.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | extern unordered_set complete_keys; 8 | 9 | template< typename T, typename Pred > 10 | typename std::vector::iterator 11 | insert_sorted( std::vector & vec, T const& item, Pred pred ) 12 | { 13 | return vec.insert 14 | ( 15 | std::upper_bound( vec.begin(), vec.end(), item, pred ), 16 | item 17 | ); 18 | } 19 | 20 | Player::Player(const vector& tiles) { 21 | this->cnt.resize(34, 0); 22 | this->add(tiles); 23 | } 24 | 25 | Player::Player(const Player& p) { 26 | this->cnt = p.cnt; 27 | this->tiles = p.tiles; 28 | } 29 | 30 | void Player::add(const vector& tiles) { 31 | for (const auto& t : tiles) { 32 | this->cnt[static_cast(t)]++; 33 | insert_sorted(this->tiles, t, [](const TILE_TYPE& t1, const TILE_TYPE& t2) { 34 | return static_cast(t1) < static_cast(t2); 35 | }); 36 | 37 | } 38 | } 39 | 40 | void Player::remove(const vector& tiles) { 41 | for (const auto& t : tiles) { 42 | this->cnt[static_cast(t)]--; 43 | auto pr = std::equal_range(std::begin(this->tiles), std::end(this->tiles), t); 44 | assert(pr.first < this->tiles.end()); 45 | this->tiles.erase(pr.first, pr.first + 1); 46 | } 47 | } 48 | 49 | bool Player::can_complete(const TILE_TYPE& tile) { 50 | if (tile != TILE_TYPE::NONE) { 51 | cnt[static_cast(tile)]++; 52 | } 53 | bool b = false; 54 | int x = 0, p = -1; 55 | for (int i = 0; i < 3; i++) { 56 | for (int j = 0; j < 9; j++) { 57 | if (cnt[i * 9 + j] == 0) { 58 | if (b) { 59 | b = false; 60 | x |= 0x1 << p; 61 | p++; 62 | } 63 | } else { 64 | p++; 65 | b = true; 66 | if (cnt[i * 9 + j] == 2) { 67 | x |= 0x3 << p; 68 | p += 2; 69 | } else if (cnt[i * 9 + j] == 3) { 70 | x |= 0xf << p; 71 | p += 4; 72 | } else if (cnt[i * 9 + j] == 4) { 73 | x |= 0x3f << p; 74 | p += 6; 75 | } 76 | } 77 | } 78 | if (b) { 79 | b = false; 80 | x |= 0x1 << p; 81 | p++; 82 | } 83 | } 84 | for (int i = 27; i < 34; i++) { 85 | if (cnt[i] > 0) { 86 | p++; 87 | if (cnt[i] == 2) { 88 | x |= 0x3 << p; 89 | p += 2; 90 | } else if (cnt[i] == 3) { 91 | x |= 0xf << p; 92 | p += 4; 93 | } else if (cnt[i] == 4) { 94 | x |= 0x3f << p; 95 | p += 6; 96 | } 97 | x |= 0x1 << p; 98 | p++; 99 | } 100 | } 101 | 102 | 103 | 104 | 105 | if (tile != TILE_TYPE::NONE) { 106 | cnt[static_cast(tile)]--; 107 | } 108 | assert(!complete_keys.empty()); 109 | return complete_keys.find(x) != complete_keys.end(); 110 | } 111 | 112 | bool Player::can_chow(const TILE_TYPE& tile, vector>& sols) { 113 | if (tile == TILE_TYPE::NONE) { 114 | return false; 115 | } 116 | 117 | vector> arr = {{-2, -1}, {-1, 1}, {1, 2}}; 118 | bool can = false; 119 | for (int bounds = 9; bounds <= 27; bounds += 9) { 120 | if (static_cast(tile) >= bounds - 9 && static_cast(tile) < bounds) { 121 | for (int i = 0; i < 3; i++) { 122 | auto val_before = static_cast(tile) + arr[i][0]; 123 | auto val_after = static_cast(tile) + arr[i][1]; 124 | if (val_before >= bounds - 9 && val_after < bounds && cnt[val_before] >= 1 && cnt[val_after] >= 1) { 125 | can = true; 126 | sols.push_back({static_cast(val_before), static_cast(val_after)}); 127 | } 128 | } 129 | } 130 | } 131 | return can; 132 | } 133 | 134 | bool Player::can_pung(const TILE_TYPE& tile) { 135 | return cnt[static_cast(tile)] >= 2; 136 | } -------------------------------------------------------------------------------- /c_src/player.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | using namespace std; 4 | 5 | 6 | enum class TILE_TYPE { 7 | BAMBOO_ONE = 0, 8 | BAMBOO_TWO = 1, 9 | BAMBOO_THREE = 2, 10 | BAMBOO_FOUR = 3, 11 | BAMBOO_FIVE = 4, 12 | BAMBOO_SIX = 5, 13 | BAMBOO_SEVEN = 6, 14 | BAMBOO_EIGHT = 7, 15 | BAMBOO_NINE = 8, 16 | CIRCLE_ONE = 9, 17 | CIRCLE_TWO = 10, 18 | CIRCLE_THREE = 11, 19 | CIRCLE_FOUR = 12, 20 | CIRCLE_FIVE = 13, 21 | CIRCLE_SIX = 14, 22 | CIRCLE_SEVEN = 15, 23 | CIRCLE_EIGHT =16, 24 | CIRCLE_NINE = 17, 25 | WAN_ONE = 18, 26 | WAN_TWO = 19, 27 | WAN_THREE = 20, 28 | WAN_FOUR = 21, 29 | WAN_FIVE = 22, 30 | WAN_SIX = 23, 31 | WAN_SEVEN = 24, 32 | WAN_EIGHT = 25, 33 | WAN_NINE = 26, 34 | SPECIAL_RED_DRAGON = 27, 35 | SPECIAL_GREEN_DRAGON = 28, 36 | SPECIAL_WHITE_DRAGON = 29, 37 | SPECIAL_EAST = 30, 38 | SPECIAL_SOUTH = 31, 39 | SPECIAL_WEST = 32, 40 | SPECIAL_NORTH = 33, 41 | NONE = 100 42 | }; 43 | 44 | class Player { 45 | 46 | 47 | 48 | public: 49 | vector cnt; 50 | vector tiles; 51 | Player(const vector& tiles); 52 | Player(const Player&); 53 | void add(const vector& tiles); 54 | void remove(const vector& tiles); 55 | 56 | bool can_complete(const TILE_TYPE& tile = TILE_TYPE::NONE); 57 | bool can_chow(const TILE_TYPE& tile, vector>& sols); 58 | bool can_pung(const TILE_TYPE& tile); 59 | }; 60 | 61 | -------------------------------------------------------------------------------- /complete.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/complete.pickle -------------------------------------------------------------------------------- /controller.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import resource 3 | import graphics 4 | import players 5 | import random 6 | import os 7 | import sprites 8 | from build.Release.clib import TILE_TYPE 9 | 10 | 11 | class Controller(): 12 | def __init__(self, src_dir): 13 | resource_mgr = resource.Resource() 14 | resource_mgr.load(src_dir) 15 | self._graphic_mgr = graphics.Graphics(resource_mgr) 16 | self._last_tick = 0 17 | self._current_turn = 0 18 | self._last_tile = None 19 | self._all_tiles = [] 20 | self._clock = pygame.time.Clock() 21 | self._players = [players.HumanPlayer(self._graphic_mgr, players.Player.POSITION.SOUTH, u'南大'), 22 | players.AIPlayer(self._graphic_mgr, players.Player.POSITION.EAST, u'东大'), 23 | players.AIPlayer(self._graphic_mgr, players.Player.POSITION.NORTH, u'北大'), 24 | players.AIPlayer(self._graphic_mgr, players.Player.POSITION.WEST, u'西大')] 25 | self._graphic_mgr.catch_players(self._players) 26 | self._graphic_mgr.clock = self._clock 27 | self._can_draw = False 28 | self._cache_text = None 29 | self.reset() 30 | 31 | def reset(self): 32 | self._current_turn = 0 33 | self._last_tile = TILE_TYPE.NONE 34 | self._can_draw = False 35 | self._cache_text = None 36 | self._all_tiles = [] 37 | self._graphic_mgr.reset() 38 | for _ in range(4): 39 | self._all_tiles.extend([TILE_TYPE(i) for i in range(34)]) 40 | random.shuffle(self._all_tiles) 41 | for player in self._players: 42 | player.reset() 43 | player.add(self._all_tiles[:13]) 44 | self._all_tiles = self._all_tiles[13:] 45 | # self._players[0].add(self._all_tiles.pop(0)) 46 | for player in self._players: 47 | player.refresh() 48 | 49 | def step(self, draw=True): 50 | if len(self._all_tiles) == 0: 51 | self._graphic_mgr.show_text(u'游戏结束') 52 | pygame.display.update() 53 | self._clock.tick(0.5) 54 | self.reset() 55 | return 56 | 57 | if self._players[self._current_turn].respond_chow(self._last_tile): 58 | return self.step(False) 59 | 60 | if draw: 61 | self._players[self._current_turn].add(self._all_tiles.pop(0)) 62 | self._players[self._current_turn].refresh() 63 | if self._players[self._current_turn].respond_complete(): 64 | self.reset() 65 | self._graphic_mgr.show_text(u'游戏结束') 66 | pygame.display.update() 67 | self._clock.tick(0.5) 68 | return 69 | 70 | self._last_tile = self._players[self._current_turn].respond_normal() 71 | 72 | # self._last_tile = self._players[self._current_turn].remove(self._players[self._current_turn]._tiles[0]) 73 | 74 | self._graphic_mgr.add_player_sprite_by_type(self._players[self._current_turn], self._last_tile, (self._graphic_mgr._width // 2, self._graphic_mgr._height // 8 * 6), draw=True) 75 | if self._cache_text and self._cache_text.alive(): 76 | self._cache_text.kill() 77 | self._cache_text = sprites.Text(u'%s 出了牌' % self._players[self._current_turn]._name, [self._graphic_mgr._width / 2 - 60, self._graphic_mgr._height / 2 - 75]) 78 | self._graphic_mgr.add_sprite(self._cache_text) 79 | for i in range(self._current_turn + 1, self._current_turn + 4): 80 | if self._players[i % 4].respond_complete(self._last_tile): 81 | self.reset() 82 | self._graphic_mgr.show_text(u'游戏结束') 83 | pygame.display.update() 84 | self._clock.tick(0.5) 85 | return 86 | for i in range(self._current_turn + 1, self._current_turn + 4): 87 | if self._players[i % 4].respond_pung(self._last_tile): 88 | self._current_turn = i % 4 89 | self._last_tile = TILE_TYPE.NONE 90 | return self.step(False) 91 | 92 | self._current_turn = (self._current_turn + 1) % 4 93 | 94 | def run(self, events): 95 | self._graphic_mgr.clear() 96 | self._graphic_mgr.draw_others() 97 | for player in self._players: 98 | self._graphic_mgr.draw_player_sprites(player) 99 | if self._graphic_mgr.handle(events): 100 | pygame.quit() 101 | # rot_surf = pygame.transform.rotate(self._graphic_mgr._layer, 90) 102 | # self._graphic_mgr._screen.blit(rot_surf, (0, 0)) 103 | 104 | self.step() 105 | self._clock.tick(30) 106 | 107 | 108 | if __name__ == '__main__': 109 | # print(pygame.font.get_fonts()) 110 | ctrlr = Controller('./tiles') 111 | 112 | pygame.init() 113 | done = False 114 | while True: 115 | ctrlr.run(pygame.event.get()) 116 | pygame.display.flip() 117 | -------------------------------------------------------------------------------- /graphics.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sprites 3 | import players 4 | import math 5 | import six 6 | import functools 7 | 8 | 9 | def get_rot_by_player_pos(pos): 10 | rot = 0 11 | if pos == players.Player.POSITION.SOUTH: 12 | rot = 0 13 | elif pos == players.Player.POSITION.NORTH: 14 | rot = 180 15 | elif pos == players.Player.POSITION.WEST: 16 | rot = 270 17 | elif pos == players.Player.POSITION.EAST: 18 | rot = 90 19 | return rot 20 | 21 | 22 | def rotate_rect(rect, rot, c): 23 | 24 | center = rect.x + rect.width / 2 - c[0], rect.y + rect.height / 2 - c[1] 25 | rotate_center = math.cos(rot / 180 * math.pi) * center[0] + math.sin(rot / 180 * math.pi) * center[1] + c[0], \ 26 | -math.sin(rot / 180 * math.pi) * center[0] + math.cos(rot / 180 * math.pi) * center[1] + c[1] 27 | if rot % 180 == 90: 28 | return pygame.Rect(rotate_center[0] - rect.height / 2, rotate_center[1] - rect.width / 2, rect.height, rect.width) 29 | else: 30 | return pygame.Rect(rotate_center[0] - rect.width / 2, rotate_center[1] - rect.height / 2, rect.width, rect.height) 31 | 32 | 33 | __all__ = ['Graphics'] 34 | 35 | 36 | class Graphics: 37 | def __init__(self, src_mgr): 38 | self._resource_mgr = src_mgr 39 | self._width, self._height = 800, 800 40 | self._screen = pygame.display.set_mode((self._width, self._height)) 41 | self._layers = dict() 42 | # self._layer.fill((255, 0, 0)) 43 | 44 | self._player_sprites = dict() 45 | self._other_sprites = pygame.sprite.OrderedUpdates() 46 | self._cached_out_tile = dict() 47 | pygame.font.init() 48 | self._font = pygame.font.SysFont('wenquanyimicroheimono', 30) 49 | # for _, src in self._resource_mgr.resources.items(): 50 | # self._all_sprites.add(sprites.Tile(src)) 51 | 52 | def show_text(self, txt, pos=None): 53 | if not pos: 54 | pos = (self._width / 2 - 60, self._height / 2 - 15) 55 | surf = self._font.render(txt, True, (0, 204, 102)) 56 | self._screen.blit(surf, pos) 57 | 58 | def catch_players(self, players): 59 | for player in players: 60 | self._player_sprites[player] = pygame.sprite.OrderedUpdates() 61 | self._layers[player] = pygame.Surface((self._width, self._height), pygame.SRCALPHA, 32) 62 | self._layers[player].convert_alpha() 63 | 64 | def reset(self): 65 | for _, sprites in self._player_sprites.items(): 66 | sprites.empty() 67 | self._other_sprites = pygame.sprite.Group() 68 | 69 | def clear_player_sprites(self, player): 70 | if player in self._player_sprites: 71 | self._player_sprites[player].empty() 72 | 73 | def add_player_sprite_by_type(self, player, t, pos=(0, 0), **kwargs): 74 | tile = sprites.Tile(self._resource_mgr.resources[t] if (isinstance(player, players.HumanPlayer) or ('draw' in kwargs and kwargs['draw'])) else self._resource_mgr.resources['hidden'], t) 75 | tile.rect.x = pos[0] 76 | tile.rect.y = pos[1] 77 | for k, v in six.iteritems(kwargs): 78 | setattr(tile, k, v) 79 | if k == 'on_click': 80 | tile.on_click = functools.partial(v, tile) 81 | tile.rot = get_rot_by_player_pos(player._pos) 82 | self._player_sprites[player].add(tile) 83 | return tile 84 | 85 | def add_player_sprite(self, player, sprite): 86 | self._player_sprites[player].add(sprite) 87 | 88 | def remove_player_sprite_by_type(self, player, t): 89 | if player not in self._player_sprites: 90 | raise Exception('player not found') 91 | to_del = None 92 | for sprite in self._player_sprites[player]: 93 | if sprite._type == t: 94 | to_del = sprite 95 | break 96 | self._player_sprites[player].remove(to_del) 97 | 98 | def draw_player_sprites(self, player): 99 | if player not in self._player_sprites: 100 | raise Exception('player not found') 101 | 102 | self._player_sprites[player].draw(self._layers[player]) 103 | rot = get_rot_by_player_pos(player._pos) 104 | rot_surf = pygame.transform.rotate(self._layers[player], rot) 105 | self._screen.blit(rot_surf, (0, 0)) 106 | 107 | def add_sprite(self, sprite): 108 | self._other_sprites.add(sprite) 109 | 110 | def clear(self): 111 | self._screen.fill((255, 255, 255)) 112 | for layer in self._layers: 113 | self._layers[layer].fill((255, 255, 255, 0)) 114 | 115 | def draw_all(self): 116 | for player in self._player_sprites: 117 | self.draw_player_sprites(player) 118 | self.draw_others() 119 | 120 | def draw_others(self): 121 | self._other_sprites.draw(self._screen) 122 | 123 | def handle(self, events): 124 | for event in events: 125 | if event.type == pygame.MOUSEBUTTONUP: 126 | pos = pygame.mouse.get_pos() 127 | 128 | for s in self._other_sprites: 129 | if s.rect.collidepoint(pos) and hasattr(s, 'on_click'): 130 | s.on_click() 131 | for player in self._player_sprites: 132 | for s in self._player_sprites[player]: 133 | if rotate_rect(s.rect, s.rot, (self._width / 2, self._height / 2)).collidepoint(pos) and hasattr(s, 'on_click'): 134 | s.on_click() 135 | if event.type == pygame.QUIT: 136 | return True 137 | return False 138 | -------------------------------------------------------------------------------- /montecarlo.py: -------------------------------------------------------------------------------- 1 | # import threading 2 | import math 3 | import numpy as np 4 | from time import sleep 5 | import copy 6 | import itertools 7 | from multiprocessing import Process, Lock 8 | import random 9 | 10 | 11 | # TODO : step need to pass AI players 12 | # TODO: random shuffle game state when player is unaware 13 | class State: 14 | class STATE_ID: 15 | CHOW = 0 16 | DISCARD = 1 17 | PUNG1 = 2 18 | PUNG2 = 3 19 | PUNG3 = 4 20 | FINISHED = 5 21 | 22 | def __init__(self, state_id, last_tile, players, remain_cards, idx): 23 | self.last_tile = last_tile 24 | self.players = players 25 | self.idx = idx 26 | self.state_id = state_id 27 | self.remain_cards = remain_cards 28 | self.winner = -1 29 | 30 | def get_action_space(self): 31 | if self.state_id == State.STATE_ID.CHOW: 32 | can_chow, sols = self.players[self.idx].can_chow(self.last_tile) 33 | return [None, *sols] if can_chow else [None] 34 | elif self.state_id == State.STATE_ID.DISCARD: 35 | return self.players[self.idx]._tiles 36 | elif State.STATE_ID.PUNG1 <= self.state_id <= State.STATE_ID.PUNG3: 37 | can_pung = self.players[(self.idx + 1 + self.state_id - State.STATE_ID.PUNG1) % 4].can_pung(self.last_tile) 38 | return [False, True] if can_pung else [False] 39 | elif self.state_id == State.STATE_ID.FINISHED: 40 | return [] 41 | else: 42 | raise Exception('invalid state id') 43 | 44 | @staticmethod 45 | def step(s, a): 46 | sprime = copy.deepcopy(s) 47 | if sprime.state_id == State.STATE_ID.CHOW: 48 | sprime.state_id = State.STATE_ID.DISCARD 49 | if a is not None: 50 | sprime.players[sprime.idx].remove(a) 51 | else: 52 | sprime.players[sprime.idx].add(sprime.remain_cards.pop(0)) 53 | if sprime.players[sprime.idx].respond_complete(): 54 | sprime.state_id = State.STATE_ID.FINISHED 55 | sprime.winner = sprime.idx 56 | elif sprime.state_id == State.STATE_ID.DISCARD: 57 | sprime.last_tile = a 58 | sprime.players[sprime.idx].remove(a) 59 | sprime.state_id = State.STATE_ID.PUNG1 60 | for i in range(sprime.idx + 1, sprime.idx + 4): 61 | if sprime.players[i % 4].respond_complete(sprime.last_tile): 62 | sprime.state_id = State.STATE_ID.FINISHED 63 | sprime.winner = i % 4 64 | break 65 | if len(sprime.remain_cards) == 0 and sprime.state_id != State.STATE_ID.FINISHED: 66 | sprime.state_id = State.STATE_ID.FINISHED 67 | sprime.winner = -1 68 | elif State.STATE_ID.PUNG1 <= sprime.state_id <= State.STATE_ID.PUNG3: 69 | if a: 70 | sprime.idx = (sprime.idx + 1 + sprime.state_id - State.STATE_ID.PUNG1) % 4 71 | sprime.players[sprime.idx].remove([sprime.last_tile, sprime.last_tile]) 72 | sprime.state_id = State.STATE_ID.DISCARD 73 | else: 74 | sprime.state_id += 1 75 | if sprime.state_id > State.STATE_ID.PUNG3: 76 | sprime.idx = (sprime.idx + 1) % 4 77 | sprime.state_id = State.STATE_ID.CHOW 78 | return sprime 79 | 80 | 81 | class Node: 82 | def __init__(self, src, state, priors=None): 83 | self.state = state 84 | self.a = state.get_action_space() 85 | if not priors: 86 | priors = np.ones([len(self.a)]) * 1. / len(self.a) 87 | self.src = src 88 | self.edges = [] 89 | self.lock = Lock() 90 | for i in range(len(self.a)): 91 | self.edges.append(Edge(self, self.a[i], priors[i])) 92 | 93 | def choose(self, c): 94 | nsum_sqrt = math.sqrt(sum([e.n for e in self.edges])) 95 | cands = [e.q + c * e.p * nsum_sqrt / (1 + e.n) for e in self.edges] 96 | return self.edges[np.argmax(cands)] 97 | 98 | 99 | class Edge: 100 | def __init__(self, src, action, prior): 101 | self.a = action 102 | self.n = 0 103 | self.w = 0 104 | self.q = 0 105 | self.terminated = False 106 | self.lock = Lock() 107 | self.r = 0 108 | self.p = prior 109 | self.src = src 110 | self.node = None 111 | 112 | 113 | class MCTree: 114 | def __init__(self, state, idx): 115 | self.root = Node(None, state) 116 | self.idx = idx 117 | self.counter = 0 118 | self.counter_lock = Lock() 119 | 120 | def rollout(self, node): 121 | state = node.state 122 | while state.state_id != State.STATE_ID.FINISHED: 123 | state = State.step(state, random.choice(state.get_action_space())) 124 | if state.winner == self.idx: 125 | r = 1 126 | elif state.winner >= 0: 127 | r = -1 128 | else: 129 | r = 0 130 | return r 131 | 132 | def search(self, nthreads, n): 133 | self.counter = n 134 | procs = [] 135 | for i in range(nthreads): 136 | p = Process(target=self.search_thread, args=()) 137 | procs.append(p) 138 | p.start() 139 | sleep(0.05) 140 | for p in procs: 141 | p.join() 142 | print([e.n for e in self.root.edges]) 143 | 144 | def search_thread(self): 145 | while True: 146 | self.counter_lock.acquire() 147 | if self.counter == 0: 148 | self.counter_lock.release() 149 | break 150 | else: 151 | self.counter -= 1 152 | self.counter_lock.release() 153 | val, leaf = self.explore(self.root) 154 | # print(val) 155 | if leaf: 156 | self.backup(leaf, val) 157 | print([e.n for e in self.root.edges]) 158 | 159 | def explore(self, node): 160 | # TODO: add virtual loss for current branch 161 | node.lock.acquire() 162 | edge = node.choose(1.) 163 | if edge.node: 164 | node.lock.release() 165 | if edge.terminated: 166 | return edge.r, edge.node 167 | else: 168 | return self.explore(edge.node) 169 | else: 170 | sprime = State.step(node.state, edge.a) 171 | edge.node = Node(edge, sprime) 172 | node.lock.release() 173 | if sprime.state_id == State.STATE_ID.FINISHED: 174 | if sprime.winner == self.idx: 175 | r = 1 176 | elif sprime.winner >= 0: 177 | r = -1 178 | else: 179 | r = 0 180 | edge.terminated = True 181 | edge.r = r 182 | return r, edge.node 183 | return self.rollout(edge.node), edge.node 184 | 185 | def backup(self, node, v): 186 | while node.src: 187 | edge = node.src 188 | edge.lock.acquire() 189 | edge.n += 1 190 | edge.w += v 191 | edge.q = edge.w / edge.n 192 | edge.lock.release() 193 | node = edge.src 194 | assert edge in node.edges 195 | if node == self.root: 196 | print(edge.n) 197 | 198 | def predict(self, temp): 199 | # print([e.n for e in self.root.edges]) 200 | probs = np.array([pow(e.n, 1. / temp) for e in self.root.edges]) 201 | print([e.n for e in self.root.edges]) 202 | probs = probs / np.sum(probs) 203 | # print(probs) 204 | 205 | return np.random.choice(self.root.a, p=probs) 206 | -------------------------------------------------------------------------------- /players.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import pygame 5 | import collections 6 | import sprites 7 | import resource 8 | from enum import Enum 9 | import numpy as np 10 | import pickle 11 | from build.Release.clib import TILE_TYPE 12 | 13 | 14 | class Player: 15 | complete_keys = pickle.load(open('complete.pickle', 'rb')) 16 | 17 | class POSITION(Enum): 18 | EAST = 1 19 | SOUTH = 2 20 | WEST = 3 21 | NORTH = 4 22 | 23 | def __init__(self, graphics, pos, name): 24 | self._name = name 25 | self._tiles = [] 26 | self._cnt = np.zeros([34]) 27 | self._graphics = graphics 28 | self._pos = pos 29 | 30 | def reset(self): 31 | self._tiles.clear() 32 | self._cnt = np.zeros([34]) 33 | 34 | def add(self, tiles): 35 | if isinstance(tiles, collections.Iterable): 36 | for tile in tiles: 37 | self.add(tile) 38 | else: 39 | self._tiles.append(tiles) 40 | 41 | self._cnt[int(tiles)] += 1 42 | self._tiles.sort(key=lambda x: int(x)) 43 | 44 | def refresh(self): 45 | self._graphics.clear_player_sprites(self) 46 | x, y = self._graphics._width // 6, self._graphics._height // 8 * 7 47 | tmp = dict() 48 | for tile in self._tiles: 49 | if isinstance(self, HumanPlayer): 50 | def on_click(a): 51 | if not a.clicked: 52 | a.rect.y -= 20 53 | a.clicked = True 54 | self._chosen_tiles.append(tmp[a]) 55 | # print(tmp[a]) 56 | else: 57 | a.rect.y += 20 58 | a.clicked = False 59 | self._chosen_tiles.remove(tmp[a]) 60 | # print(tmp[a]) 61 | tmp[self._graphics.add_player_sprite_by_type(self, tile, (x, y), on_click=on_click, clicked=False)] = tile 62 | else: 63 | self._graphics.add_player_sprite_by_type(self, tile, (x, y)) 64 | x += 40 65 | 66 | def remove(self, tiles): 67 | if isinstance(tiles, collections.Iterable): 68 | for tile in tiles: 69 | self.remove(tile) 70 | return tiles 71 | else: 72 | self._tiles.remove(tiles) 73 | self._cnt[int(tiles)] -= 1 74 | return tiles 75 | 76 | def can_complete(self, tile=TILE_TYPE.NONE): 77 | cnt = self._cnt.copy() 78 | if tile != TILE_TYPE.NONE: 79 | cnt[int(tile)] += 1 80 | b = False 81 | x = 0 82 | p = -1 83 | for i in range(3): 84 | for j in range(9): 85 | if cnt[i * 9 + j] == 0: 86 | if b: 87 | b = False 88 | x |= 0x1 << p 89 | p += 1 90 | else: 91 | p += 1 92 | b = True 93 | if cnt[i * 9 + j] == 2: 94 | x |= 0x3 << p 95 | p += 2 96 | elif cnt[i * 9 + j] == 3: 97 | x |= 0xf << p 98 | p += 4 99 | elif cnt[i * 9 + j] == 4: 100 | x |= 0x3f << p 101 | p += 6 102 | if b: 103 | b = False 104 | x |= 0x1 << p 105 | p += 1 106 | for i in range(27, 34): 107 | if cnt[i] > 0: 108 | p += 1 109 | if cnt[i] == 2: 110 | x |= 0x3 << p 111 | p += 2 112 | elif cnt[i] == 3: 113 | x |= 0xf << p 114 | p += 4 115 | elif cnt[i] == 4: 116 | x |= 0x3f << p 117 | p += 6 118 | x |= 0x1 << p 119 | p += 1 120 | return x in Player.complete_keys 121 | 122 | def can_chow(self, tile): 123 | if tile == TILE_TYPE.NONE: 124 | return False, None 125 | cnt = self._cnt 126 | arr = [[-2, -1], [-1, 1], [1, 2]] 127 | # print(cnt) 128 | 129 | can_chow = False 130 | sols = [] 131 | for bound in range(9, 28, 9): 132 | if bound - 9 <= int(tile) <= bound: 133 | for i in range(3): 134 | val = int(tile) + arr[i][0], int(tile) + arr[i][1] 135 | if val[0] >= bound - 9 and val[1] < bound and cnt[val[0]] >= 1 and cnt[val[1]] >= 1: 136 | can_chow = True 137 | sols.append([TILE_TYPE(val[0]), TILE_TYPE(val[1])]) 138 | return can_chow, sols 139 | 140 | def can_pung(self, tile): 141 | 142 | return self._cnt[int(tile)] >= 2 143 | 144 | def respond_normal(self): 145 | pass 146 | 147 | def respond_chow(self, tile): 148 | pass 149 | 150 | def respond_complete(self, tile=None): 151 | pass 152 | 153 | def respond_pung(self, tile): 154 | pass 155 | 156 | 157 | class HumanPlayer(Player): 158 | def __init__(self, graphics, pos, name): 159 | Player.__init__(self, graphics, pos, name) 160 | self._chosen_tiles = [] 161 | 162 | def reset(self): 163 | super().reset() 164 | self._chosen_tiles = [] 165 | 166 | def respond_normal(self): 167 | clicked = False 168 | btn = sprites.Button((255, 0, 0), 100, 30) 169 | btn.rect.x = 300 170 | btn.rect.y = 600 171 | 172 | txt = sprites.Text(u'请出牌', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 173 | 174 | def on_click(): 175 | if btn.alive(): 176 | nonlocal clicked, txt 177 | if len(self._chosen_tiles) != 1: 178 | # txt = u'请出一张牌' 179 | txt.kill() 180 | txt = sprites.Text(u'请出一张牌', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 181 | self._graphics.add_sprite(txt) 182 | return 183 | btn.kill() 184 | clicked = True 185 | txt.kill() 186 | 187 | self._graphics.add_sprite(btn) 188 | self._graphics.add_sprite(txt) 189 | 190 | btn.on_click = on_click 191 | while not clicked: 192 | self._graphics.clear() 193 | self._graphics.draw_all() 194 | # self._graphics.show_text(txt) 195 | pygame.display.update() 196 | self._graphics.handle(pygame.event.get()) 197 | self._graphics.clock.tick(30) 198 | tile = self._chosen_tiles[0] 199 | self.remove(tile) 200 | self._chosen_tiles.clear() 201 | self.refresh() 202 | return tile 203 | 204 | def respond_chow(self, tile): 205 | can_chow, sols = self.can_chow(tile) 206 | if can_chow: 207 | clicked = False 208 | btn = sprites.Button((255, 0, 0), 100, 30) 209 | btn.rect.x = 300 210 | btn.rect.y = 600 211 | 212 | # txt = u'要吃吗' 213 | txt = sprites.Text(u'要吃吗', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 214 | 215 | def on_click(): 216 | if btn.alive(): 217 | 218 | nonlocal clicked, txt, sols 219 | if len(self._chosen_tiles) == 0: 220 | btn.kill() 221 | txt.kill() 222 | clicked = True 223 | return 224 | for sol in sols: 225 | if sorted(self._chosen_tiles, key=lambda x: int(x)) == sorted(sol, key=lambda x: int(x)): 226 | btn.kill() 227 | txt.kill() 228 | clicked = True 229 | return 230 | txt.kill() 231 | txt = sprites.Text(u'出牌不符合规定', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 232 | self._graphics.add_sprite(txt) 233 | # txt = u'出牌不符合规定' 234 | 235 | self._graphics.add_sprite(btn) 236 | self._graphics.add_sprite(txt) 237 | 238 | btn.on_click = on_click 239 | while not clicked: 240 | self._graphics.clear() 241 | self._graphics.draw_all() 242 | # self._graphics.show_text(txt) 243 | pygame.display.update() 244 | self._graphics.handle(pygame.event.get()) 245 | self._graphics.clock.tick(30) 246 | if len(self._chosen_tiles) > 0: 247 | self.remove(self._chosen_tiles) 248 | self._chosen_tiles.clear() 249 | self.refresh() 250 | return True 251 | return False 252 | 253 | def respond_complete(self, tile=TILE_TYPE.NONE): 254 | can_complete = self.can_complete(tile) 255 | if can_complete: 256 | clicked = False 257 | btn_yes = sprites.Button((0, 255, 0), 100, 30) 258 | btn_yes.rect.x = 300 259 | btn_yes.rect.y = 600 260 | 261 | btn_no = sprites.Button((255, 0, 0), 100, 30) 262 | btn_no.rect.x = 500 263 | btn_no.rect.y = 600 264 | 265 | txt = sprites.Text(u'要胡吗', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 266 | response = False 267 | 268 | def on_click_yes(): 269 | if btn_yes.alive(): 270 | nonlocal response, clicked 271 | btn_yes.kill() 272 | clicked = True 273 | txt.kill() 274 | response = True 275 | 276 | def on_click_no(): 277 | if btn_no.alive(): 278 | nonlocal response, clicked 279 | btn_no.kill() 280 | txt.kill() 281 | clicked = True 282 | response = False 283 | 284 | self._graphics.add_sprite(btn_yes) 285 | self._graphics.add_sprite(btn_no) 286 | self._graphics.add_sprite(txt) 287 | 288 | btn_yes.on_click = on_click_yes 289 | btn_no.on_click = on_click_no 290 | while not clicked: 291 | self._graphics.clear() 292 | self._graphics.draw_all() 293 | pygame.display.update() 294 | self._graphics.handle(pygame.event.get()) 295 | self._graphics.clock.tick(30) 296 | 297 | self._graphics.clear() 298 | self._graphics.draw_all() 299 | return response 300 | else: 301 | return False 302 | 303 | def respond_pung(self, tile): 304 | can_pung = self.can_pung(tile) 305 | 306 | if can_pung: 307 | print(tile) 308 | print(self._tiles) 309 | print(self._cnt) 310 | clicked = False 311 | btn = sprites.Button((255, 0, 0), 100, 30) 312 | btn.rect.x = 300 313 | btn.rect.y = 600 314 | 315 | txt = sprites.Text(u'要碰吗', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 316 | 317 | def on_click(): 318 | if btn.alive(): 319 | 320 | nonlocal clicked, txt 321 | if len(self._chosen_tiles) == 0: 322 | btn.kill() 323 | txt.kill() 324 | clicked = True 325 | return 326 | if len(self._chosen_tiles) == 2 and int(self._chosen_tiles[0]) == int(self._chosen_tiles[1]) == int(tile): 327 | btn.kill() 328 | txt.kill() 329 | clicked = True 330 | return 331 | 332 | txt.kill() 333 | txt = sprites.Text(u'出牌不符合规定', [self._graphics._width / 2 - 60, self._graphics._height / 2 - 15]) 334 | self._graphics.add_sprite(txt) 335 | 336 | self._graphics.add_sprite(btn) 337 | self._graphics.add_sprite(txt) 338 | 339 | btn.on_click = on_click 340 | while not clicked: 341 | self._graphics.clear() 342 | self._graphics.draw_all() 343 | pygame.display.update() 344 | self._graphics.handle(pygame.event.get()) 345 | self._graphics.clock.tick(30) 346 | if len(self._chosen_tiles) > 0: 347 | self.remove(self._chosen_tiles) 348 | self._chosen_tiles.clear() 349 | self.refresh() 350 | return True 351 | return False 352 | 353 | 354 | class AIPlayer(Player): 355 | def __init__(self, graphics, pos, name): 356 | Player.__init__(self, graphics, pos, name) 357 | 358 | def respond_normal(self): 359 | ret = self.remove(self._tiles[0]) 360 | self.refresh() 361 | return ret 362 | 363 | def respond_chow(self, tile): 364 | # ret = self.remove(self._tiles[0]) 365 | # self.refresh() 366 | return False 367 | 368 | def respond_complete(self, tile=None): 369 | pass 370 | 371 | def respond_pung(self, tile): 372 | return False 373 | 374 | 375 | if __name__ == '__main__': 376 | player = Player(None, None, 'nihao') 377 | # player.add([TILE_TYPE.BAMBOO_ONE, TILE_TYPE.BAMBOO_ONE]) 378 | # print(player._cnt) 379 | # print(player.can_complete()) 380 | player.add([TILE_TYPE.BAMBOO_ONE, TILE_TYPE.BAMBOO_ONE, TILE_TYPE.BAMBOO_ONE, 381 | TILE_TYPE.BAMBOO_TWO, TILE_TYPE.BAMBOO_THREE, TILE_TYPE.BAMBOO_FOUR, 382 | TILE_TYPE.BAMBOO_FIVE, 383 | TILE_TYPE.BAMBOO_SIX, TILE_TYPE.BAMBOO_SEVEN, TILE_TYPE.SPECIAL_EAST, 384 | TILE_TYPE.SPECIAL_EAST, 385 | TILE_TYPE.SPECIAL_EAST, TILE_TYPE.SPECIAL_WEST, TILE_TYPE.SPECIAL_WEST]) 386 | print(player._cnt) 387 | print(player.can_complete()) 388 | # print('#08X' % player.can_complete()) -------------------------------------------------------------------------------- /pyenv.py: -------------------------------------------------------------------------------- 1 | 2 | import resource 3 | import random 4 | from players import Player 5 | import copy 6 | import itertools 7 | from montecarlo import MCTree 8 | import sys 9 | import numpy as np 10 | sys.path.insert(0, './build') 11 | from clib import mc_search, STATE_ID, TILE_TYPE 12 | 13 | NUM_PROCS = 8 14 | NUM_SEARCH = 100 15 | c = 1. 16 | 17 | class ACTION_TYPE: 18 | DISCARD = 0 19 | CHOW = 1 20 | PUNG = 2 21 | 22 | 23 | class RandomPlayer(Player): 24 | def __init__(self, name): 25 | Player.__init__(self, None, None, name) 26 | 27 | def respond_chow(self, tile): 28 | can_chow, sols = self.can_chow(tile) 29 | if can_chow: 30 | self.remove(random.choice(sols)) 31 | return True 32 | return False 33 | 34 | def respond_pung(self, tile): 35 | if self.can_pung(tile): 36 | self.remove([tile, tile]) 37 | return True 38 | return False 39 | 40 | def respond_normal(self): 41 | return self.remove(random.choice(self._tiles)) 42 | 43 | def respond_complete(self, tile=TILE_TYPE.NONE): 44 | return self.can_complete(tile) 45 | 46 | 47 | class MCPlayer(Player): 48 | def __init__(self, name, idx, env): 49 | Player.__init__(self, None, None, name) 50 | self.idx = idx 51 | self.env = env 52 | 53 | def respond_chow(self, tile): 54 | assert self.env._current_turn == self.idx 55 | a = mc_search(STATE_ID.CHOW, self.env._last_tile, [player._tiles for player in self.env._players], 56 | self.env._all_tiles, self.env._current_turn, self.idx, NUM_PROCS, NUM_SEARCH, c) 57 | # s = State(State.STATE_ID.CHOW, self.env._last_tile, self.env._players, self.env._all_tiles, self.env._current_turn) 58 | # mc = MCTree(s, self.env._current_turn) 59 | # mc.search(NUM_PROCS, NUM_SEARCH) 60 | # a = mc.predict(0.5) 61 | if len(a) > 0: 62 | self.remove(a) 63 | return True 64 | return False 65 | 66 | def respond_pung(self, tile): 67 | diff = self.idx - self.env._current_turn 68 | if diff < 0: 69 | diff += 4 70 | a = mc_search(STATE_ID(int(STATE_ID.PUNG1) + diff - 1), self.env._last_tile, [player._tiles for player in self.env._players], 71 | self.env._all_tiles, self.env._current_turn, self.idx, NUM_PROCS, NUM_SEARCH, c) 72 | # s = State(State.STATE_ID.PUNG1 + diff - 1, self.env._last_tile, self.env._players, self.env._all_tiles, self.env._current_turn) 73 | # mc = MCTree(s, self.idx) 74 | # mc.search(NUM_PROCS, NUM_SEARCH) 75 | # a = mc.predict(0.5) 76 | if len(a) > 0: 77 | self.remove([tile, tile]) 78 | return True 79 | return False 80 | 81 | # TODO: assume incomplete information, sample from multiple monte-carlo trees 82 | def respond_normal(self): 83 | assert self.env._current_turn == self.idx 84 | a = mc_search(STATE_ID.DISCARD, self.env._last_tile, [player._tiles for player in self.env._players], 85 | self.env._all_tiles, self.env._current_turn, self.idx, NUM_PROCS, NUM_SEARCH, c) 86 | # s = State(State.STATE_ID. DISCARD, self.env._last_tile, self.env._players, self.env._all_tiles, self.env._current_turn) 87 | # mc = MCTree(s, self.env._current_turn) 88 | # mc.search(NUM_PROCS, NUM_SEARCH) 89 | # a = mc.predict(0.5) 90 | return self.remove(a[0]) 91 | 92 | def respond_complete(self, tile=TILE_TYPE.NONE): 93 | return self.can_complete(tile) 94 | 95 | 96 | class Env: 97 | ALL_TILES = [] 98 | for _ in range(4): 99 | ALL_TILES.extend([TILE_TYPE(i) for i in range(34)]) 100 | 101 | def __init__(self): 102 | self._current_turn = 0 103 | self._players = [RandomPlayer('player'), 104 | # RandomPlayer('player'), 105 | MCPlayer(None, 1, self), 106 | RandomPlayer('player'), 107 | RandomPlayer('player'), 108 | 109 | ] 110 | self._last_tile = TILE_TYPE.NONE 111 | self._control_player = 0 112 | self._all_tiles = [] 113 | 114 | def reset(self): 115 | self._last_tile = TILE_TYPE.NONE 116 | self._all_tiles = Env.ALL_TILES.copy() 117 | random.shuffle(self._all_tiles) 118 | for player in self._players: 119 | player.reset() 120 | player.add(self._all_tiles[:13]) 121 | self._all_tiles = self._all_tiles[13:] 122 | 123 | def step(self, draw=True): 124 | # print('stepped') 125 | # cards all out 126 | 127 | if self._last_tile != TILE_TYPE.NONE and self._players[self._current_turn].respond_chow(self._last_tile): 128 | draw = False 129 | 130 | if draw: 131 | self._players[self._current_turn].add(self._all_tiles.pop(0)) 132 | if self._players[self._current_turn].respond_complete(): 133 | self.reset() 134 | return self._current_turn, True 135 | 136 | self._last_tile = self._players[self._current_turn].respond_normal() 137 | for i in range(self._current_turn + 1, self._current_turn + 4): 138 | if self._players[i % 4].respond_complete(self._last_tile): 139 | self.reset() 140 | return i % 4, True 141 | if len(self._all_tiles) == 0: 142 | self.reset() 143 | return -1, True 144 | 145 | for i in range(self._current_turn + 1, self._current_turn + 4): 146 | if self._players[i % 4].respond_pung(self._last_tile): 147 | self._current_turn = i % 4 148 | self._last_tile = TILE_TYPE.NONE 149 | return self.step(False) 150 | 151 | self._current_turn = (self._current_turn + 1) % 4 152 | return self._current_turn, False 153 | 154 | # def run(self): 155 | # draw = True 156 | # while True: 157 | # # cards all out 158 | # if len(self._all_tiles) == 0: 159 | # self.reset() 160 | # return -1, True 161 | # 162 | # if self._last_tile: 163 | # yield self._current_turn, ACTION_TYPE.CHOW 164 | # if not self._searching and self._current_turn == self._control_player: 165 | # mc = MCTree(self, self._control_player, ACTION_TYPE.CHOW) 166 | # mc.search(4, 100) 167 | # a = mc.predict(0.5) 168 | # 169 | # def foo(_): 170 | # self._players[self._current_turn].remove(a) 171 | # return True 172 | # 173 | # self._players[self._current_turn].respond_chow = foo if a is not None else lambda _: False 174 | # if self._players[self._current_turn].respond_chow(self._last_tile): 175 | # draw = False 176 | # continue 177 | # 178 | # if draw: 179 | # self._players[self._current_turn].add(self._all_tiles.pop(0)) 180 | # if self._players[self._current_turn].respond_complete(): 181 | # self.reset() 182 | # return self._current_turn, True 183 | # 184 | # yield self._current_turn, ACTION_TYPE.DISCARD 185 | # if not self._searching and self._current_turn == self._control_player: 186 | # mc = MCTree(self, self._current_turn, ACTION_TYPE.DISCARD) 187 | # mc.search(4, 100) 188 | # a = mc.predict(0.5) 189 | # self._players[self._current_turn].respond_normal = lambda: self._players[self._current_turn].remove(self._players[self._current_turn]._tiles[a]) 190 | # self._last_tile = self._players[self._current_turn].respond_normal() 191 | # for i in range(self._current_turn + 1, self._current_turn + 4): 192 | # if self._players[i % 4].respond_complete(self._last_tile): 193 | # self.reset() 194 | # return i % 4, True 195 | # 196 | # jump_turn = False 197 | # for i in range(self._current_turn + 1, self._current_turn + 4): 198 | # yield i, ACTION_TYPE.PUNG 199 | # if not self._searching and i == self._control_player: 200 | # mc = MCTree(self, i, ACTION_TYPE.PUNG) 201 | # mc.search(4, 100) 202 | # a = mc.predict(0.5) 203 | # 204 | # def foo(tile): 205 | # self._players[i].remove([tile, tile]) 206 | # return True 207 | # 208 | # self._players[i].respond_pung = foo if a else lambda _: False 209 | # if self._players[i % 4].respond_pung(self._last_tile): 210 | # self._current_turn = i % 4 211 | # self._last_tile = None 212 | # jump_turn = True 213 | # draw = False 214 | # break 215 | # 216 | # if jump_turn: 217 | # continue 218 | # 219 | # self._current_turn = (self._current_turn + 1) % 4 220 | # draw = True 221 | # 222 | # def step(self, idx, a_type, a): 223 | # if a_type == ACTION_TYPE.DISCARD: 224 | # self._players[idx].respond_normal = lambda: self._players[idx].remove(self._players[idx]._tiles[a]) 225 | # elif a_type == ACTION_TYPE.CHOW: 226 | # def foo(_): 227 | # self._players[idx].remove(a) 228 | # return True 229 | # self._players[idx].respond_chow = foo if a is not None else lambda _: False 230 | # elif a_type == ACTION_TYPE.PUNG: 231 | # def foo(tile): 232 | # self._players[idx].remove([tile, tile]) 233 | # return True 234 | # self._players[idx].respond_pung = foo if a else lambda _: False 235 | # 236 | # r = 0 237 | # a_type = None 238 | # try: 239 | # current_player = -1 240 | # while current_player != idx: 241 | # current_player, a_type = next(self._run) 242 | # done = False 243 | # except StopIteration as e: 244 | # winner = e.value[0] 245 | # if winner == idx: 246 | # r = 1 247 | # elif winner == -1: 248 | # r = 0 249 | # else: 250 | # r = -1 251 | # done = True 252 | # return r, done, a_type 253 | # 254 | # def get_action_space(self, idx, a_type): 255 | # if a_type == ACTION_TYPE.DISCARD: 256 | # return list(range(len(self._players[idx]._tiles))) 257 | # elif a_type == ACTION_TYPE.CHOW: 258 | # can_chew, sols = self._players[idx].can_chow(self._last_tile) 259 | # return [None, *sols] if can_chew else [None] 260 | # elif a_type == ACTION_TYPE.PUNG: 261 | # can_pung = self._players[idx].can_pung(self._last_tile) 262 | # return [False, True] if can_pung else [False] 263 | 264 | 265 | if __name__ == '__main__': 266 | env = Env() 267 | random.seed(0) 268 | cnts = [0 for i in range(5)] 269 | for i in range(100): 270 | env.reset() 271 | done = False 272 | while not done: 273 | winner, done = env.step() 274 | if winner == -1: 275 | cnts[0] += 1 276 | else: 277 | cnts[winner + 1] += 1 278 | print('finished') 279 | 280 | for i in range(5): 281 | print(cnts[i] / 1) 282 | 283 | -------------------------------------------------------------------------------- /resource.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import pygame 4 | from enum import Enum 5 | from build.Release.clib import TILE_TYPE 6 | 7 | 8 | # class TileType: 9 | # 10 | # class BAMBOO(Enum): 11 | # ONE = 0 12 | # TWO = 1 13 | # THREE = 2 14 | # FOUR = 3 15 | # FIVE = 4 16 | # SIX = 5 17 | # SEVEN = 6 18 | # EIGHT = 7 19 | # NINE = 8 20 | # 21 | # class CIRCLE(Enum): 22 | # ONE = 9 23 | # TWO = 10 24 | # THREE = 11 25 | # FOUR = 12 26 | # FIVE = 13 27 | # SIX = 14 28 | # SEVEN = 15 29 | # EIGHT = 16 30 | # NINE = 17 31 | # 32 | # class WAN(Enum): 33 | # ONE = 18 34 | # TWO = 19 35 | # THREE = 20 36 | # FOUR = 21 37 | # FIVE = 22 38 | # SIX = 23 39 | # SEVEN = 24 40 | # EIGHT = 25 41 | # NINE = 26 42 | # 43 | # class SPECIAL(Enum): 44 | # RED_DRAGON = 27 45 | # GREEN_DRAGON = 28 46 | # WHITE_DRAGON = 29 47 | # EAST = 30 48 | # SOUTH = 31 49 | # WEST = 32 50 | # NORTH = 33 51 | # 52 | # @staticmethod 53 | # def same_type(a, b): 54 | # return type(a) == type(b) 55 | # 56 | # @staticmethod 57 | # def int2enum(x): 58 | # if x < 9: 59 | # return TileType.BAMBOO(x) 60 | # elif x < 18: 61 | # return TileType.CIRCLE(x) 62 | # elif x < 27: 63 | # return TileType.WAN(x) 64 | # else: 65 | # return TileType.SPECIAL(x) 66 | 67 | 68 | class Resource: 69 | fn2type = { 70 | 'MJd1': TILE_TYPE.SPECIAL_RED_DRAGON, 71 | 'MJd2': TILE_TYPE.SPECIAL_GREEN_DRAGON, 72 | 'MJd3': TILE_TYPE.SPECIAL_WHITE_DRAGON, 73 | 'MJf1': TILE_TYPE.SPECIAL_EAST, 74 | 'MJf2': TILE_TYPE.SPECIAL_SOUTH, 75 | 'MJf3': TILE_TYPE.SPECIAL_WEST, 76 | 'MJf4': TILE_TYPE.SPECIAL_NORTH, 77 | 'MJs1': TILE_TYPE.BAMBOO_ONE, 78 | 'MJs2': TILE_TYPE.BAMBOO_TWO, 79 | 'MJs3': TILE_TYPE.BAMBOO_THREE, 80 | 'MJs4': TILE_TYPE.BAMBOO_FOUR, 81 | 'MJs5': TILE_TYPE.BAMBOO_FIVE, 82 | 'MJs6': TILE_TYPE.BAMBOO_SIX, 83 | 'MJs7': TILE_TYPE.BAMBOO_SEVEN, 84 | 'MJs8': TILE_TYPE.BAMBOO_EIGHT, 85 | 'MJs9': TILE_TYPE.BAMBOO_NINE, 86 | 'MJt1': TILE_TYPE.CIRCLE_ONE, 87 | 'MJt2': TILE_TYPE.CIRCLE_TWO, 88 | 'MJt3': TILE_TYPE.CIRCLE_THREE, 89 | 'MJt4': TILE_TYPE.CIRCLE_FOUR, 90 | 'MJt5': TILE_TYPE.CIRCLE_FIVE, 91 | 'MJt6': TILE_TYPE.CIRCLE_SIX, 92 | 'MJt7': TILE_TYPE.CIRCLE_SEVEN, 93 | 'MJt8': TILE_TYPE.CIRCLE_EIGHT, 94 | 'MJt9': TILE_TYPE.CIRCLE_NINE, 95 | 'MJw1': TILE_TYPE.WAN_ONE, 96 | 'MJw2': TILE_TYPE.WAN_TWO, 97 | 'MJw3': TILE_TYPE.WAN_THREE, 98 | 'MJw4': TILE_TYPE.WAN_FOUR, 99 | 'MJw5': TILE_TYPE.WAN_FIVE, 100 | 'MJw6': TILE_TYPE.WAN_SIX, 101 | 'MJw7': TILE_TYPE.WAN_SEVEN, 102 | 'MJw8': TILE_TYPE.WAN_EIGHT, 103 | 'MJw9': TILE_TYPE.WAN_NINE, 104 | } 105 | 106 | def __init__(self): 107 | self.resources = dict() 108 | pass 109 | 110 | def load(self, src_dir): 111 | imgs = glob.glob(os.path.join(src_dir, '*.png')) 112 | for img in imgs: 113 | t = os.path.splitext(os.path.basename(img))[0] 114 | if t not in Resource.fn2type: 115 | self.resources['hidden'] = pygame.image.load(img) 116 | continue 117 | self.resources[Resource.fn2type[t]] = pygame.image.load(img) 118 | 119 | 120 | if __name__ == '__main__': 121 | print(TILE_TYPE.CIRCLE(3)) 122 | print(TILE_TYPE.CIRCLE_ONE.value * 90) 123 | # assert TileType.CIRCLE.ONE == TileType.CIRCLE.ONE 124 | # a = [TileType.CIRCLE.ONE, TileType.CIRCLE.ONE] 125 | # a.remove(TileType.CIRCLE.ONE) 126 | # print(a) 127 | # assert TileType.same_type(TileType.CIRCLE.ONE, TileType.CIRCLE.THREE) 128 | # print(type(TileType.CIRCLE.ONE)) -------------------------------------------------------------------------------- /sprites.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class Tile(pygame.sprite.Sprite): 5 | def __init__(self, image, type): 6 | pygame.sprite.Sprite.__init__(self) 7 | 8 | self.image = image 9 | self._type = type 10 | self.rect = self.image.get_rect() 11 | 12 | 13 | class HandTile(Tile): 14 | def __init__(self, image, type): 15 | Tile.__init__(self, image, type) 16 | 17 | 18 | class Button(pygame.sprite.Sprite): 19 | def __init__(self, color, width, height): 20 | pygame.sprite.Sprite.__init__(self) 21 | 22 | self.image = pygame.Surface([width, height]) 23 | self.image.fill(color) 24 | self.rect = self.image.get_rect() 25 | 26 | def on_click(self): 27 | pass 28 | 29 | 30 | class Text(pygame.sprite.Sprite): 31 | pygame.font.init() 32 | font = pygame.font.SysFont('wenquanyimicroheimono', 30) 33 | 34 | def __init__(self, text, pos): 35 | pygame.sprite.Sprite.__init__(self) 36 | 37 | self.image = Text.font.render(text, True, (0, 204, 102)) 38 | # self.image.fill(color) 39 | self.rect = self.image.get_rect() 40 | self.rect.x, self.rect.y = pos[0], pos[1] 41 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import re 3 | import pickle 4 | import sys 5 | import struct 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | sys.path.insert(0, './build') 9 | # from clib import State, TileType 10 | 11 | if __name__ == '__main__': 12 | # a = np.random.rand(1000) 13 | # plt.plot(a) 14 | # plt.show() 15 | # b = a 16 | # for i in range(100): 17 | # b = np.convolve(b, a) 18 | # b /= np.sum(b) 19 | # plt.plot(b) 20 | # plt.show() 21 | # t = TileType.BAMBOO_ONE 22 | # s = State(0, 0, [t], 0) 23 | # d = set() 24 | # with open('AgariIndex.java') as f: 25 | # while True: 26 | # l = f.readline() 27 | # if not l: 28 | # break 29 | # if 'tbl.put' in l: 30 | # k = int(re.findall('0[Xx]\w+', l)[0], 0) 31 | # d.add(k) 32 | # with open('complete.pickle', 'wb') as f: 33 | # pickle.dump(d, f) 34 | with open('complete.pickle', 'rb') as f: 35 | b = pickle.load(f) 36 | b = np.asarray([*b], dtype=np.int32) 37 | print(b.shape) 38 | b.tofile('complete.bin') 39 | 40 | -------------------------------------------------------------------------------- /tiles/MJd1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJd1.png -------------------------------------------------------------------------------- /tiles/MJd2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJd2.png -------------------------------------------------------------------------------- /tiles/MJd3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJd3.png -------------------------------------------------------------------------------- /tiles/MJf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJf1.png -------------------------------------------------------------------------------- /tiles/MJf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJf2.png -------------------------------------------------------------------------------- /tiles/MJf3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJf3.png -------------------------------------------------------------------------------- /tiles/MJf4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJf4.png -------------------------------------------------------------------------------- /tiles/MJs1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs1.png -------------------------------------------------------------------------------- /tiles/MJs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs2.png -------------------------------------------------------------------------------- /tiles/MJs3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs3.png -------------------------------------------------------------------------------- /tiles/MJs4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs4.png -------------------------------------------------------------------------------- /tiles/MJs5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs5.png -------------------------------------------------------------------------------- /tiles/MJs6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs6.png -------------------------------------------------------------------------------- /tiles/MJs7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs7.png -------------------------------------------------------------------------------- /tiles/MJs8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs8.png -------------------------------------------------------------------------------- /tiles/MJs9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJs9.png -------------------------------------------------------------------------------- /tiles/MJt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt1.png -------------------------------------------------------------------------------- /tiles/MJt2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt2.png -------------------------------------------------------------------------------- /tiles/MJt3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt3.png -------------------------------------------------------------------------------- /tiles/MJt4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt4.png -------------------------------------------------------------------------------- /tiles/MJt5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt5.png -------------------------------------------------------------------------------- /tiles/MJt6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt6.png -------------------------------------------------------------------------------- /tiles/MJt7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt7.png -------------------------------------------------------------------------------- /tiles/MJt8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt8.png -------------------------------------------------------------------------------- /tiles/MJt9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJt9.png -------------------------------------------------------------------------------- /tiles/MJw1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw1.png -------------------------------------------------------------------------------- /tiles/MJw2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw2.png -------------------------------------------------------------------------------- /tiles/MJw3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw3.png -------------------------------------------------------------------------------- /tiles/MJw4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw4.png -------------------------------------------------------------------------------- /tiles/MJw5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw5.png -------------------------------------------------------------------------------- /tiles/MJw6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw6.png -------------------------------------------------------------------------------- /tiles/MJw7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw7.png -------------------------------------------------------------------------------- /tiles/MJw8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw8.png -------------------------------------------------------------------------------- /tiles/MJw9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/MJw9.png -------------------------------------------------------------------------------- /tiles/hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq456cvb/Mahjong/196411a5d476c3f43ce6661dfb935ec90a939674/tiles/hidden.png --------------------------------------------------------------------------------